beartype
beartype enforces the type hints you already wrote, at runtime. Decorate a function with @beartype and it generates a small wrapper that checks arguments and the return value against their annotations, raising a beartype.roar exception with a message pointing at the exact offending value. The trick that makes it usable in production is its default O(1) strategy: for containers it checks the container type plus one randomly chosen item, so the cost per call stays roughly constant no matter how big the list is. Beyond the decorator there is beartype.claw, an import hook that applies the decorator to every annotated class, function, and even variable assignment across a package without you touching any files, and beartype.door, which gives you is_bearable and die_if_unbearable for checking any object against any hint at any point in your code. It is pure Python with no runtime dependencies and needs Python 3.10 or later.
The cheapest way to make Python annotations mean something at runtime, and the import hook makes adopting it a two-line change. Just be clear with your team that the default check samples one item per container, so it catches shape mistakes rather than proving a collection is homogeneous.
Use it if
- Your code is already annotated and you want those annotations enforced against real data at runtime, not just checked against source by mypy in CI
- You are handling data from outside your process (JSON payloads, database rows, ML tensors, user config) and want the failure to happen at the boundary with a readable message instead of three frames deeper as an AttributeError
- You want per-call overhead you can actually pay: the O(1) strategy means checking a million-element list costs about the same as checking a ten-element one
- You want to switch it on for a whole package in two lines with beartype.claw.beartype_this_package() rather than decorating hundreds of functions by hand
- You need a runtime isinstance() that understands modern hints: is_bearable(x, list[tuple[int, str]] | None) works where isinstance() raises TypeError on subscripted generics
- You want type checking in tests and development but zero cost in production, which BeartypeStrategy.O0 or simply not installing the import hook gives you
- You expect every element of a container to be validated. The default strategy checks one random item, and the two deeper strategies (Ologn and On) are documented in the source as currently unimplemented, so a list[str] holding one int among a thousand strings will usually pass
- You want data coercion or parsing. beartype only checks and raises; it never converts '5' to 5 or builds a model object, so if you are parsing external JSON you want pydantic and not this
- You want to replace a static checker. It cannot see code paths that never run, so it complements mypy or pyright rather than substituting for either
- You are on Python 3.9 or older. The floor is 3.10, and the package pins it, so a legacy service has to upgrade the interpreter first
- You need a version number you can trust for planning. It is still 0.x after more than a decade, so every minor release can and does change behavior, and pinning matters
- Your hot loop calls small annotated functions millions of times. The overhead is small but not zero, and a wrapper on a two-line function called in a tight loop is measurable
- Your team will be reading tracebacks under pressure. The project documentation and error text are written in a heavy joke register that some people find charming and others find hard to skim
Setup reality
pip install beartype pulls nothing else in: no compiler, no wheels to worry about, no runtime dependencies at all on Python 3.10 through 3.14. Turning it on is where decisions start. The decorator route is explicit and boring. The import hook route (beartype.claw.beartype_this_package() at the top of your package __init__) is two lines but has real ordering rules: it must run before the submodules it covers are imported, so putting it below your other imports silently covers nothing. Import hooks also do not apply to modules already in sys.modules, which bites in test suites and in notebooks where you re-run a cell. Third-party code is a separate decision: beartype_all() with a plain config will raise exceptions from libraries you do not control, so the documented pattern is beartype_all(conf=BeartypeConf(violation_type=UserWarning)) to downgrade those to warnings while your own package still raises. Finally, PEP 563 (from __future__ import annotations) turns hints into strings that beartype has to resolve, and forward references to names that are not importable at runtime will fail there rather than at definition time.
Patterns
Check one function's arguments and return valuedecorate-function
from beartype import beartype
@beartype
def repeat(text: str, times: int) -> list[str]:
return [text] * times
repeat('hi', 3) # fine
repeat('hi', '3') # BeartypeCallHintParamViolationPut @beartype closest to the function when stacking decorators, otherwise it wraps the outer decorator's signature instead of yours. Unannotated parameters are simply not checked, which makes partial adoption safe.
Turn it on for a whole packageimport-hook-package
# your_package/__init__.py, at the very top
from beartype.claw import beartype_this_package
beartype_this_package()
# only after this line:
from your_package import api, modelsThe hook only affects modules imported after it runs, so anything already in sys.modules is skipped. That is why it has to be the first statement in __init__, above your own imports.
Raise in your code, warn about everyone else'swarn-on-third-party
from beartype import BeartypeConf
from beartype.claw import beartype_all, beartype_this_package
beartype_this_package()
beartype_all(conf=BeartypeConf(violation_type=UserWarning))beartype_all() without the UserWarning config will crash on the first sloppy annotation in a dependency you cannot patch. Expect a noisy first run either way and use warnings filters to mute known offenders.
Type-check an object outside a function signaturecheck-value-anywhere
from beartype.door import is_bearable, die_if_unbearable
rows: object = load_json()
if is_bearable(rows, list[dict[str, int]]):
total = sum(r['n'] for r in rows)
# assert-style, raises BeartypeDoorHintViolation
die_if_unbearable(rows, list[dict[str, int]])This is the replacement for isinstance() when the hint is subscripted; isinstance(x, list[int]) raises TypeError outright. Same O(1) sampling applies, so is_bearable on a big list is fast but not exhaustive.
Add value constraints on top of a typeconstrain-values
from typing import Annotated
from beartype import beartype
from beartype.vale import Is
NonEmptyStr = Annotated[str, Is[lambda s: bool(s.strip())]]
Port = Annotated[int, Is[lambda n: 1 <= n <= 65535]]
@beartype
def connect(host: NonEmptyStr, port: Port = 5432) -> None:
...Validators run on every check, so keep the lambda cheap: no I/O, no regex compilation inside the body. mypy sees only the base type, so these constraints exist at runtime only.
Catch violations instead of crashinghandle-violations
from beartype.roar import (
BeartypeCallHintParamViolation,
BeartypeCallHintReturnViolation,
)
try:
handle_request(payload)
except BeartypeCallHintParamViolation as exc:
return {'error': 'bad request', 'detail': str(exc)}, 400
except BeartypeCallHintReturnViolation:
log.exception('we produced the wrong shape')
raiseParameter violations mean the caller sent garbage; return violations mean your own code is wrong. Both subclass BeartypeCallHintViolation, so catch the specific one when the distinction should change your HTTP status.
Raise your own exception class on violationcustom-exception-type
from beartype import beartype, BeartypeConf
class PayloadError(ValueError):
pass
@beartype(conf=BeartypeConf(violation_type=PayloadError))
def ingest(record: dict[str, str]) -> None:
...
# or split it: violation_param_type / violation_return_typeUseful when an existing error handler already catches ValueError and you do not want beartype types leaking into your API layer. violation_param_type and violation_return_type override violation_type for that direction only.
Keep checks in development, drop them in productiondisable-in-production
import os
from beartype import BeartypeConf, BeartypeStrategy, beartype
noop = beartype(conf=BeartypeConf(strategy=BeartypeStrategy.O0))
check = beartype if os.getenv('APP_ENV') != 'prod' else noop
@check
def hot_path(xs: list[float]) -> float:
return sum(xs)O0 reduces the decorator to the identity function, so there is no wrapper and no cost. If you use the import hook instead, gate the beartype_this_package() call on the same environment variable.
Enable checking only inside a blockscoped-checking
from beartype.claw import beartyping
with beartyping():
import plugins.risky_plugin
# modules imported outside the block are untouchedHandy in tests and notebooks where you want one suspect module checked without turning the hook on globally. Modules already imported before the block are not re-checked.
Ask whether one hint is narrower than anothercompare-hints
from beartype.door import TypeHint, is_subhint
is_subhint(list[int], list[object]) # True
is_subhint(int | str, int) # False
hint = TypeHint(dict[str, list[int]])
hint.args # (TypeHint(str), TypeHint(list[int]))
TypeHint(int) <= TypeHint(int | str) # TrueThis is the piece to reach for when writing plugin registries or dispatchers that need to reason about annotations. TypeHint wraps a hint into a comparable object so you can sort and dedupe handlers by specificity.
Work with postponed annotation evaluationdeferred-annotations
from __future__ import annotations
from beartype import beartype
@beartype
def link(node: Node, parent: Node | None = None) -> Node:
...
class Node:
passWith PEP 563 every hint is a string that beartype resolves on the first call, so a name that only exists under TYPE_CHECKING will raise then rather than at import. Keep runtime-referenced classes importable at runtime.
Check only your package while the test suite runspytest-integration
# conftest.py at the repo root
from beartype import BeartypeConf
from beartype.claw import beartype_packages
beartype_packages(
('your_package', 'your_package.plugins'),
conf=BeartypeConf(is_debug=True),
)conftest.py loads before your test modules, which is exactly the ordering the hook needs. is_debug prints the generated wrapper source, which is the fastest way to understand why a hint is or is not being checked.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| typeguard | PyPI | You want every element of a container checked rather than a random sample, and you accept the linear cost per call |
| pydantic | PyPI | You need to parse and coerce untrusted input into validated model objects, not just assert that existing objects match their hints |
| mypy | PyPI | You want errors before the code runs, across paths your tests never reach, with no runtime cost at all |