Skip to main content

Unbearably fast runtime type checking in pure Python.

Project description

GitHub Actions status

Look for the bare necessities,
  the simple bare necessities.
Forget about your worries and your strife.

                        — The Jungle Book.

Beartype is an open-source pure-Python PEP-compliant O(1) constant-time runtime type checker emphasizing efficiency, portability, and thrilling puns.

Beartype brings Rust- and C++-inspired zero-cost abstractions into the lawless world of dynamically-typed pure Python.

Beartype is portably implemented in Python 3, continuously stress-tested via GitHub Actions + tox + pytest, and permissively distributed under the MIT license. Beartype has no runtime dependencies, only one test-time dependency, and supports all Python 3.x releases still in active development.


Installation

Let’s install beartype with pip, because community standards are good:

pip3 install beartype

Let’s install beartype with Anaconda, because corporate standards are (occasionally) good too:

conda config --add channels conda-forge
conda install beartype

Linux

Let’s install beartype with Gentoo, because source-based Linux distros are the computational nuclear option:

emerge --ask app-eselect/eselect-repository
mkdir -p /etc/portage/repos.conf
eselect repository enable raiagent
emerge --sync raiagent
emerge beartype

Overview

Beartype imposes no developer constraints beyond importation and usage of a single configuration-free decorator – trivializing integration with new and existing applications, frameworks, modules, packages, and scripts. Beartype stresses zero-cost strategies at both:

Like competing static type checkers operating at the coarse-grained application level with ad-hoc heuristic type inference (e.g., Pyre, mypy, pyright, pytype), beartype effectively imposes no runtime overhead. Unlike static type checkers:

  • Beartype operates exclusively at the fine-grained callable level of pure-Python functions and methods via the standard decorator design pattern. This renders beartype natively compatible with all interpreters and compilers targeting the Python language – including PyPy, Numba, Nuitka, and (wait for it) CPython itself.

  • Beartype enjoys deterministic Turing-complete access to the actual callables, objects, and types being type-checked. This enables beartype to solve dynamic problems decidable only at runtime – including type-checking of arbitrary objects whose:

Unlike comparable runtime type checkers (e.g., enforce, pytypes, typeguard), beartype decorates callables with dynamically generated wrappers efficiently type-checking each parameter passed to and value returned from those callables in constant time. Since “performance by default” is our first-class concern, generated wrappers are guaranteed to:

Cheatsheet

Let’s type-check like greased lightning:

# Import the core @beartype decorator.
from beartype import beartype

# Import PEP 585-compliant types to annotate callables with.
from collections.abc import MutableSequence

# Import PEP 484-compliant types to annotate callables with, too.
from typing import List, Optional, Tuple, Union

# Import beartype-specific types to annotate callables with, too.
from beartype.cave import (
    AnyType,
    BoolType,
    FunctionTypes,
    CallableTypes,
    GeneratorType,
    IntOrFloatType,
    IntType,
    IterableType,
    IteratorType,
    NoneType,
    NoneTypeOr,
    NumberType,
    RegexTypes,
    ScalarTypes,
    SequenceType,
    StrType,
    VersionTypes,
)

# Import user-defined types for use with @beartype, three.
from my_package.my_module import MyClass

# Decorate functions with @beartype and...
@beartype
def bare_necessities(
    # Annotate builtin types as is.
    param_must_satisfy_builtin_type: str,

    # Annotate user-defined classes as is, too. Note this covariantly
    # matches all instances of both this class and subclasses of this class.
    param_must_satisfy_user_type: MyClass,

    # Annotate PEP 585-compliant builtin container types, subscripted by the
    # types of items these containers are required to contain.
    param_must_satisfy_pep585_builtin: list[str],

    # Annotate PEP 585-compliant standard collection types, subscripted too.
    param_must_satisfy_pep585_collection: MutableSequence[str],

    # Annotate PEP 484-compliant non-standard container types defined by the
    # "typing" module, optionally subscripted and only usable as type hints.
    param_must_satisfy_pep484_typing: List[int],

    # Annotate PEP 484-compliant unions of types.
    param_must_satisfy_pep484_union: Union[dict, Tuple[MyClass, ...], int],

    # Annotate PEP 484-compliant relative forward references dynamically
    # resolved at call time as unqualified classnames relative to the
    # current user-defined submodule. Note this class is defined below.
    param_must_satisfy_pep484_relative_forward_ref: 'MyCrassClass',

    # Annotate PEP 484-compliant objects predefined by the "typing" module
    # subscripted by PEP-compliant relative forward references.
    param_must_satisfy_pep484_hint_relative_forward_ref: (
        List['MyCrassClass']),

    # Annotate beartype-specific types predefined by the beartype cave.
    param_must_satisfy_beartype_type_from_cave: NumberType,

    # Annotate beartype-specific unions of types as tuples.
    param_must_satisfy_beartype_union: (dict, MyClass, int),

    # Annotate beartype-specific unions predefined by the beartype cave.
    param_must_satisfy_beartype_union_from_cave: CallableTypes,

    # Annotate beartype-specific unions concatenated together.
    param_must_satisfy_beartype_union_concatenated: (
        IteratorType,) + ScalarTypes,

    # Annotate beartype-specific absolute forward references dynamically
    # resolved at call time as fully-qualified "."-delimited classnames.
    param_must_satisfy_beartype_absolute_forward_ref: (
        'my_package.my_module.MyClass'),

    # Annotate beartype-specific forward references in unions of types, too.
    param_must_satisfy_beartype_union_with_forward_ref: (
        IterableType, 'my_package.my_module.MyOtherClass', NoneType),

    # Annotate PEP 484-compliant optional types.
    param_must_satisfy_pep484_optional: Optional[float] = None,

    # Annotate PEP 484-compliant optional unions of types.
    param_must_satisfy_pep484_optional_union: Optional[
        Union[float, int]]) = None,

    # Annotate beartype-specific optional types.
    param_must_satisfy_beartype_type_optional: NoneTypeOr[float] = None,

    # Annotate beartype-specific optional unions of types.
    param_must_satisfy_beartype_tuple_optional: NoneTypeOr[
        float, int] = None,

    # Annotate variadic positional arguments as above, too.
    *args: VersionTypes + (
        IntOrFloatType, 'my_package.my_module.MyVersionType'),

    # Annotate keyword-only arguments as above, too.
    param_must_be_passed_by_keyword_only: SequenceType,

# Annotate return types as above, too.
) -> (IntType, 'my_package.my_module.MyOtherOtherClass', BoolType):
    return 0xDEADBEEF


# Decorate generators as above but returning a generator type.
@beartype
def bare_generator() -> GeneratorType:
    yield from range(0xBEEFBABE, 0xCAFEBABE)


