The Nightly Build.
Expert Tools

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.

render_row.pyPYTHON
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.
Fig. 1 · A value from an untyped boundary breaks 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.

mean.pyPYTHON
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.
Fig. 2 · A single wrong item in a large list is caught only when the random sample lands on it, so the same value can pass one call and fail the next.5

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 on its own trade
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.

What each tool does at runtime to an argument annotated x: int, given the string "3".17814
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.

Verdict

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

  1. GitHub · beartype README
  2. beartype documentation · Beartype in five (or fewer) minutes
  3. beartype documentation · Exceptions and warnings (beartype.roar)
  4. GitHub · beartype type-check code factory (codemain.py)
  5. beartype documentation · Frequently asked questions
  6. beartype documentation · Decorator API (BeartypeStrategy)
  7. typeguard documentation · User guide
  8. pydantic documentation · Validation decorator
  9. typeguard documentation · Version history
  10. beartype documentation · PEP compliance
  11. beartype documentation · Import hooks (beartype.claw)
  12. PyPI · beartype
  13. GitHub · beartype releases
  14. GitHub · beartype issue tracker