Don't worry, this is the last one for today, but probably the trickiest.
These two functions are intended to help users who want to check how their API behaves with a type checker: they want to assert that some type is inferred correctly, or that some code that they want the type checker to catch is rejected.
# Specification
The signature of typing.assert_type() is:
def assert_type(obj: T, typ: TypeForm[T], /) -> T: ...
(Using TypeForm from David Foster's pending proposal.)
At runtime, this simply returns the first argument unchanged. As with my proposed behavior for reveal_type(), this allows using the function within an expression.
A static type checker will emit an error if it encounters a call to assert_type() and the inferred type for the obj argument is not exactly equal to the provided type. The type passed in as the `typ` argument is interpreted the same way the type checker would interpret a type annotation, just as with the first argument to the existing `typing.cast()` function.
For example:
def f(x: int):
assert_type(x, int) # ok
assert_type(x, str) # error: type of x is int, not str
assert_type(x, Any) # error: type of x is int, not Any
The signature of typing.assert_error() is:
def assert_error() -> ContextManager[None]: ...
At runtime, this returns a context manager that does nothing.
At type checking time, the context asserts that an error is found within the block. A static type checker will handle a `with assert_error():` block as follows:
- Type check the block as normal, but don't emit any errors to the user
- If there were no errors, emit an error
- If there were errors, continue as normal
For example:
with assert_error(): # ok
1 + "x"
with assert_error(): # error
1 + 1
# Motivation
There has been a longstanding need for a way to check that typed code is understood correctly by the type checker:
Adding these two functions to the standard library with defined semantics will create a portable, well-documented way to test type checker behavior. This will be useful for typeshed and for any library author who needs to support multiple type checkers.
# Use case
As a motivating example, I'd like to go over a recent change I made to typeshed:
https://github.com/python/typeshed/pull/6941. I wanted to make it so that you cannot call `dict.setdefault()` with one argument if the dict's value type does not include `None`, so I wrote some tricky overloads.
With `assert_type()` and `assert_error()`, I could have written tests like these to make sure my stubs work with all type checkers:
int_int: dict[int, int] = {}
int_any: dict[int, Any] = {}
int_optional: dict[int, int | None] = {}
with assert_error():
int_int.setdefault(1) # should error because this may set the value to None
assert_type(int_int.setdefault(1, 1), int)
assert_type(int_any.setdefault(1), Any)
assert_type(int_optional.setdefault(1), int | None)
What I did instead was run some samples manually with mypy, but that of course doesn't guarantee that the sample continues to work.
This example comes from typeshed, but the utility isn't limited to typeshed; it can help any user who wants to make sure the types they wrote behave as they expect.
# Open issues
The assert_error() context manager is unlike other existing special forms in the typing module, since no other context manager currently has special meaning to the type checker. I'd be happy to hear any alternative ideas for covering this use case.
Here are two alternative approaches I considered:
- A magical comment, like TypeScript's "// $ExpectError". But a magical comment is not as explicit, is harder to understand for users, and may cause problems with autoformatters.
- A function call: perhaps `typing.assert_type(expr, typing.Error)` would assert that evaluating `expr` produces an error. But this wouldn't allow asserting that an error occurs in a statement as opposed to an expression.
In
https://github.com/python/typing/discussions/1030, we explored several variants for the behavior of assert_type(), such as allowing an explicit way to provide the type as a string. I think the behavior specified here is the most intuitive and consistent with the behavior of other typing constructs, but I'm happy to discuss this further.