typing
typing copied to clipboard
Request: an AssertingTypeGuard type for TypeGuard-like semantics
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, https://github.com/microsoft/pyright/issues/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.)
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
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.
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]
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
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]