class MyCrassClass:
    # Decorate instance methods as above without annotating "self".
    @beartype
    def __init__(self, scalar: ScalarTypes) -> NoneType:
        self._scalar = scalar

    # Decorate class methods as above without annotating "cls". When
    # chaining decorators, "@beartype" should typically be specified last.
    @classmethod
    @beartype
    def bare_classmethod(cls, regex: RegexTypes, wut: str) -> FunctionTypes:
        import re
        return lambda: re.sub(regex, 'unbearable', str(cls._scalar) + wut)

    # Decorate static methods as above.
    @staticmethod
    @beartype
    def bare_staticmethod(callable: CallableTypes, *args: str) -> AnyType:
        return callable(*args)

    # Decorate property getter methods as above.
    @property
    @beartype
    def bare_gettermethod(self) -> IteratorType:
        return range(0x0B00B135 + int(self._scalar), 0xB16B00B5)

    # Decorate property setter methods as above.
    @bare_gettermethod.setter
    @beartype
    def bare_settermethod(self, bad: IntType = 0xBAAAAAAD) -> NoneType:
        self._scalar = bad if bad else 0xBADDCAFE

Features

Let’s chart current and future compliance with Python’s typing landscape:

category

feature

versions partially supporting

versions fully supporting

decoratable

classes

none

none

coroutines

none

none

functions

0.1.0current

0.1.0current

generators

0.1.0current

0.1.0current

methods

0.1.0current

0.1.0current

parameters

optional

0.1.0current

0.1.0current

keyword-only

0.1.0current

0.1.0current

positional-only

none

none

variadic keyword

none

none

variadic positional

0.1.0current

0.1.0current

hints

covariant

0.1.0current

0.1.0current

contravariant

none

none

absolute forward references

0.1.0current

0.1.0current

relative forward references

0.4.0current

0.4.0current

tuple unions

0.1.0current

0.1.0current

builtins

dict

0.5.0current

none

frozenset

0.5.0current

none

list

0.5.0current

0.5.0current

set

0.5.0current

none

tuple

0.5.0current

0.5.0current

type

0.5.0current

none

collections

collections.ChainMap

0.5.0current

none

collections.Counter

0.5.0current

none

collections.OrderedDict

0.5.0current

none

collections.defaultdict

0.5.0current

none

collections.deque

0.5.0current

none

collections.abc

collections.abc.AsyncGenerator

0.5.0current

none

collections.abc.AsyncIterable

0.5.0current

none

collections.abc.AsyncIterator

0.5.0current

none

collections.abc.Awaitable

0.5.0current

none

collections.abc.ByteString

0.5.0current

0.5.0current

collections.abc.Callable

0.5.0current

none

collections.abc.Collection

0.5.0current

none

collections.abc.Container

0.5.0current

none

collections.abc.Coroutine

0.5.0current

none

collections.abc.Generator

0.5.0current

none

collections.abc.ItemsView

0.5.0current

none

collections.abc.Iterable

0.5.0current

none

collections.abc.Iterator

0.5.0current

none

collections.abc.KeysView

0.5.0current

none

collections.abc.Mapping

0.5.0current

none

collections.abc.MappingView

0.5.0current

none

collections.abc.MutableMapping

0.5.0current

none

collections.abc.MutableSequence

0.5.0current

0.5.0current

collections.abc.MutableSet

0.5.0current

none

collections.abc.Reversible

0.5.0current

none

collections.abc.Sequence

0.5.0current

0.5.0current

collections.abc.Set

0.5.0current

none

collections.abc.ValuesView

0.5.0current

none

contextlib

contextlib.AbstractAsyncContextManager

0.5.0current

none

contextlib.AbstractContextManager

0.5.0current

none

re

re.Match

0.5.0current

none

re.Pattern

0.5.0current

none

typing

typing.AbstractSet

0.2.0current

none

typing.Annotated

0.4.0current

0.4.0current

typing.Any

0.2.0current

0.2.0current

typing.AnyStr

0.4.0current

none

typing.AsyncContextManager

0.4.0current

none

typing.AsyncGenerator

0.2.0current

none

typing.AsyncIterable

0.2.0current

none

typing.AsyncIterator

0.2.0current

none

typing.Awaitable

0.2.0current

none

typing.BinaryIO

0.4.0current

none

typing.ByteString

0.2.0current

0.2.0current

typing.Callable

0.2.0current

none

typing.ChainMap

0.2.0current

none

typing.ClassVar

none

none

typing.Collection

0.2.0current

none

typing.Container

0.2.0current

none

typing.ContextManager

0.4.0current

none

typing.Coroutine

0.2.0current

none

typing.Counter

0.2.0current

none

typing.DefaultDict

0.2.0current

none

typing.Deque

0.2.0current

none

typing.Dict

0.2.0current

none

typing.Final

none

none

typing.ForwardRef

0.4.0current

0.4.0current

typing.FrozenSet

0.2.0current

none

typing.Generator

0.2.0current

none

typing.Generic

0.4.0current

0.4.0current

typing.Hashable

0.2.0current

none

typing.IO

0.4.0current

none

typing.ItemsView

0.2.0current

none

typing.Iterable

0.2.0current

none

typing.Iterator

0.2.0current

none

typing.KeysView

0.2.0current

none

typing.List

0.2.0current

0.3.0current

typing.Literal

none

none

typing.Mapping

0.2.0current

none

typing.MappingView

0.2.0current

none

typing.Match

0.4.0current

none

typing.MutableMapping

0.2.0current

none

typing.MutableSequence

0.2.0current

0.3.0current

typing.MutableSet

0.2.0current

none

typing.NamedTuple

0.1.0current

none

typing.NewType

0.4.0current

0.4.0current

typing.NoReturn

0.4.0current

0.4.0current

typing.Optional

0.2.0current

0.2.0current

typing.OrderedDict

0.2.0current

none

typing.Pattern

0.4.0current

none

typing.Protocol

0.4.0current

0.4.0current

typing.Reversible

0.2.0current

none

typing.Sequence

0.2.0current

0.3.0current

typing.Set

0.2.0current

none

typing.Sized

0.2.0current

0.2.0current

typing.SupportsAbs

0.4.0current

0.4.0current

typing.SupportsBytes

0.4.0current

0.4.0current

typing.SupportsComplex

0.4.0current

0.4.0current

typing.SupportsFloat

0.4.0current

0.4.0current

typing.SupportsIndex

0.4.0current

0.4.0current

typing.SupportsInt

0.4.0current

0.4.0current

typing.SupportsRound

0.4.0current

0.4.0current

typing.Text

0.1.0current

0.1.0current

typing.TextIO

0.4.0current

none

typing.Tuple

0.2.0current

0.4.0current

typing.Type

0.2.0current

none

typing.TypedDict

0.1.0current

none

typing.TypeVar

0.4.0current

none

typing.Union

0.2.0current

0.2.0current

typing.ValuesView

0.2.0current

none

typing.TYPE_CHECKING

0.5.0current

0.5.0current

@typing.final

none

none

@typing.no_type_check

0.5.0current

0.5.0current

PEP

484

0.2.0current

none

544

0.4.0current

0.4.0current

560

0.4.0current

0.4.0current

563

0.1.1current

0.1.1current

572

0.3.0current

0.4.0current

585

0.5.0current

0.5.0current

586

none

none

589

none

none

591

none

none

593

0.4.0current

0.4.0current

packages

PyPI

0.1.0current

Anaconda

0.1.0current

Gentoo

0.2.0current

Python

3.5

0.1.00.3.0

