typing.assert_type and typing.reveal_type help developers understand and test static inference. reveal_type() asks a checker to display the inferred type of an expression. assert_type() declares the type you expect, turning that expectation into a static test that fails when inference changes.
These tools are valuable for libraries, overloads, generics, Protocols, TypedDicts, decorators, narrowing functions, and public APIs. This guide covers exploration, automated typing tests, CI, runtime behavior, Any leakage, variadic types, Self, and checker differences.
What reveal_type does
from typing import reveal_type
value = [1, 2, 3]
reveal_type(value)A checker may report list[int]. The diagnostic appears during static analysis. Runtime behavior is secondary and may print a message, depending on the implementation.
Revealing intermediate expressions
data: dict[str, int | None] = {"a": 1}
value = data.get("a")
reveal_type(value)The result should be a union such as int | None. That explains why a check is required before arithmetic.
Control-flow narrowing
if value is not None:
reveal_type(value)
print(value + 1)Inside the branch, the type should narrow to int. reveal_type confirms whether the checker interpreted the condition as expected.
What assert_type does
from typing import assert_type
result = len("python")
assert_type(result, int)The checker compares the inferred type with the expected type. At runtime, the function returns the first argument and does not convert or validate it.
assert_type is not isinstance
value: object = 10
assert_type(value, int) # expected static failureThe concrete runtime object is an integer, but the declared static type is object. assert_type tests available static information, not the observed runtime value.
Testing a generic function
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
assert_type(first([1, 2]), int)
assert_type(first(["a", "b"]), str)These checks confirm that the type parameter propagates to the return value.
Testing overloads
from typing import Literal, overload
@overload
def read(*, binary: Literal[False] = False) -> str: ...
@overload
def read(*, binary: Literal[True]) -> bytes: ...
def read(*, binary: bool = False) -> str | bytes:
return b"data" if binary else "data"
assert_type(read(), str)
assert_type(read(binary=True), bytes)Changing overload order or signatures may degrade inference. assert_type catches the regression without executing the function.
Testing Literal inference
mode = "fast"
reveal_type(mode)
mode_literal: Literal["fast"] = "fast"
assert_type(mode_literal, Literal["fast"])A mutable variable may widen to str, while an explicit annotation preserves the literal value.
Testing TypedDict
from typing import TypedDict
class User(TypedDict):
id: int
name: str
user: User = {"id": 1, "name": "Ana"}
assert_type(user["id"], int)
assert_type(user["name"], str)Optional keys should be tested before access. reveal_type is useful before and after membership checks.
Testing TypeGuard and TypeIs
from typing import TypeGuard
def is_str(value: object) -> TypeGuard[str]:
return isinstance(value, str)
item: object = "x"
if is_str(item):
assert_type(item, str)The test confirms narrowing in the true branch. For TypeIs, also test the false branch. See the Python TypeGuard guide.
Testing Protocols
from typing import Protocol
class Closable(Protocol):
def close(self) -> None: ...
class File:
def close(self) -> None: ...
file = File()
assert_type(file, File)
closable: Closable = file
assert_type(closable, Closable)assert_type checks the static type of the variable rather than merely structural compatibility of the original expression.
Decorators and signature loss
@my_decorator
def find(id_: int) -> str:
...
reveal_type(find)
assert_type(find(1), str)Poorly typed decorators often turn precise functions into Callable[..., Any]. ParamSpec and TypeVar can preserve the contract. See Python ParamSpec.
Static test files
Create files such as tests/typing/test_api.py. They do not need to run under pytest; CI runs a checker over them. These files document the type experience expected by library users.
Positive and negative tests
assert_type expresses successful inference. Calls that should fail require checker-specific expected-error comments or typing test tools. Keep negative cases close to the public API they protect.
Do not depend on diagnostic wording
reveal_type messages vary across checkers and versions. Use assert_type for automated expectations and reveal_type for exploration.
Equivalent types
Different syntax may represent equivalent types, and union order may differ. The checker decides equivalence rather than comparing text output.
Any can hide failures
If a dependency returns Any, many operations pass unchecked. Add assert_type tests at boundaries and enable strict Any-related settings. reveal_type quickly shows where Any entered the API.
Never and unreachable branches
In exhaustive code, reveal_type may report Never. That confirms a union has been completely handled. The Python Never guide explains assert_never().
Self and fluent methods
builder = SpecialBuilder().configure()
assert_type(builder, SpecialBuilder)This verifies that a method returning Self preserves subclasses.
Variadic types
result = add_prefix((1, "a"))
assert_type(result, tuple[str, int, str])TypeVarTuple and Unpack can produce complex inference. Static assertions document the exact positional relationship.
Runtime behavior
assert_type(value, Type) returns value without checking it. reveal_type also does not replace runtime validation. Never use either function to protect user input.
Remove exploratory reveals
reveal_type is useful during development but may create runtime output or clutter production source. Keep diagnostic calls in dedicated typing tests or remove them after investigation.
Version compatibility
Use typing_extensions.assert_type where needed. Static behavior depends heavily on the checker version. Pin checker versions in CI and review upgrades deliberately.
Comparing checkers
Mypy and pyright may infer complex expressions differently. If a library supports both, run the same typing suite with both and avoid relying on unspecified behavior.
Common mistakes
- Expecting runtime validation: assert_type does not call isinstance.
- Leaving reveal_type in production paths: it may produce unnecessary output.
- Testing only implementation internals: test the consumer-facing API.
- Allowing Any to spread unnoticed: assertions lose value.
- Comparing diagnostic strings: wording changes.
- Not pinning the checker: upgrades may alter inference.
Complete example: cache API
from typing import TypeVar, overload, assert_type
T = TypeVar("T")
_MISSING = object()
@overload
def get(key: str) -> object: ...
@overload
def get(key: str, default: T) -> object | T: ...
def get(key: str, default: object = _MISSING) -> object:
...
assert_type(get("x"), object)
assert_type(get("x", 0), object | int)
assert_type(get("x", None), object | None)The tests document the relationship between defaults and return types. CI reports a regression when overloads or stubs change.
Library testing strategy
Create realistic consumer files. Import the installed library rather than internal modules. Test return inference, expected failures, subclasses, overloads, generics, and stub packaging. Public typing can break even when runtime tests remain green.
Conclusion
reveal_type() is a lens into checker inference. assert_type() turns an expected type into a regression test. Together they make typed APIs more predictable and maintainable.
The official Python assert_type and reveal_type documentation defines their behavior. Use reveal_type to explore, assert_type to automate, and run the tests with the exact checker configurations supported by the project.







