typeguard
typeguard checks at run time that values actually match the type annotations you already wrote. mypy and pyright only look at your source; typeguard looks at the objects flowing through it, so a dict that arrives from JSON claiming to be a list of Person gets caught the moment it is wrong. It offers three levels of intrusion. check_type(value, SomeType) is an isinstance that understands typing constructs like Sequence[int], TypedDict, Literal, and Protocol. The @typechecked decorator rewrites one function or class so its arguments, return value, yields, and annotated local assignments are all checked. install_import_hook does the same rewriting to every module in a package as it is imported, with no changes to that code at all. The usual pattern is checks on in tests and CI, off in production.
As a test-time safety net, typeguard is the most thorough run-time checker in Python and the pytest plugin makes adopting it a one-line change. For always-on checking in production code, beartype is the pragmatic choice and pydantic is the right tool at the edges where data enters your system.
Use it if
- You are loading data from JSON, YAML, or a message queue and want the shape verified against an existing TypedDict or dataclass annotation instead of writing a parallel validation schema
- You want your test suite to catch annotation lies; the pytest plugin instruments your packages with one command-line flag and nothing in your source changes
- You have a large codebase where a static checker cannot see everything, such as plugin loading, dynamic dispatch, or values crossing a C extension boundary
- You want checks on generator yield and send values, or on assignments to annotated local variables, which no other run-time checker covers as thoroughly
- You need speed on a hot path. Instrumented functions do real work per call, and beartype's constant-time sampling approach is the usual answer when the checking itself shows up in a profile
- Your code is not available as source at run time. Instrumentation reads and re-parses the original source, so Cython-compiled modules, some frozen or packaged applications, and decorated-then-wrapped functions fall outside what it can rewrite
- You want parsing and coercion, not just checking. typeguard tells you a str is not an int; pydantic turns "5" into 5 and gives you a validated model, which is what an API boundary usually needs
- You assume every element of a collection is checked. The default collection_check_strategy is FIRST_ITEM, so a list whose first entry is an int and whose second is a str passes quietly until you switch to ALL_ITEMS
- You are stuck on Python 3.9 or earlier. 4.6.0 dropped 3.9, and the 3.x line before it was a full rewrite with a different API, so old pins are a dead end
- You already stack another import hook. The docs warn it may clash, and two AST-rewriting hooks fighting over the same modules is a bad afternoon
Setup reality
pip install typeguard is a pure Python package with one dependency, typing_extensions, and nothing to compile. Getting value out of it takes more thought than installing it. The import hook has to be installed before the target modules are imported, so a stray top-level import in your conftest or __init__ silently means half your package is never instrumented. @typechecked has to be the innermost decorator, because it works by recompiling the function it wraps, and anything already wrapping that function hides it. check_type deliberately ignores the global typeguard.config object, so settings you tuned for the decorator do not apply and you pass them per call instead. The pytest route, pytest --typeguard-packages=myapp, is the least fragile way in, and --typeguard-packages-ignore exists for the modules that do not tolerate rewriting.
Patterns
isinstance that understands typingcheck-type-directly
from typeguard import check_type
check_type([1, 2, 3], list[int]) # passes, returns the value
check_type({"a": 1}, dict[str, str]) # raises TypeCheckErrorIt returns the value on success, so you can inline it. Note that check_type ignores the global typeguard.config; pass forward_ref_policy or collection_check_strategy as keyword arguments here instead.
Check parsed JSON against a TypedDictvalidate-external-json
import json
from typing import TypedDict
from typeguard import check_type
class Person(TypedDict):
name: str
phone: str
with open("people.json") as f:
people = check_type(json.load(f), list[Person])
# static checkers now treat `people` as list[Person]This is the safe replacement for typing.cast, which asserts nothing. It validates rather than coerces, so a phone number that arrives as a number stays an error instead of becoming a string.
Instrument one functiontypechecked-function
from typeguard import typechecked
@typechecked
def send(user_id: int, tags: list[str]) -> bool:
...
return TruePut it closest to the function, below every other decorator. It recompiles the function from source, so if another decorator has already wrapped it there is nothing left to instrument and the checks silently never run.
Instrument every method on a classtypechecked-class
from typeguard import typechecked
@typechecked
class Repository:
def get(self, key: str) -> bytes:
...
@property
def size(self) -> int:
...Covers static methods, class methods, and properties, but not inner classes and not methods that already carry a wrapping decorator. Nested classes need their own decorator.
Instrument a whole package on importimport-hook
from typeguard import install_import_hook
install_import_hook("myapp", ignore_packages=["myapp.vendor"])
import myapp.service # must come AFTER the hookAnything imported before the hook is installed keeps running unchecked, which is the number one reason people think the hook did nothing. It also returns a manager with uninstall(), and works as a context manager.
Turn checks on for the test suite onlypytest-plugin
pytest --typeguard-packages=myapp,mylib \
--typeguard-packages-ignore=myapp.legacy
# or in pyproject.toml
# [tool.pytest.ini_options]
# typeguard-packages = "myapp"The plugin installs the import hook early enough in the pytest startup to catch your package. This is the lowest-risk way to adopt typeguard: full checking in CI, zero overhead in production.
Stop it from checking only the first elementcheck-all-collection-items
from typeguard import CollectionCheckStrategy, typechecked
@typechecked(collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS)
def ingest(rows: list[int]) -> None:
...
# or globally, for the decorator and import hook
import typeguard
typeguard.config.collection_check_strategy = CollectionCheckStrategy.ALL_ITEMSFIRST_ITEM is the default because checking every element of a large list on every call is expensive. It also means [1, "two"] passes as list[int], which surprises people who trusted the check.
Log failures rather than blowing upwarn-instead-of-raise
import typeguard
from typeguard import warn_on_error
typeguard.config.typecheck_fail_callback = warn_on_errorEmits a TypeCheckWarning instead of raising, which is how you roll checks out over an existing codebase without breaking production. Combine with pytest's -W error to make the same warnings fatal in CI.
Catch and report a failurehandle-check-errors
from typeguard import TypeCheckError, check_type
try:
config = check_type(raw, AppConfig)
except TypeCheckError as exc:
log.error("bad config: %s", exc)
raise SystemExit(1)The message names the exact path, for example 'item 2 of argument "rows" is not an int', which is usually enough to find the offender without a traceback dig.
Skip checking inside a hot blocksuppress-checks
from typeguard import suppress_type_checks
with suppress_type_checks():
for row in million_rows:
process(row) # instrumented, but checks are inert hereWorks as a context manager or a decorator and is process-wide while active, not thread-local, so do not reach for it in concurrent code expecting isolation.
Validate what a generator yieldscheck-generator-yields
from collections.abc import Iterator
from typeguard import typechecked
@typechecked
def rows() -> Iterator[tuple[int, str]]:
for raw in source:
yield parse(raw) # each yielded value is checkedInstrumentation checks yield and send values, which plain decorators cannot do because the annotation describes the generator, not the individual items.
Keep TYPE_CHECKING-only imports workingtype-checking-imports
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from expensive.module import Heavy
@typechecked
def handle(obj: "Heavy") -> None:
...typeguard recognizes module-level TYPE_CHECKING blocks and substitutes Any for those names rather than failing to resolve them. For unresolvable forward references elsewhere, typeguard.config.forward_ref_policy chooses between ERROR, WARN, and IGNORE.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| beartype | PyPI | You want run-time checks cheap enough to leave on in production, with a plain decorator and no AST rewriting. |
| pydantic | PyPI | The values come from outside your process and you want parsing, coercion, and error reports rather than assertions. |
| trycast | PyPI | You only need to test whether a JSON-shaped value matches a TypedDict and branch on the result instead of catching an exception. |