3.6

0.1.0current

3.7

0.1.0current

3.8

0.1.0current

3.9

0.3.2current

Timings

Let’s run our profiler suite timing beartype and fellow runtime type-checkers against a battery of surely fair, impartial, and unbiased use cases:

beartype profiler [version]: 0.0.2

python    [version]: Python 3.7.8
beartype  [version]: 0.3.0
typeguard [version]: 2.9.1

========================== str (100 calls each loop) ==========================
decoration         [none     ]: 100 loops, best of 3: 366 nsec per loop
decoration         [beartype ]: 100 loops, best of 3: 346 usec per loop
decoration         [typeguard]: 100 loops, best of 3: 13.4 usec per loop
decoration + calls [none     ]: 100 loops, best of 3: 16.4 usec per loop
decoration + calls [beartype ]: 100 loops, best of 3: 480 usec per loop
decoration + calls [typeguard]: 100 loops, best of 3: 7 msec per loop

==================== Union[int, str] (100 calls each loop) ====================
decoration         [none     ]: 100 loops, best of 3: 2.97 usec per loop
decoration         [beartype ]: 100 loops, best of 3: 363 usec per loop
decoration         [typeguard]: 100 loops, best of 3: 16.7 usec per loop
decoration + calls [none     ]: 100 loops, best of 3: 20.4 usec per loop
decoration + calls [beartype ]: 100 loops, best of 3: 543 usec per loop
decoration + calls [typeguard]: 100 loops, best of 3: 11.1 msec per loop

================ List[int] of 1000 items (7485 calls each loop) ================
decoration         [none     ]: 1 loop, best of 1: 41.7 usec per loop
decoration         [beartype ]: 1 loop, best of 1: 1.33 msec per loop
decoration         [typeguard]: 1 loop, best of 1: 82.2 usec per loop
decoration + calls [none     ]: 1 loop, best of 1: 1.4 msec per loop
decoration + calls [beartype ]: 1 loop, best of 1: 22.5 msec per loop
decoration + calls [typeguard]: 1 loop, best of 1: 124 sec per loop

ELI5

beartype is:

  • At least twenty times faster (i.e., 20,000%) and consumes three orders of magnitude less time in the worst case than typeguard – the only comparable runtime type-checker also compatible with most modern Python versions.

  • Asymptotically faster in the best case than typeguard, which scales linearly (rather than not at all) with the size of checked containers.

  • Constant across type hints, taking roughly the same time to check parameters and return values hinted by the builtin type str as it does to check those hinted by the unified type Union[int, str] as it does to check those hinted by the container type List[object]. typeguard is variable across type hints, taking significantly longer to check List[object] as as it does to check Union[int, str], which takes roughly twice the time as it does to check str.

beartype performs most of its work at decoration time. The @beartype decorator consumes most of the time needed to first decorate and then repeatedly call a decorated function. beartype is thus front-loaded. After paying the initial cost of decoration, each type-checked call thereafter incurs comparatively little overhead.

Conventional runtime type checkers perform most of their work at call time. The @typeguard.typechecked and similar decorators consume almost none of the time needed to first decorate and then repeatedly call a decorated function. They are thus back-loaded. Although the initial cost of decoration is essentially free, each type-checked call thereafter incurs significant overhead.

Nobody Expects the Linearithmic Time

Math time, people. it’s happening

Most runtime type-checkers exhibit O(n) time complexity (where n is the total number of items recursively contained in a container to be checked) by recursively and repeatedly checking all items of all containers passed to or returned from all calls of decorated callables.

beartype guarantees O(1) time complexity by non-recursively but repeatedly checking one random item at all nesting levels of all containers passed to or returned from all calls of decorated callables, thus amortizing the cost of deeply checking containers across calls. (See the subsection on @beartype-generated code deeply type-checking arbitrarily nested containers in constant time for what this means in practice.)

beartype exploits the well-known coupon collector’s problem applied to abstract trees of nested type hints, enabling us to statistically predict the number of calls required to fully type-check all items of an arbitrary container on average. Formally, let:

  • E(T) be the expected number of calls needed to check all items of a container containing only non-container items (i.e., containing no nested subcontainers) either passed to or returned from a @beartype-decorated callable.

  • γ ≈ 0.5772156649 be the Euler–Mascheroni constant.

Then:

https://render.githubusercontent.com/render/math?math=%5Cdisplaystyle+E%28T%29+%3D+n+%5Clog+n+%2B+%5Cgamma+n+%2B+%5Cfrac%7B1%7D%7B2%7D+%2B+O%5Cleft%28%5Cfrac%7B1%7D%7Bn%7D%5Cright%29

The summation ½ + O(1/n) is strictly less than 1 and thus negligible. While non-negligible, the term γn grows significantly slower than the term nlogn. So this reduces to:

https://render.githubusercontent.com/render/math?math=%5Cdisplaystyle+E%28T%29+%3D+O%28n+%5Clog+n%29

We now generalize this bound to the general case. When checking a container containing no subcontainers, beartype only randomly samples one item from that container on each call. When checking a container containing arbitrarily many nested subcontainers, however, beartype randomly samples one random item from each nesting level of that container on each call.

In general, beartype thus samples h random items from a container on each call, where h is that container’s height (i.e., maximum number of edges on the longest path from that container to a non-container leaf item reachable from items directly contained in that container). Since h ≥ 1, beartype samples at least as many items each call as assumed in the usual coupon collector’s problem and thus paradoxically takes a fewer number of calls on average to check all items of a container containing arbitrarily many subcontainers as it does to check all items of a container containing no subcontainers.

Ergo, the expected number of calls E(S) needed to check all items of an arbitrary container exhibits the same or better growth rate and remains bound above by at least the same upper bounds – but probably tighter: e.g.,

https://render.githubusercontent.com/render/math?math=%5Cdisplaystyle+E%28S%29+%3D+O%28E%28T%29%29+%3D+O%28n+%5Clog+n%29%0A

Fully checking a container takes no more calls than that container’s size times the logarithm of that size on average. For example, fully checking a list of 50 integers is expected to take 225 calls on average.

Compliance

beartype is fully compliant with these Python Enhancement Proposals (PEPs):

beartype is currently not compliant whatsoever with these PEPs:

See also the PEP and typing categories of our features matrix for further details.

Full Compliance

beartype deeply type-checks (i.e., directly checks the types of and recursively checks the types of items contained in) parameters and return values annotated with these typing types:

beartype also fully supports callables decorated by these typing decorators:

Lastly, beartype fully supports these typing constants:

Partial Compliance

beartype currently only shallowly type-checks (i.e., only directly checks the types of) parameters and return values annotated with these typing types:

Subsequent beartype versions will deeply type-check these typing types while preserving our O(1) time complexity (with negligible constant factors) guarantee.

No Compliance

beartype currently silently ignores these typing types at decoration time:

beartype currently raises exceptions at decoration time when passed these typing types:

Subsequent beartype versions will first shallowly and then deeply type-check these typing types while preserving our O(1) time complexity (with negligible constant factors) guarantee.

Tutorial

Let’s begin with the simplest type of type-checking supported by @beartype.

