beartype type-checks a whole list by reading one random item of it
A list of bytes where the annotation promised strings slips past mypy at author time and trips beartype's decorator at the call, which names the exact item that broke the hint.
The call boundary a static checker stops watching
A type hint is a promise the author writes and a static checker keeps
before the program runs. Point mypy or pyright at a function
annotated list[str] and it proves, at author time, that
every call it can see passes a list of strings. That proof covers the
code the checker reads. It says nothing about the value that arrives
at runtime from a place the checker cannot read: a JSON payload, a
database row, a third-party function annotated to return
Any. When that value is wrong, the body runs on it
anyway, and the failure surfaces deep inside the call with a message
that never mentions the promise that was broken.
beartype is a pure-Python decorator that closes that gap.1
Put @beartype on the function and the same hint mypy read
at author time becomes a check that runs at the call, on the live
object.2 Pass a real
list of strings and nothing happens. Pass a list of
bytes, and beartype raises before the body executes,
naming the parameter, the hint, and the item that failed it.
from beartype import beartype
@beartype
def render_row(cells: list[str]) -> str:
return " | ".join(cells)
# cells arrived from a parser annotated `-> Any`, so mypy proved nothing about it
render_row([b"2026-08-17", b"OK"])
# beartype.roar.BeartypeCallHintParamViolation: Function render_row()
# parameter cells=[b'2026-08-17', b'OK'] violates type hint list[str],
# as list index 0 item b'2026-08-17' not instance of str.
list[str]; beartype raises at the call and names the
offending item. The violation follows the shape beartype
documents.1
The exception is a BeartypeCallHintParamViolation, from
the family of call-time violations beartype's own reference singles out
as its most important.3
Without the
decorator, " | ".join would still raise, but from inside
the join, complaining about a bytes object with no word about
render_row or its contract. beartype did not coerce the
bytes into text or repair the input. It asserted that the value
already matched the hint, found that it did not, and stopped.
beartype writes the check once, at decoration time
The expensive work happens once, when Python first evaluates the
decorator, not on every call. beartype reads the hint and generates a
wrapper function as Python source, tailored to that exact
hint.2 The wrapper it
builds for list[str] holds the literal checks for a list
and for a string, with no hint left to interpret when the call comes.
That is what the project means when it advertises "O(1) non-amortized
worst-case time with negligible constant factors": the per-call work
is a short, pre-written function, and the reading of the hint already
happened.1
Generating code instead of interpreting the hint has two consequences
worth naming. The wrapper closes over only what it needs. When a hint
requires picking a container item at random, beartype binds Python's
standard-library getrandbits into the wrapper's namespace,
and only then.4 And the
whole design carries nothing underneath it: "Beartype has no
install-time or runtime dependencies."2 The thing you add to catch type errors does not
enlarge your supply chain.
One random item per level, and why the list's size drops out
For a flat list[str], the generated wrapper does not
iterate the list. It confirms the object is a list, then checks a single item
chosen at random.5 A
nested hint such as list[tuple[Sequence[str]]] makes it
walk down one level per step. The object must be a list. A random item
of that list must be a tuple, a random item of that tuple a
Sequence, and a random item of that a string. beartype
calls this "a one-way random tree walk over the
expected nested structure of those objects at call time," and it
samples one item per level of nesting.5 A structure nested three deep costs three
checks, whatever the width at each level.
This is what the O(1) claim means, and what it does not. The per-call cost is independent of how many items sit at each level: a list of ten strings and a list of ten million cost the same single check. It is not independent of nesting depth, and it is not zero. beartype still runs a real wrapper on every call, and the constant it advertises is a small constant rather than an absent one. Its headline figure, roughly a microsecond to check one access into a list of a million two-tuples, is a number the project measured on itself, with no independent reproduction I could find.5 Read it as beartype's claim about beartype.
What checking one item cannot promise
The trade the sampling makes is real, and beartype documents it against itself. Checking one random item per level means a container can pass a check it should fail.
from beartype import beartype
@beartype
def mean(xs: list[float]) -> float:
return sum(xs) / len(xs)
one_bad = [1.0, 2.0, "3.0"] + [0.0] * 10_000 # one string among ten thousand floats
mean(one_bad) # passes on a call that samples a float; raises only on a call
# that happens to sample the string. The same input can do both.
A list uniformly of the wrong type, as in the first example, is caught every call, because every item the sample could land on is wrong. A lone bad item in a large right list is the case the sampling can miss, and beartype says so plainly.
beartype also invites false negatives, because this approach only checks a vertical slice of the full container structure each call, which is bad. We claim without evidence that false negatives are unlikely under the optimistic assumption that most real-world containers are homogeneous.5 beartype FAQ
There is a second half to the limit: you cannot ask for the full walk
instead. beartype defines four checking strategies. O0 is
a no-op, O1 is the default single-random-item check, and
Ologn and On, the log-many and
check-everything strategies, are named in the configuration but
"currently unimplemented."6
Random sampling is beartype's default, and today it is also its only
container check: no setting makes it inspect more of a container.
beartype, typeguard, and pydantic answer different questions
beartype, typeguard, and pydantic all run at the call, and each does something different with a value that fails its hint.
| Tool | What runs, and what happens to the value |
|---|---|
| mypy / pyright | Nothing runs at the call; a mismatch is flagged only when you run the checker at author time. |
| @beartype | Raises BeartypeCallHintParamViolation; the value is left unchanged. |
| @typechecked | Raises TypeCheckError; the value is left unchanged. For collections it checks the first item only by default. |
| @validate_call | Coerces "3" to the integer 3 and passes it on; validates every item, not a sample. |
typeguard is the closest to beartype. Since version 3 it also checks
one item per collection by default, so the per-call cost is
similar.714 Two things
differ. Its sample is the first item rather than a random one, so a bad
value at index 0 is always caught and a bad value anywhere else never
is. And typeguard keeps the exhaustive option beartype dropped:
CollectionCheckStrategy.ALL_ITEMS restores a full
walk.7 Underneath, it
rewrites the abstract syntax tree rather than generating a wrapper, so
its decorator "will not work when there is another decorator wrapping
the function" and must sit innermost.7
pydantic's @validate_call does a different job. It coerces
before it checks: "types are by default coerced by the decorator before
they're passed to the actual function," so a "3" becomes
3 and the call proceeds.8 It validates every argument and is built to parse
untrusted input. beartype changes nothing about the values it inspects;
pydantic's purpose is to change them. Reach for pydantic at the edge of
the system where data arrives, and for beartype inside it, where you
want to assert that values already are what the hints say.
beartype's own coverage of the hint surface is broad but not total. The
mainstream annotations are fully supported; several advanced constructs
are only partial, including TypedDict,
ParamSpec, and the variadic generics of
TypeVarTuple, and a few of the newest PEPs are not
supported at all.9 If your
code leans on the newest typing features, read the coverage table
before assuming beartype enforces them.
Whether a one-maintainer decorator is safe to leave on
You do not decorate every function by hand. beartype's import hook
decorates a whole package from its __init__.py: a single
call to beartype_this_package() checks every annotated
callable and class in the package as it imports.10 A broader beartype_all() exists,
and the docs tell you not to reach for it: "most codebases should not
call this hook."10
The package is MIT-licensed, requires Python 3.10 or newer, and its current stable release is 0.22.9, from December 2025.11 It has shipped point releases roughly monthly across 2025, with 0.23.0 now in release candidate.12 It is essentially one person's project, maintained by Cecil Curry, and that is the main risk to weigh: the bus factor is one.11 Against it, the zero-dependency design adds no third-party supply-chain surface when you adopt it,2 and the roughly 108 open issues cluster on advanced-hint support and edge cases rather than core breakage.13 The release notes claim a milestone of a million downloads a day.12 Treat the exact figure as self-reported, though the direction is clear enough that a single maintainer stepping away would strand a great deal of code.
Leave beartype on where values cross into typed code from untyped
sources, and treat its check as a smoke detector rather than a proof.
It reliably catches a container whose items are uniformly wrong, and
it will let a lone bad item in a large one through more often than
not.5 If you need every
item verified, beartype cannot promise that today, where typeguard's
ALL_ITEMS can.67 Implementing the On strategy, or
gaining a second maintainer, is what would raise the assessment.
Sources
- GitHub · beartype README
- beartype documentation · Beartype in five (or fewer) minutes
- beartype documentation · Exceptions and warnings (beartype.roar)
- GitHub · beartype type-check code factory (codemain.py)
- beartype documentation · Frequently asked questions
- beartype documentation · Decorator API (BeartypeStrategy)
- typeguard documentation · User guide
- pydantic documentation · Validation decorator
- typeguard documentation · Version history
- beartype documentation · PEP compliance
- beartype documentation · Import hooks (beartype.claw)
- PyPI · beartype
- GitHub · beartype releases
- GitHub · beartype issue tracker