Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request: an AssertingTypeGuard type for TypeGuard-like semantics #930

Open
sirosen opened this issue Nov 9, 2021 · 5 comments
Open

Request: an AssertingTypeGuard type for TypeGuard-like semantics #930

sirosen opened this issue Nov 9, 2021 · 5 comments
Labels
topic: feature Discussions about new features for Python's type annotations

Comments

@sirosen
Copy link
Contributor

sirosen commented Nov 9, 2021

There is a frequent pattern of TypeGuard-like functions which assert or otherwise raise an exception if a type constraint is not met.

For example, microsoft/pyright#2007 points to a case in which unittest provides assertIsNotNone, but a type-checker cannot infer that type narrowing has occurred. Arguably, the popular typeguard library is based around an implementation of "asserting" type guards. (One which deduces what assertions should be made from the annotations.)

TypeGuards allow for semantics like

y: str
assert is_list_of_str(x)
assert len(x) > 0
y = x[0]

An AssertingTypeGuard would allow for

y: str
assert_is_nonempty_list_of_str(x)  # note, this encodes another runtime check, the len check
y = x[0]

This becomes especially valuable if we consider that you might not want to do this all with assert. I may, as an author, prefer my own custom exceptions, e.g.

def assert_is_nonempty_list_of_str(x) -> AssertingTypeGuard[list[str]]:
    if not isinstance(x, list):
        raise ExpectedListError(x)
    if not x:
        raise EmptyContainerError(x)
    if not all(isinstance(y, str) for y in x):
        raise ContainedInvalidTypeError(x, str)
    return x

(Apologies if this repo is the wrong place to submit this request/idea. I'm happy to go through another process if necessary.)

@srittau srittau added the topic: feature Discussions about new features for Python's type annotations label Nov 9, 2021
@AlexWaygood
Copy link
Member

I like this idea, and have been mulling something similar myself. The name floating around my head was FailsUnless:

def assert_is_nonempty_list_of_str(x: object) -> FailsUnless[list[str]]:
    if not isinstance(x, list):
        raise ExpectedListError(x)
    if not x:
        raise EmptyContainerError(x)
    if not all(isinstance(y, str) for y in x):
        raise ContainedInvalidTypeError(x, str)
    return x

@JelleZijlstra
Copy link
Member

pyanalyze provides a version of this, though there's no Python-level syntax for it yet. They're called "no_return_unless constraints". I have found it useful in two main contexts:

  • Assertion helpers like those from qcore.asserts, so that they provide narrowing like normal assert does.
  • Container mutators like list.append. In pyanalyze we type those as returning a constraint that sets the list that is being appended to to a new value, which helps with precise type inference for containers and makes it so pyanalyze generally doesn't need explicit types for them.

@KotlinIsland
Copy link

KotlinIsland commented Nov 22, 2021

Container mutators

That sounds incredibly unsafe, eg:

def foo(the_list: list[str]):
    the_list[0].upper()  # SUS ALERT

l1 = []
l2 = l1

l2.append(1)
l1.append("AMONGUS")

foo(l1)  # is l1 a list[str] or a list[str | int]

@sirosen
Copy link
Contributor Author

sirosen commented Nov 29, 2021

They're called "no_return_unless constraints".

That seems consistent with typing.NoReturn. Would NoReturnGuard be a viable name? The idea is a mash-up of NoReturn and TypeGuard in some ways.

Container mutators

That sounds incredibly unsafe, eg: ...

This seems OT to me. TypeGuard and cast already let you do dangerous stuff. I have no opinion on the container-mutator case in particular, but I don't want to derail into discussing it.

I'm focused more on cases like

# some web application with a DB

def check_exists(db_object: Optional[DBModelBase]) -> NoReturnGuard[DBModelBase]:
    if db_object is None:
        raise HTTPNotFound(db_object)

maybe_user = get_user(user_id)
check_exists(maybe_user)
reveal_type(maybe_user)  # DBUser

PeterJCLaw added a commit to PeterJCLaw/srcomp that referenced this issue Mar 26, 2023
mypy doesn't yet understand that `assertIsNotNone` validates the
non-None nature of the variable.

Related issues:
- python/mypy#5088
- python/mypy#4063
- python/typing#930
@fwiesweg
Copy link

fwiesweg commented Apr 25, 2023

Something like the following gets nearly what you want (I'm currently using it in a project), but I admit it'd be a bit neater to have a real assert-like custom guard, which would also work with non-assignable variables and types which happen not to be classes.

class ParentClass:
	a: int


class ChildClass(ParentClass):
	b: str


T = typing.TypeVar('T', bound=ParentClass)


def assert_type(instance: ParentClass, expected_type: type[T]) -> T:
	if not isinstance(instance, expected_type):
		raise ValueError()
	return instance


variable: ParentClass = ParentClass()
variable = assert_type(variable, ChildClass)
print(variable.a)
print(variable.b)
print(variable.c)

variable = assert_type(variable, ParentClass)
print(variable.a)
print(variable.b)
print(variable.c)

Mypy 1.1.1 returns:

error: "ChildClass" has no attribute "c"  [attr-defined]
error: "ParentClass" has no attribute "b"  [attr-defined]
error: "ParentClass" has no attribute "c"  [attr-defined]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: feature Discussions about new features for Python's type annotations
Projects
None yet
Development

No branches or pull requests

6 participants