Builtin Types

Builtin types like dict, int, list, set, and str are trivially type-checked by annotating parameters and return values with those types as is.

Let’s declare a simple beartyped function accepting a string and a dictionary and returning a tuple:

from beartype import beartype

@beartype
def law_of_the_jungle(wolf: str, pack: dict) -> tuple:
    return (wolf, pack[wolf]) if wolf in pack else None

Let’s call that function with good types:

>>> law_of_the_jungle(wolf='Akela', pack={'Akela': 'alone', 'Raksha': 'protection'})
('Akela', 'alone')

Good function. Let’s call it again with bad types:

>>> law_of_the_jungle(wolf='Akela', pack=['Akela', 'Raksha'])
Traceback (most recent call last):
  File "<ipython-input-10-7763b15e5591>", line 1, in <module>
    law_of_the_jungle(wolf='Akela', pack=['Akela', 'Raksha'])
  File "<string>", line 22, in __law_of_the_jungle_beartyped__
beartype.roar.BeartypeCallTypeParamException: @beartyped law_of_the_jungle() parameter pack=['Akela', 'Raksha'] not a <class 'dict'>.

The beartype.roar submodule publishes exceptions raised at both decoration time by @beartype and at runtime by wrappers generated by @beartype. In this case, a runtime type exception describing the improperly typed pack parameter is raised.

Good function! Let’s call it again with good types exposing a critical issue in this function’s implementation and/or return type annotation:

>>> law_of_the_jungle(wolf='Leela', pack={'Akela': 'alone', 'Raksha': 'protection'})
Traceback (most recent call last):
  File "<ipython-input-10-7763b15e5591>", line 1, in <module>
    law_of_the_jungle(wolf='Leela', pack={'Akela': 'alone', 'Raksha': 'protection'})
  File "<string>", line 28, in __law_of_the_jungle_beartyped__
beartype.roar.BeartypeCallTypeReturnException: @beartyped law_of_the_jungle() return value None not a <class 'tuple'>.

Bad function. Let’s conveniently resolve this by permitting this function to return either a tuple or None as detailed below:

>>> from beartype.cave import NoneType
>>> @beartype
... def law_of_the_jungle(wolf: str, pack: dict) -> (tuple, NoneType):
...     return (wolf, pack[wolf]) if wolf in pack else None
>>> law_of_the_jungle(wolf='Leela', pack={'Akela': 'alone', 'Raksha': 'protection'})
None

The beartype.cave submodule publishes generic types suitable for use with the @beartype decorator and anywhere else you might need them. In this case, the type of the None singleton is imported from this submodule and listed in addition to tuple as an allowed return type from this function.

Note that usage of the beartype.cave submodule is entirely optional (but more efficient and convenient than most alternatives). In this case, the type of the None singleton can also be accessed directly as type(None) and listed in place of NoneType above: e.g.,

>>> @beartype
... def law_of_the_jungle(wolf: str, pack: dict) -> (tuple, type(None)):
...     return (wolf, pack[wolf]) if wolf in pack else None
>>> law_of_the_jungle(wolf='Leela', pack={'Akela': 'alone', 'Raksha': 'protection'})
None

Of course, the beartype.cave submodule also publishes types not accessible directly like RegexCompiledType (i.e., the type of all compiled regular expressions). All else being equal, beartype.cave is preferable.

Good function! The type hints applied to this function now accurately document this function’s API. All’s well that ends typed well. Suck it, Shere Khan.

Arbitrary Types

Everything above also extends to:

  • Arbitrary types like user-defined classes and stock classes in the Python stdlib (e.g., argparse.ArgumentParser) – all of which are also trivially type-checked by annotating parameters and return values with those types.

  • Arbitrary callables like instance methods, class methods, static methods, and generator functions and methods – all of which are also trivially type-checked with the @beartype decorator.

Let’s declare a motley crew of beartyped callables doing various silly things in a strictly typed manner, just ‘cause:

from beartype import beartype
from beartype.cave import GeneratorType, IterableType, NoneType

class MaximsOfBaloo(object):
    @beartype
    def __init__(self, sayings: IterableType):
        self.sayings = sayings

@beartype
def inform_baloo(maxims: MaximsOfBaloo) -> GeneratorType:
    for saying in maxims.sayings:
        yield saying

For genericity, the MaximsOfBaloo class initializer accepts any generic iterable (via the beartype.cave.IterableType tuple listing all valid iterable types) rather than an overly specific list or tuple type. Your users may thank you later.

For specificity, the inform_baloo generator function has been explicitly annotated to return a beartype.cave.GeneratorType (i.e., the type returned by functions and methods containing at least one yield statement). Type safety brings good fortune for the New Year.

Let’s iterate over that generator with good types:

>>> maxims = MaximsOfBaloo(sayings={
...     '''If ye find that the Bullock can toss you,
...           or the heavy-browed Sambhur can gore;
...      Ye need not stop work to inform us:
...           we knew it ten seasons before.''',
...     '''“There is none like to me!” says the Cub
...           in the pride of his earliest kill;
...      But the jungle is large and the Cub he is small.
...           Let him think and be still.''',
... })
>>> for maxim in inform_baloo(maxims): print(maxim.splitlines()[-1])
       Let him think and be still.
       we knew it ten seasons before.

Good generator. Let’s call it again with bad types:

>>> for maxim in inform_baloo([
...     'Oppress not the cubs of the stranger,',
...     '     but hail them as Sister and Brother,',
... ]): print(maxim.splitlines()[-1])
Traceback (most recent call last):
  File "<ipython-input-10-7763b15e5591>", line 30, in <module>
    '     but hail them as Sister and Brother,',
  File "<string>", line 12, in __inform_baloo_beartyped__
beartype.roar.BeartypeCallTypeParamException: @beartyped inform_baloo() parameter maxims=['Oppress not the cubs of the stranger,', '     but hail them as Sister and ...'] not a <class '__main__.MaximsOfBaloo'>.

Good generator! The type hints applied to these callables now accurately document their respective APIs. Thanks to the pernicious magic of beartype, all ends typed well… yet again.

Unions of Types

That’s all typed well, but everything above only applies to parameters and return values constrained to singular types. In practice, parameters and return values are often relaxed to any of multiple types referred to as unions of types. You can thank set theory for the jargon… unless you hate set theory. Then it’s just our fault.

Unions of types are trivially type-checked by annotating parameters and return values with tuples containing those types. Let’s declare another beartyped function accepting either a mapping or a string and returning either another function or an integer:

from beartype import beartype
from beartype.cave import FunctionType, IntType, MappingType

@beartype
def toomai_of_the_elephants(memory: (str, MappingType)) -> (
    IntType, FunctionType):
    return len(memory) if isinstance(memory, str) else lambda key: memory[key]

For genericity, the toomai_of_the_elephants function accepts any generic integer (via the beartype.cave.IntType abstract base class (ABC) matching both builtin integers and third-party integers from frameworks like NumPy and SymPy) rather than an overly specific int type. The API you relax may very well be your own.

Let’s call that function with good types:

>>> memory_of_kala_nag = {
...     'remember': 'I will remember what I was, I am sick of rope and chain—',
...     'strength': 'I will remember my old strength and all my forest affairs.',
...     'not sell': 'I will not sell my back to man for a bundle of sugar-cane:',
...     'own kind': 'I will go out to my own kind, and the wood-folk in their lairs.',
...     'morning':  'I will go out until the day, until the morning break—',
...     'caress':   'Out to the wind’s untainted kiss, the water’s clean caress;',
...     'forget':   'I will forget my ankle-ring and snap my picket stake.',
...     'revisit':  'I will revisit my lost loves, and playmates masterless!',
... }
>>> toomai_of_the_elephants(memory_of_kala_nag['remember'])
56
>>> toomai_of_the_elephants(memory_of_kala_nag)('remember')
'I will remember what I was, I am sick of rope and chain—'

Good function. Let’s call it again with a tastelessly bad type:

>>> toomai_of_the_elephants(0xDEADBEEF)
Traceback (most recent call last):
  File "<ipython-input-7-e323f8d6a4a0>", line 1, in <module>
    toomai_of_the_elephants(0xDEADBEEF)
  File "<string>", line 12, in __toomai_of_the_elephants_beartyped__
BeartypeCallTypeParamException: @beartyped toomai_of_the_elephants() parameter memory=3735928559 not a (<class 'str'>, <class 'collections.abc.Mapping'>).

Good function! The type hints applied to this callable now accurately documents its API. All ends typed well… still again and again.

Optional Types

That’s also all typed well, but everything above only applies to mandatory parameters and return values whose types are never NoneType. In practice, parameters and return values are often relaxed to optionally accept any of multiple types including NoneType referred to as optional types.

Optional types are trivially type-checked by annotating optional parameters (parameters whose values default to None) and optional return values (callables returning None rather than raising exceptions in edge cases) with the NoneTypeOr tuple factory indexed by those types or tuples of types.

Let’s declare another beartyped function accepting either an enumeration type or None and returning either an enumeration member or None:

from beartype import beartype
from beartype.cave import EnumType, EnumMemberType, NoneTypeOr
from enum import Enum

class Lukannon(Enum):
    WINTER_WHEAT = 'The Beaches of Lukannon—the winter wheat so tall—'
    SEA_FOG      = 'The dripping, crinkled lichens, and the sea-fog drenching all!'
    PLAYGROUND   = 'The platforms of our playground, all shining smooth and worn!'
    HOME         = 'The Beaches of Lukannon—the home where we were born!'
    MATES        = 'I met my mates in the morning, a broken, scattered band.'
    CLUB         = 'Men shoot us in the water and club us on the land;'
    DRIVE        = 'Men drive us to the Salt House like silly sheep and tame,'
    SEALERS      = 'And still we sing Lukannon—before the sealers came.'

@beartype
def tell_the_deep_sea_viceroys(story: NoneTypeOr[EnumType] = None) -> (
    NoneTypeOr[EnumMemberType]):
    return story if story is None else list(story.__members__.values())[-1]

For efficiency, the NoneTypeOr tuple factory creates, caches, and returns new tuples of types appending NoneType to the original types and tuples of types it’s indexed with. Since efficiency is good, NoneTypeOr is also good.

Let’s call that function with good types:

>>> tell_the_deep_sea_viceroys(Lukannon)
<Lukannon.SEALERS: 'And still we sing Lukannon—before the sealers came.'>
>>> tell_the_deep_sea_viceroys()
None

You may now be pondering to yourself grimly in the dark: “…but could we not already do this just by manually annotating optional types with tuples containing NoneType?”

You would, of course, be correct. Let’s grimly redeclare the same function accepting and returning the same types – only annotated with NoneType rather than NoneTypeOr:

from beartype import beartype
from beartype.cave import EnumType, EnumMemberType, NoneType

@beartype
def tell_the_deep_sea_viceroys(story: (EnumType, NoneType) = None) -> (
    (EnumMemberType, NoneType)):
    return list(story.__members__.values())[-1] if story is not None else None

This manual approach has the same exact effect as the prior factoried approach with one exception: the factoried approach efficiently caches and reuses tuples over every annotated type, whereas the manual approach inefficiently recreates tuples for each annotated type. For small codebases, that difference is negligible; for large codebases, that difference is still probably negligible. Still, “waste not want not” is the maxim we type our lives by here.

Naturally, the NoneTypeOr tuple factory accepts tuples of types as well. Let’s declare another beartyped function accepting either an enumeration type, enumeration type member, or None and returning either an enumeration type, enumeration type member, or None:

from beartype import beartype
from beartype.cave import EnumType, EnumMemberType, NoneTypeOr

EnumOrEnumMemberType = (EnumType, EnumMemberType)

@beartype
def sang_them_up_the_beach(
    woe: NoneTypeOr[EnumOrEnumMemberType] = None) -> (
    NoneTypeOr[EnumOrEnumMemberType]):
    return woe if isinstance(woe, NoneTypeOr[EnumMemberType]) else (
        list(woe.__members__.values())[-1])

Let’s call that function with good types:

>>> sang_them_up_the_beach(Lukannon)
<Lukannon.SEALERS: 'And still we sing Lukannon—before the sealers came.'>
>>> sang_them_up_the_beach()
None

Behold! The terrifying power of the NoneTypeOr tuple factory, resplendent in its highly over-optimized cache utilization.

Decoration

Let’s take a deep dive into the deep end of runtime type checking – the beartype way. In this subsection, we show code generated by the @beartype decorator in real-world use cases and tell why that code is the fastest possible code type-checking those cases.

Identity Decoration

We begin by wading into the torpid waters of the many ways beartype avoids doing any work whatsoever, because laziness is the virtue we live by. The reader may recall that the fastest decorator at decoration- and call-time is the identity decorator returning its decorated callable unmodified: e.g.,

from collections.abc import Callable

def identity_decorator(func: Callable): -> Callable:
    return func

beartype silently reduces to the identity decorator whenever it can, which is surprisingly often. Our three weapons are laziness, surprise, ruthless efficiency, and an almost fanatical devotion to constant-time type checking.

Unconditional Identity Decoration

Let’s define a trivial function annotated by no type hints:

def law_of_the_jungle(strike_first_and_then_give_tongue):
    return strike_first_and_then_give_tongue

Let’s decorate that function by @beartype and verify that @beartype reduced to the identity decorator by returning that function unmodified:

>>> from beartype import beartype
>>> beartype(law_of_the_jungle) is law_of_the_jungle
True

We’ve verified that @beartype reduces to the identity decorator when decorating unannotated callables. That’s but the tip of the iceberg, though. @beartype unconditionally reduces to a noop when:

Shallow Identity Decoration

Let’s define a trivial function annotated by the PEP 484-compliant typing.Any type hint:

from typing import Any

def law_of_the_jungle_2(never_order_anything_without_a_reason: Any) -> Any:
    return never_order_anything_without_a_reason

Again, let’s decorate that function by @beartype and verify that @beartype reduced to the identity decorator by returning that function unmodified:

>>> from beartype import beartype
>>> beartype(law_of_the_jungle_2) is law_of_the_jungle_2
True

We’ve verified that @beartype reduces to the identity decorator when decorating callables annotated by typing.Any – a novel category of type hint we refer to as shallowly ignorable type hints (known to be ignorable by constant-time lookup in a predefined frozen set). That’s but the snout of the crocodile, though. @beartype conditionally reduces to a noop when all type hints annotating the decorated callable are shallowly ignorable. These include:

  • object, the root superclass of Python’s class hierarchy. Since all objects are instances of object, object conveys no meaningful constraints as a type hint and is thus shallowly ignorable.

  • typing.Any, equivalent to object.

  • typing.Generic, equivalent to typing.Generic[typing.Any], which conveys no meaningful constraints as a type hint and is thus shallowly ignorable.

  • typing.Protocol, equivalent to typing.Protocol[typing.Any] and shallowly ignorable for similar reasons.

  • typing.Union, equivalent to typing.Union[typing.Any], equivalent to Any.

  • typing.Optional, equivalent to typing.Optional[typing.Any], equivalent to Union[Any, type(None)]. Since any union subscripted by ignorable type hints is itself ignorable, [1] typing.Optional is shallowly ignorable as well.

Deep Identity Decoration

Let’s define a trivial function annotated by a non-trivial PEP 484-, 585- and 593-compliant type hint that superficially appears to convey meaningful constraints:

from typing import Annotated, NewType, Union

hint = Union[str, list[int], NewType('MetaType', Annotated[object, 53])]
def law_of_the_jungle_3(bring_them_to_the_pack_council: hint) -> hint:
    return bring_them_to_the_pack_council

Despite appearances, it can be shown by exhaustive (and frankly exhausting) reduction that that hint is actually ignorable. Let’s decorate that function by @beartype and verify that @beartype reduced to the identity decorator by returning that function unmodified:

>>> from beartype import beartype
>>> beartype(law_of_the_jungle_3) is law_of_the_jungle_3
True

We’ve verified that @beartype reduces to the identity decorator when decorating callables annotated by the above object – a novel category of type hint we refer to as deeply ignorable type hints (known to be ignorable only by recursive linear-time inspection of subscripted arguments). That’s but the trunk of the elephant, though. @beartype conditionally reduces to a noop when all type hints annotating the decorated callable are deeply ignorable. These include:

Constant Decoration

We continue by trundling into the turbid waters out at sea, where beartype reluctantly performs its minimal amount of work with a heavy sigh.

Constant Builtin Type Decoration

Let’s define a trivial function annotated by type hints that are builtin types:

from beartype import beartype

@beartype
def law_of_the_jungle_4(he_must_be_spoken_for_by_at_least_two: int):
    return he_must_be_spoken_for_by_at_least_two

Let’s see the wrapper function @beartype dynamically generated from that:

def __beartyped_law_of_the_jungle_4(
    *args,
    __beartype_func=__beartype_func,
    __beartypistry=__beartypistry,
    **kwargs
):
    # Localize the number of passed positional arguments for efficiency.
    __beartype_args_len = len(args)
    # Localize this positional or keyword parameter if passed *OR* to the
    # sentinel value "__beartypistry" guaranteed to never be passed otherwise.
    __beartype_pith_0 = (
        args[0] if __beartype_args_len > 0 else
        kwargs.get('he_must_be_spoken_for_by_at_least_two', __beartypistry)
    )

    # If this parameter was passed...
    if __beartype_pith_0 is not __beartypistry:
        # Type-check this passed parameter or return value against this
        # PEP-compliant type hint.
        if not isinstance(__beartype_pith_0, int):
            __beartype_raise_pep_call_exception(
                func=__beartype_func,
                pith_name='he_must_be_spoken_for_by_at_least_two',
                pith_value=__beartype_pith_0,
            )

    # Call this function with all passed parameters and return the value
    # returned from this call.
    return __beartype_func(*args, **kwargs)

Let’s dismantle this bit by bit:

  • The code comments above are verbatim as they appear in the generated code.

  • __beartyped_law_of_the_jungle_4() is the ad-hoc function name @beartype assigned this wrapper function.

  • __beartype_func is the original law_of_the_jungle_4() function.

  • __beartypistry is a thread-safe global registry of all types, tuples of types, and forward references to currently undeclared types visitable from type hints annotating callables decorated by @beartype. We’ll see more about the __beartypistry in a moment. For know, just know that __beartypistry is a private singleton of the beartype package. This object is frequently accessed and thus localized to the body of this wrapper rather than accessed as a global variable, which would be mildly slower.

  • __beartype_pith_0 is the value of the first passed parameter, regardless of whether that parameter is passed as a positional or keyword argument. If unpassed, the value defaults to the __beartypistry. Since no caller should access (let alone pass) that object, that object serves as an efficient sentinel value enabling us to discern passed from unpassed parameters. beartype internally favours the term “pith” (which we absolutely just made up) to transparently refer to the arbitrary object currently being type-checked against its associated type hint.

  • isinstance(__beartype_pith_0, int) tests whether the value passed for this parameter satisfies the type hint annotating this parameter.

  • __beartype_raise_pep_call_exception() raises a human-readable exception if this value fails this type-check.

So good so far. But that’s easy. Let’s delve deeper.

Constant Non-Builtin Type Decoration

Let’s define a trivial function annotated by type hints that are pure-Python classes rather than builtin types:

from argparse import ArgumentParser
from beartype import beartype

@beartype
def law_of_the_jungle_5(a_cub_may_be_bought_at_a_price: ArgumentParser):
    return a_cub_may_be_bought_at_a_price

Let’s see the wrapper function @beartype dynamically generated from that:

def __beartyped_law_of_the_jungle_5(
    *args,
    __beartype_func=__beartype_func,
    __beartypistry=__beartypistry,
    **kwargs
):
    # Localize the number of passed positional arguments for efficiency.
    __beartype_args_len = len(args)
    # Localize this positional or keyword parameter if passed *OR* to the
    # sentinel value "__beartypistry" guaranteed to never be passed otherwise.
    __beartype_pith_0 = (
        args[0] if __beartype_args_len > 0 else
        kwargs.get('a_cub_may_be_bought_at_a_price', __beartypistry)
    )

    # If this parameter was passed...
    if __beartype_pith_0 is not __beartypistry:
        # Type-check this passed parameter or return value against this
        # PEP-compliant type hint.
        if not isinstance(__beartype_pith_0, __beartypistry['argparse.ArgumentParser']):
            __beartype_raise_pep_call_exception(
                func=__beartype_func,
                pith_name='a_cub_may_be_bought_at_a_price',
                pith_value=__beartype_pith_0,
            )

    # Call this function with all passed parameters and return the value
    # returned from this call.
    return __beartype_func(*args, **kwargs)

The result is largely the same. The only meaningful difference is the type-check on line 20:

if not isinstance(__beartype_pith_0, __beartypistry['argparse.ArgumentParser']):

Since we annotated that function with a pure-Python class rather than builtin type, @beartype registered that class with the __beartypistry at decoration time and then subsequently looked that class up with its fully-qualified classname at call time to perform this type-check.

So good so far… so what! Let’s spelunk harder.

Constant Shallow Sequence Decoration

Let’s define a trivial function annotated by type hints that are PEP 585-compliant builtin types subscripted by ignorable arguments:

from beartype import beartype

@beartype
def law_of_the_jungle_6(all_the_jungle_is_thine: list[object]):
    return all_the_jungle_is_thine

Let’s see the wrapper function @beartype dynamically generated from that:

def __beartyped_law_of_the_jungle_6(
    *args,
    __beartype_func=__beartype_func,
    __beartypistry=__beartypistry,
    **kwargs
):
    # Localize the number of passed positional arguments for efficiency.
    __beartype_args_len = len(args)
    # Localize this positional or keyword parameter if passed *OR* to the
    # sentinel value "__beartypistry" guaranteed to never be passed otherwise.
    __beartype_pith_0 = (
        args[0] if __beartype_args_len > 0 else
        kwargs.get('all_the_jungle_is_thine', __beartypistry)
    )

    # If this parameter was passed...
    if __beartype_pith_0 is not __beartypistry:
        # Type-check this passed parameter or return value against this
        # PEP-compliant type hint.
        if not isinstance(__beartype_pith_0, list):
            __beartype_raise_pep_call_exception(
                func=__beartype_func,
                pith_name='all_the_jungle_is_thine',
                pith_value=__beartype_pith_0,
            )

    # Call this function with all passed parameters and return the value
    # returned from this call.
    return __beartype_func(*args, **kwargs)

We are still within the realm of normalcy. Correctly detecting this type hint to be subscripted by an ignorable argument, @beartype only bothered type-checking this parameter to be an instance of this builtin type:

if not isinstance(__beartype_pith_0, list):

It’s time to iteratively up the ante.

Constant Deep Sequence Decoration

Let’s define a trivial function annotated by type hints that are PEP 585-compliant builtin types subscripted by builtin types:

from beartype import beartype

@beartype
def law_of_the_jungle_7(kill_everything_that_thou_canst: list[str]):
    return kill_everything_that_thou_canst

Let’s see the wrapper function @beartype dynamically generated from that:

def __beartyped_law_of_the_jungle_7(
    *args,
    __beartype_func=__beartype_func,
    __beartypistry=__beartypistry,
    **kwargs
):
    # Generate and localize a sufficiently large pseudo-random integer for
    # subsequent indexation in type-checking randomly selected container items.
    __beartype_random_int = __beartype_getrandbits(64)
    # Localize the number of passed positional arguments for efficiency.
    __beartype_args_len = len(args)
    # Localize this positional or keyword parameter if passed *OR* to the
    # sentinel value "__beartypistry" guaranteed to never be passed otherwise.
    __beartype_pith_0 = (
        args[0] if __beartype_args_len > 0 else
        kwargs.get('kill_everything_that_thou_canst', __beartypistry)
    )

    # If this parameter was passed...
    if __beartype_pith_0 is not __beartypistry:
        # Type-check this passed parameter or return value against this
        # PEP-compliant type hint.
        if not (
            # True only if this pith shallowly satisfies this hint.
            isinstance(__beartype_pith_0, list) and
            # True only if either this pith is empty *OR* this pith is
            # both non-empty and deeply satisfies this hint.
            (not __beartype_pith_0 or isinstance(__beartype_pith_0[__beartype_random_int % len(__beartype_pith_0)], str))
        ):
            __beartype_raise_pep_call_exception(
                func=__beartype_func,
                pith_name='kill_everything_that_thou_canst',
                pith_value=__beartype_pith_0,
            )

    # Call this function with all passed parameters and return the value
    # returned from this call.
    return __beartype_func(*args, **kwargs)

We have now diverged from normalcy. Let’s dismantle this iota by iota:

  • __beartype_random_int is a pseudo-random unsigned 32-bit integer whose bit length intentionally corresponds to the number of bits generated by each call to Python’s C-based Mersenne Twister internally performed by the random.getrandbits function generating this integer. Exceeding this length would cause that function to internally perform that call multiple times for no gain. Since the cost of generating integers to this length is the same as generating integers of smaller lengths, this length is preferred. Since most sequences are likely to contain fewer items than this integer, pseudo-random sequence items are indexable by taking the modulo of this integer with the sizes of those sequences. For big sequences containing more than this number of items, beartype deeply type-checks leading items with indices in this range while ignoring trailing items. Given the practical infeasibility of storing big sequences in memory, this seems an acceptable real-world tradeoff. Suck it, big sequences!

  • As before, @beartype first type-checks this parameter to be a list.

  • @beartype then type-checks this parameter to either be:

    • not __beartype_pith_0, an empty list.

    • isinstance(__beartype_pith_0[__beartype_random_int % len(__beartype_pith_0)], str), a non-empty list whose pseudo-randomly indexed list item satisfies this nested builtin type.

Well, that escalated quickly.

Constant Nested Deep Sequence Decoration

Let’s define a trivial function annotated by type hints that are PEP 585-compliant builtin types recursively subscripted by instances of themselves, because we are typing masochists:

from beartype import beartype

@beartype
def law_of_the_jungle_8(pull_thorns_from_all_wolves_paws: (
    list[list[list[str]]])):
    return pull_thorns_from_all_wolves_paws

Let’s see the wrapper function @beartype dynamically generated from that:

def __beartyped_law_of_the_jungle_8(
    *args,
    __beartype_func=__beartype_func,
    __beartypistry=__beartypistry,
    **kwargs
):
    # Generate and localize a sufficiently large pseudo-random integer for
    # subsequent indexation in type-checking randomly selected container items.
    __beartype_random_int = __beartype_getrandbits(32)
    # Localize the number of passed positional arguments for efficiency.
    __beartype_args_len = len(args)
    # Localize this positional or keyword parameter if passed *OR* to the
    # sentinel value "__beartypistry" guaranteed to never be passed otherwise.
    __beartype_pith_0 = (
        args[0] if __beartype_args_len > 0 else
        kwargs.get('pull_thorns_from_all_wolves_paws', __beartypistry)
    )

    # If this parameter was passed...
    if __beartype_pith_0 is not __beartypistry:
        # Type-check this passed parameter or return value against this
        # PEP-compliant type hint.
        if not (
            # True only if this pith shallowly satisfies this hint.
            isinstance(__beartype_pith_0, list) and
            # True only if either this pith is empty *OR* this pith is
            # both non-empty and deeply satisfies this hint.
            (not __beartype_pith_0 or (
                # True only if this pith shallowly satisfies this hint.
                isinstance(__beartype_pith_1 := __beartype_pith_0[__beartype_random_int % len(__beartype_pith_0)], list) and
                # True only if either this pith is empty *OR* this pith is
                # both non-empty and deeply satisfies this hint.
                (not __beartype_pith_1 or (
                    # True only if this pith shallowly satisfies this hint.
                    isinstance(__beartype_pith_2 := __beartype_pith_1[__beartype_random_int % len(__beartype_pith_1)], list) and
                    # True only if either this pith is empty *OR* this pith is
                    # both non-empty and deeply satisfies this hint.
                    (not __beartype_pith_2 or isinstance(__beartype_pith_2[__beartype_random_int % len(__beartype_pith_2)], str))
                ))
            ))
        ):
            __beartype_raise_pep_call_exception(
                func=__beartype_func,
                pith_name='pull_thorns_from_all_wolves_paws',
                pith_value=__beartype_pith_0,
            )

    # Call this function with all passed parameters and return the value
    # returned from this call.
    return __beartype_func(*args, **kwargs)

We are now well beyond the deep end, where the benthic zone and the cruel denizens of the fathomless void begins. Let’s dismantle this pascal by pascal:

  • __beartype_pith_1 := __beartype_pith_0[__beartype_random_int % len(__beartype_pith_0)], a PEP 572-style assignment expression localizing repeatedly accessed random items of the first nested list for efficiency.

  • __beartype_pith_2 := __beartype_pith_1[__beartype_random_int % len(__beartype_pith_1)], a similar expression localizing repeatedly accessed random items of the second nested list.

  • The same __beartype_random_int pseudo-randomly indexes all three lists.

  • Under older Python interpreters lacking PEP 572 support, @beartype generates equally valid (albeit less efficient) code repeating each nested list item access.

In the kingdom of the linear-time runtime type checkers, the constant-time runtime type checker really stands out like a sore giant squid, doesn’t it?

See the Developers section for further commentary on runtime optimization from the higher-level perspective of architecture and internal API design.

Developers

Let’s contribute pull requests to beartype for the good of typing. The primary maintainer of this repository is a friendly beardless Canadian guy who guarantees that he will always be nice and congenial and promptly merge all requests that pass continuous integration (CI) tests.

And thanks for merely reading this! Like all open-source software, beartype thrives on community contributions, activity, and interest. This means you, stalwart Python hero.

beartype has two problem spots (listed below in order of decreasing importance and increasing complexity) that could always benefit from a volunteer army of good GitHub Samaritans. Let’s take this from the top.

Moar Depth

So, you want to help beartype deeply type-check even more type hints than she already does? Let us help you help us, because you are awesome.

First, an egregious lore dump. It’s commonly assumed that beartype only internally implements a single type-checker. After all, every other static and runtime type-checker only internally implements a single type-checker. Why would a type-checker internally implement several divergent overlapping type-checkers and… what would that even mean? Who would be so vile, cruel, and sadistic as to do something like that?

We would. beartype often violates assumptions. This is no exception. Externally, of course, beartype presents itself as a single type-checker. Internally, beartype is implemented as a two-phase series of orthogonal type-checkers. Why? Because efficiency, which is the reason we are all here. These type-checkers are (in the order that callables decorated by beartype perform them at runtime):

  1. Testing phase. In this fast first pass, each callable decorated by @beartype only tests whether all parameters passed to and values returned from the current call to that callable satisfy all type hints annotating that callable. This phase does not raise human-readable exceptions (in the event that one or more parameters or return values fails to satisfy these hints). @beartype highly optimizes this phase by dynamically generating one wrapper function wrapping each decorated callable with unique pure-Python performing these tests in O(1) constant-time. This phase is always unconditionally performed by code dynamically generated and returned by:

    • The fast-as-lightning pep_code_check_hint() function declared in the “beartype._decor._code._pep._pephint” submodule, which generates memoized O(1) code type-checking an arbitrary object against an arbitrary PEP-compliant type hint by iterating over all child hints nested in that hint with a highly optimized breadth-first search (BFS) leveraging extreme caching, fragile cleverness, and other salacious micro-optimizations.

  2. Error phase. In this slow second pass, each call to a callable decorated by @beartype that fails the fast first pass (due to one or more parameters or return values failing to satisfy these hints) recursively discovers the exact underlying cause of that failure and raises a human-readable exception precisely detailing that cause. @beartype does not optimize this phase whatsoever. Whereas the implementation of the first phase is uniquely specific to each decorated callable and constrained to O(1) constant-time non-recursive operation, the implementation of the second phase is generically shared between all decorated callables and generalized to O(n) linear-time recursive operation. Efficiency no longer matters when you’re raising exceptions. Exception handling is slow in any language and doubly slow in dynamically-typed (and mostly interpreted) languages like Python, which means that performance is mostly a non-concern in “cold” code paths guaranteed to raise exceptions. This phase is only conditionally performed when the first phase fails by:

    • The slow-as-molasses raise_pep_call_exception() function declared in the “beartype._decor._code._pep._error.peperror” submodule, which generates human-readable exceptions after performing unmemoized O(n) type-checking of an arbitrary object against a PEP-compliant type hint by recursing over all child hints nested in that hint with an unoptimized recursive algorithm prioritizing debuggability, readability, and maintainability.

This separation of concerns between performant O(1) testing on the one hand and perfect O(n) error handling on the other preserves both runtime performance and readable errors at a cost of developer pain. This is good! …what?

Secondly, the same separation of concerns also complicates the development of @beartype. This is bad. Since @beartype internally implements two divergent type-checkers, deeply type-checking a new category of type hint requires adding that support to (wait for it) two divergent type-checkers – which, being fundamentally distinct codebases sharing little code in common, requires violating the Don’t Repeat Yourself (DRY) principle by reinventing the wheel in the second type-checker. Such is the high price of high-octane performance. You probably thought this would be easier and funner. So did we.

Thirdly, this needs to be tested. After surmounting the above roadblocks by deeply type-checking that new category of type hint in both type-checkers, you’ll now add one or more unit tests exhaustively exercising that checking. Thankfully, we already did all of the swole lifting for you. All you need to do is add at least one PEP-compliant type hint, one object satisfying that hint, and one object not satisfying that hint to:

You’re done! Praise Guido.

Moar Compliance

So, you want to help beartype comply with even more Python Enhancement Proposals (PEPs) than she already complies with? Let us help you help us, because you are young and idealistic and you mean well.

You will need a spare life to squander. A clone would be most handy. In short, you will want to at least:

You’re probably not done by a long shot! But the above should at least get you fitfully started, though long will you curse our names. Praise Cleese.

License

beartype is open-source software released under the permissive MIT license.

Funding

beartype is currently financed as a purely volunteer open-source project – which is to say, it’s unfinanced. Prior funding sources (yes, they once existed) include:

  1. Over the period 2015—2018 preceding the untimely death of Paul Allen, beartype was graciously associated with the Paul Allen Discovery Center at Tufts University and grant-funded by a Paul Allen Discovery Center award from the Paul G. Allen Frontiers Group through its parent applications – the multiphysics biology simulators BETSE and BETSEE.

See Also

Runtime type checkers (i.e., third-party mostly pure-Python packages dynamically validating Python callable types at Python runtime, typically via decorators, explicit function calls, and import hooks) include:

Static type checkers (i.e., third-party tooling not implemented in Python statically validating Python callable and/or variable types across a full application stack at tool rather than Python runtime) include:

  • mypy.

  • Pyre, published by FaceBook. …yah.

  • pyright, published by Microsoft.

  • pytype, published by Google.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

beartype-0.5.1.tar.gz (354.3 kB view hashes)

Uploaded Source

Built Distribution

beartype-0.5.1-py3-none-any.whl (260.9 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page