Skip to main content

Unbearably fast runtime type checking in pure Python.

Project description

beartype continuous integration (CI) status beartype test coverage 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 constant-time runtime type checker emphasizing efficiency, portability, and thrilling puns.

# Install beartype.
$ pip3 install beartype

# So let's do this.
$ python3
# Import the @beartype decorator.
>>> from beartype import beartype

# Annotate @beartype-decorated callables with type hints.
>>> @beartype
... def quote_wiggum(lines: list[str]) -> None:
...     print('“{}\n\t— Police Chief Wiggum'.format("\n ".join(lines)))

# Call those callables with valid parameters.
>>> quote_wiggum(["Okay, folks. Show's over!", "Nothing to see here. Show's…",])
Okay, folks. Show's over!
 Nothing to see here. Show's…”
    Police Chief Wiggum

# Call those callables with invalid parameters.
>>> quote_wiggum([b"Oh, my God! A horrible plane crash!", b"Hey, everybody! Get a load of this flaming wreckage!",])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 30, in quote_wiggum
  File "/home/springfield/beartype/lib/python3.9/site-packages/beartype/_decor/_code/_pep/_error/errormain.py", line 220, in raise_pep_call_exception
    raise exception_cls(
beartype.roar.BeartypeCallHintPepParamException: @beartyped
quote_wiggum() parameter lines=[b'Oh, my God! A horrible plane
crash!', b'Hey, everybody! Get a load of thi...'] violates type hint
list[str], as list item 0 value b'Oh, my God! A horrible plane crash!'
not str.

# Squash bugs by refining type hints with beartype validators.
# Import the requisite machinery.
>>> from beartype.vale import Is
>>> from typing import Annotated   # <--------------- if Python ≥ 3.9.0
# >>> from typing_extensions import Annotated   # <-- if Python < 3.9.0

# Define validators by combining type hints with lambda functions.
# This validator accepts any non-empty list of strings.
>>> ListOfSomeStrings = Annotated[list[str], Is[lambda lst: bool(lst)]]

# Annotate @beartype-decorated callables with validators.
>>> @beartype
... def quote_wiggum_safer(lines: ListOfSomeStrings) -> None:
...     print('“{}\n\t— Police Chief Wiggum'.format("\n ".join(lines)))

# Call those callables with invalid parameters.
>>> quote_wiggum_safer([])
beartype.roar.BeartypeCallHintPepParamException: @beartyped
quote_wiggum_safer() parameter lines=[] violates type hint
typing.Annotated[list[str], Is[lambda lst: bool(lst)]], as value []
violates validator Is[lambda lst: bool(lst)].

# Lastly, type-check anything against any type hint... anytime.
# Import the is_bearable() tester.
>>> from beartype.abby import is_bearable

# Decide whether an object satisfies a type hint – just like
# isinstance() or issubclass() but for type hints. Power erupts.
>>> is_bearable(['The', 'goggles', 'do', 'nothing.'], list[str])
True
>>> is_bearable([0xCAFEBEEF, 0x8BADF00D], ListOfSomeStrings)
False

Beartype brings Rust- and C++-inspired zero-cost abstractions into the lawless world of dynamically-typed Python by enforcing type safety at the granular level of functions and methods against type hints standardized by the Python community in O(1) non-amortized worst-case time with negligible constant factors. If the prior sentence was unreadable jargon, see our friendly and approachable FAQ for a human-readable synopsis.

Beartype is portably implemented in Python 3, continuously stress-tested via GitHub Actions + tox + pytest + Codecov, and permissively distributed under the MIT license. Beartype has no runtime dependencies, only one test-time dependency, and only one documentation-time dependency. Beartype supports all actively developed Python versions, all Python package managers, and multiple platform-specific package managers.

Beartype powers quality assurance across the Python ecosystem.

Graspologic: Python package for graph statistical algorithms Hydra-Zen: Reproducible Pythonic CLI<->config APIs via Hydra PyCrisp: Typed Python interactions with the Crisp API SentiPy: Typed Python interactions with the Sentiment Investor API


Install

Let’s install beartype with pip:

pip3 install beartype

Let’s install beartype with Anaconda:

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

Commemorate this moment in time with bear-ified, our overbearing project shield. What says quality like a bear on a badge, amirite?

Platform

Beartype is also installable with platform-specific package managers, because sometimes you just need this thing to work.

macOS

Let’s install beartype with Homebrew on macOS courtesy our third-party tap:

brew install beartype/beartype/beartype

Let’s install beartype with MacPorts on macOS:

sudo port install py-beartype

A big bear hug to our official macOS package maintainer @harens for packaging beartype for our Apple-appreciating audience.

Linux

Let’s install beartype with emerge on Gentoo courtesy a third-party overlay, because source-based Linux distributions are the CPU-bound nuclear option:

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

What could be simpler? O_o

Badge

If you’re feeling the quality assurance and want to celebrate, consider signaling that you’re now publicly bear-ified:

YummySoft is now bear-ified!

All this magic and possibly more can be yours with:

  • Markdown:

    YummySoft is now [![bear-ified](https://raw.githubusercontent.com/beartype/beartype-assets/main/badge/bear-ified.svg)](https://beartype.rtfd.io)!
  • reStructuredText:

    YummySoft is now |bear-ified|!
    
    .. # See https://docutils.sourceforge.io/docs/ref/rst/directives.html#image
    .. |bear-ified| image:: https://raw.githubusercontent.com/beartype/beartype-assets/main/badge/bear-ified.svg
       :align: top
       :target: https://beartype.rtfd.io
       :alt: bear-ified
  • Raw HTML:

    YummySoft is now <a href="https://beartype.rtfd.io"><img
      src="https://raw.githubusercontent.com/beartype/beartype-assets/main/badge/bear-ified.svg"
      alt="bear-ified"
      style="vertical-align: middle;"></a>!

Let a soothing pastel bear give your users the reassuring OK sign.

Overview

Beartype is a novel first line of defense. In Python’s vast arsenal of software quality assurance (SQA), beartype holds the shield wall against breaches in type safety by improper parameter and return values violating developer expectations.

Beartype is unopinionated. Beartype inflicts no developer constraints beyond importation and usage of a single configuration-free decorator. Beartype is trivially integrated into new and existing applications, stacks, modules, and scripts already annotating callables with PEP-compliant industry-standard type hints.

Beartype is zero-cost. Beartype inflicts no harmful developer tradeoffs, instead stressing expense-free strategies at both:

Versus Static Type Checkers

Like competing static type checkers operating at the coarse-grained application level via 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:

Versus Runtime Type Checkers

Unlike comparable runtime type checkers (e.g., pydantic, 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:

Frequently Asked Questions (FAQ)

What is beartype?

Why, it’s the world’s first O(1) runtime type checker in any dynamically-typed lang… oh, forget it.

You know typeguard? Then you know beartype – more or less. beartype is typeguard’s younger, faster, and slightly sketchier brother who routinely ingests performance-enhancing anabolic nootropics.

What is typeguard?

Okay. Work with us here, people.

You know how in low-level statically-typed memory-unsafe languages that no one should use like C and C++, the compiler validates at compilation time the types of all values passed to and returned from all functions and methods across the entire codebase?

$ gcc -Werror=int-conversion -xc - <<EOL
#include <stdio.h>
int main() {
    printf("Hello, world!");
    return "Goodbye, world.";
}
EOL
<stdin>: In function ‘main’:
<stdin>:4:11: error: returning ‘char *’ from a function with return type
‘int’ makes integer from pointer without a cast [-Werror=int-conversion]
cc1: some warnings being treated as errors

You know how in high-level duck-typed languages that everyone should use instead like Python and Ruby, the interpreter performs no such validation at any interpretation phase but instead permits any arbitrary values to be passed to or returned from any function or method?

$ python3 - <<EOL
def main() -> int:
    print("Hello, world!");
    return "Goodbye, world.";
main()
EOL

Hello, world!

Runtime type checkers like beartype and typeguard selectively shift the dial on type safety in Python from duck to static typing while still preserving all of the permissive benefits of the former as a default behaviour.

$ python3 - <<EOL
from beartype import beartype
@beartype
def main() -> int:
    print("Hello, world!");
    return "Goodbye, world.";
main()
EOL

Hello, world!
Traceback (most recent call last):
  File "<stdin>", line 6, in <module>
  File "<string>", line 17, in main
  File "/home/leycec/py/beartype/beartype/_decor/_code/_pep/_error/errormain.py", line 218, in raise_pep_call_exception
    raise exception_cls(
beartype.roar.BeartypeCallHintPepReturnException: @beartyped main() return
'Goodbye, world.' violates type hint <class 'int'>, as value 'Goodbye,
world.' not int.

When should I use beartype?

Use beartype to assure the quality of Python code beyond what tests alone can assure. If you have yet to test, do that first with a pytest-based test suite, tox configuration, and continuous integration (CI). If you have any time, money, or motivation left, annotate callables with PEP-compliant type hints and decorate those callables with the @beartype.beartype decorator.

Prefer beartype over other runtime and static type checkers whenever you lack control over the objects passed to or returned from your callables – especially whenever you cannot limit the size of those objects. This includes common developer scenarios like:

  • You are the author of an open-source library intended to be reused by a general audience.

  • You are the author of a public app accepting as input or generating as output sufficiently large data internally passed to or returned from app callables.

If none of the above apply, prefer beartype over static type checkers whenever:

  • You want to check types decidable only at runtime.

  • You want to write code rather than fight a static type checker, because static type inference of a dynamically-typed language is guaranteed to fail and frequently does. If you’ve ever cursed the sky after suffixing working code incorrectly typed by mypy with non-portable vendor-specific pragmas like # type: ignore[{unreadable_error}], beartype was written for you.

  • You want to preserve dynamic typing, because Python is a dynamically-typed language. Unlike beartype, static type checkers enforce static typing and are thus strongly opinionated; they believe dynamic typing is harmful and emit errors on dynamically-typed code. This includes common use patterns like changing the type of a variable by assigning that variable a value whose type differs from its initial value. Want to freeze a variable from a set into a frozenset? That’s sad, because static type checkers don’t want you to. In contrast:

    Beartype never emits errors, warnings, or exceptions on dynamically-typed code, because Python is not an error.

    Beartype believes dynamic typing is beneficial by default, because Python is beneficial by default.

    Beartype is unopinionated. That’s because beartype operates exclusively at the higher level of pure-Python callables rather than the lower level of individual statements inside pure-Python callables. Unlike static type checkers, beartype can’t be opinionated about things that no one should be.

If none of the above still apply, still use beartype. It’s free as in beer and speech, cost-free at installation- and runtime, and transparently stacks with existing type-checking solutions. Leverage beartype until you find something that suites you better, because beartype is always better than nothing.

Why should I use beartype?

The idea of beartype is that it never costs you anything. It might not do as much as you’d like, but it will always do something – which is more than Python’s default behaviour, which is to do nothing and ignore type hints altogether. This means you can always safely add beartype to any Python package, module, app, or script regardless of size, scope, funding, or audience and never worry about your backend Django server taking a nosedive on St. Patty’s Day just because your frontend React client helpfully sent a 5MB JSON file serializing a doubly-nested list of integers.

The idea of typeguard is that it does everything. If you annotate a function decorated by typeguard as accepting a triply-nested list of integers and pass that function a list of 1,000 nested lists of 1,000 nested lists of 1,000 integers, every call to that function will check every integer transitively nested in that list – even if that list never changes. Did we mention that list transitively contains 1,000,000,000 integers in total?

$ python3 -m timeit -n 1 -r 1 -s '
from typeguard import typechecked
@typechecked
def behold(the_great_destroyer_of_apps: list[list[list[int]]]) -> int:
    return len(the_great_destroyer_of_apps)
' 'behold([[[0]*1000]*1000]*1000)'

1 loop, best of 1: 6.42e+03 sec per loop

Yes, 6.42e+03 sec per loop == 6420 seconds == 107 minutes == 1 hour, 47 minutes to check a single list once. Yes, it’s an uncommonly large list, but it’s still just a list. This is the worst-case cost of a single call to a function decorated by a naïve runtime type checker.

What does beartype do?

Generally, as little as it can while still satisfying the accepted definition of “runtime type checker.” Specifically, beartype performs a one-way random walk over the expected data structure of objects passed to and returned from @beartype-decorated functions and methods. Basically, beartype type-checks randomly sampled data.

Consider the prior example of a function annotated as accepting a triply-nested list of integers passed a list containing 1,000 nested lists each containing 1,000 nested lists each containing 1,000 integers.

When decorated by typeguard, every call to that function checks every integer nested in that list.

When decorated by beartype, every call to the same function checks only a single random integer contained in a single random nested list contained in a single random nested list contained in that parent list. This is what we mean by the quaint phrase “one-way random walk over the expected data structure.”

$ python3 -m timeit -n 1024 -r 4 -s '
from beartype import beartype
@beartype
def behold(the_great_destroyer_of_apps: list[list[list[int]]]) -> int:
   return len(the_great_destroyer_of_apps)
' 'behold([[[0]*1000]*1000]*1000)'

1024 loops, best of 4: 13.8 usec per loop

13.8 usec per loop == 13.8 microseconds = 0.0000138 seconds to transitively check only a random integer nested in a single triply-nested list passed to each call of that function. This is the worst-case cost of a single call to a function decorated by an O(1) runtime type checker.

How do I type-check…

…yes? Go on.

…Boto3 types?

tl;dr: You just want bearboto3, a well-maintained third-party package cleanly integrating beartype + Boto3. But you’re not doing that. You’re reading on to find out why you want bearboto3, aren’t you? I knew it.

Boto3 is the official Amazon Web Services (AWS) Software Development Kit (SDK) for Python. Type-checking Boto3 types is decidedly non-trivial, because Boto3 dynamically fabricates unimportable types from runtime service requests. These types cannot be externally accessed and thus cannot be used as type hints.

H-hey! Put down the hot butter knife. Your Friday night may be up in flames, but we’re gonna put out the fire. It’s what we do here. Now, you have two competing solutions with concomitant tradeoffs. You can type-check Boto3 types against either:

  • Static type checkers (e.g., mypy, pyright) by importing Boto3 stub types from an external third-party dependency (e.g., mypy-boto3), enabling context-aware code completion across compliant IDEs (e.g., PyCharm, VSCode Pylance). Those types are merely placeholder stubs; they do not correspond to actual Boto3 types and thus break runtime type checkers (including beartype) when used as type hints.

  • Beartype by fabricating your own PEP-compliant beartype validators, enabling beartype to validate arbitrary objects against actual Boto3 types at runtime when used as type hints. You already require beartype, so no additional third-party dependencies are required. Those validators are silently ignored by static type checkers; they do not enable context-aware code completion across compliant IDEs.

“B-but that sucks! How can we have our salmon and devour it too?”, you demand with a tremulous quaver. Excessive caffeine and inadequate gaming did you no favors tonight. You know this. Yet again you reach for the hot butter knife.

H-hey! You can, okay? You can have everything that market forces demand. Bring to bear cough the combined powers of PEP 484-compliant type aliases, the PEP 484-compliant “typing.TYPE_CHECKING” boolean global, and beartype validators to satisfy both static and runtime type checkers:

# Import the requisite machinery.
from beartype import beartype
from boto3 import resource
from boto3.resources.base import ServiceResource
from typing import TYPE_CHECKING

# If performing static type-checking (e.g., mypy, pyright), import boto3
# stub types safely usable *ONLY* by static type checkers.
if TYPE_CHECKING:
    from mypy_boto3_s3.service_resource import Bucket
# Else, @beartime-based runtime type-checking is being performed. Alias the
# same boto3 stub types imported above to their semantically equivalent
# beartype validators accessible *ONLY* to runtime type checkers.
else:
    # Import even more requisite machinery. Can't have enough, I say!
    from beartype.vale import IsAttr, IsEqual
    from typing import Annotated   # <--------------- if Python ≥ 3.9.0
    # from typing_extensions import Annotated   # <-- if Python < 3.9.0

    # Generalize this to other boto3 types by copy-and-pasting this and
    # replacing the base type and "s3.Bucket" with the wonky runtime names
    # of those types. Sadly, there is no one-size-fits all common base class,
    # but you should find what you need in the following places:
    # * "boto3.resources.base.ServiceResource".
    # * "boto3.resources.collection.ResourceCollection".
    # * "botocore.client.BaseClient".
    # * "botocore.paginate.Paginator".
    # * "botocore.waiter.Waiter".
    Bucket = Annotated[ServiceResource,
        IsAttr['__class__', IsAttr['__name__', IsEqual["s3.Bucket"]]]]

# Do this for the good of the gross domestic product, @beartype.
@beartype
def get_s3_bucket_example() -> Bucket:
    s3 = resource('s3')
    return s3.Bucket('example')

You’re welcome.

…NumPy arrays?

Beartype fully supports typed NumPy arrays. Because beartype cares.

…mock types?

Beartype fully relies upon the isinstance() builtin under the hood for its low-level runtime type-checking needs. If you can fool isinstance(), you can fool beartype. Can you fool beartype into believing an instance of a mock type is an instance of the type it mocks, though?

You bet your bottom honey barrel. In your mock type, just define a new __class__() property returning the original type: e.g.,

>>> class OriginalType: pass
>>> class MockType:
...     @property
...     def __class__(self) -> OriginalType: return OriginalType
>>> from beartype import beartype
>>> @beartype
... def muh_func(self, muh_arg: OriginalType): print('Yolo, bro.')
>>> muh_func(MockType())
Yolo, bro.

This is why we beartype.

Usage

Beartype makes type-checking painless, portable, and purportedly fun. Just:

Decorate functions and methods annotated by standard type hints with the @beartype.beartype decorator, which wraps those functions and methods in performant type-checking dynamically generated on-the-fly.

When standard type hints fail to support your use case, annotate functions and methods with beartype-specific validator type hints instead. Validators enforce runtime constraints on the internal structure and contents of parameters and returns via simple caller-defined lambda functions and declarative expressions – all seamlessly composable with standard type hints in an expressive domain-specific language (DSL) designed just for you.

“Embrace the bear,” says the bear peering over your shoulder as you read this.

Standard Hints

Beartype supports most type hints standardized by the developer community through Python Enhancement Proposals (PEPs). Since type hinting is its own special hell, we’ll start by wading into the thalassophobia-inducing waters of type-checking with a sane example – the O(1) @beartype way.

Toy Example

Let’s type-check a "Hello, Jungle!" toy example. Just:

  1. Import the @beartype.beartype decorator:

    from beartype import beartype
  2. Decorate any annotated function with that decorator:

    from sys import stderr, stdout
    from typing import TextIO
    
    @beartype
    def hello_jungle(
        sep: str = ' ',
        end: str = '\n',
        file: TextIO = stdout,
        flush: bool = False,
    ):
        '''
        Print "Hello, Jungle!" to a stream, or to sys.stdout by default.
    
        Optional keyword arguments:
        file:  a file-like object (stream); defaults to the current sys.stdout.
        sep:   string inserted between values, default a space.
        end:   string appended after the last value, default a newline.
        flush: whether to forcibly flush the stream.
        '''
    
        print('Hello, Jungle!', sep, end, file, flush)
  3. Call that function with valid parameters and caper as things work:

    >>> hello_jungle(sep='...ROOOAR!!!!', end='uhoh.', file=stderr, flush=True)
    Hello, Jungle! ...ROOOAR!!!! uhoh.
  4. Call that function with invalid parameters and cringe as things blow up with human-readable exceptions exhibiting the single cause of failure:

    >>> hello_jungle(sep=(
    ...     b"What? Haven't you ever seen a byte-string separator before?"))
    BeartypeCallHintPepParamException: @beartyped hello_jungle() parameter
    sep=b"What? Haven't you ever seen a byte-string separator before?"
    violates type hint <class 'str'>, as value b"What? Haven't you ever seen
    a byte-string separator before?" not str.

Industrial Example

Let’s wrap the third-party numpy.empty_like() function with automated runtime type checking to demonstrate beartype’s support for non-trivial combinations of nested type hints compliant with different PEPs:

from beartype import beartype
from collections.abc import Sequence
from typing import Optional, Union
import numpy as np

@beartype
def empty_like_bear(
    prototype: object,
    dtype: Optional[np.dtype] = None,
    order: str = 'K',
    subok: bool = True,
    shape: Optional[Union[int, Sequence[int]]] = None,
) -> np.ndarray:
    return np.empty_like(prototype, dtype, order, subok, shape)

Note the non-trivial hint for the optional shape parameter, synthesized from a PEP 484-compliant optional of a PEP 484-compliant union of a builtin type and a PEP 585-compliant subscripted abstract base class (ABC), accepting as valid either:

  • The None singleton.

  • An integer.

  • A sequence of integers.

Let’s call that wrapper with both valid and invalid parameters:

>>> empty_like_bear(([1,2,3], [4,5,6]), shape=(2, 2))
array([[94447336794963,              0],
       [             7,             -1]])
>>> empty_like_bear(([1,2,3], [4,5,6]), shape=([2], [2]))
BeartypeCallHintPepParamException: @beartyped empty_like_bear() parameter
shape=([2], [2]) violates type hint typing.Union[int,
collections.abc.Sequence, NoneType], as ([2], [2]):
* Not <class "builtins.NoneType"> or int.
* Tuple item 0 value [2] not int.

Note the human-readable message of the raised exception, containing a bulleted list enumerating the various ways this invalid parameter fails to satisfy its type hint, including the types and indices of the first container item failing to satisfy the nested Sequence[int] hint.

See a subsequent section for actual code dynamically generated by beartype for real-world use cases resembling those above. Fun!

Would You Like to Know More?

If you know type hints, you know beartype. Since beartype is driven entirely by tool-agnostic community standards, the public API for beartype is exactly the sum of those standards. As the user, all you need to know is that decorated callables magically raise human-readable exceptions when you pass parameters or return values violating the PEP-compliant type hints annotating those parameters or returns.

If you don’t know type hints, this is your moment to go deep on the hardest hammer in Python’s SQA toolbox. Here are a few friendly primers to guide you on your maiden voyage through the misty archipelagos of type hinting:

Beartype Validators

Validate anything with two-line type hints
       designed by you ⇄ built by beartype

When official type hints fail to suffice, design your own PEP-compliant type hints with compact two-line beartype validators:

# Import the requisite machinery.
from beartype import beartype
from beartype.vale import Is
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Type hint matching any two-dimensional NumPy array of floats of arbitrary
# precision. Aye, typing matey. Beartype validators a-hoy!
import numpy as np
Numpy2DFloatArray = Annotated[np.ndarray, Is[lambda array:
    array.ndim == 2 and np.issubdtype(array.dtype, np.floating)]]

# Annotate @beartype-decorated callables with beartype validators.
@beartype
def polygon_area(polygon: Numpy2DFloatArray) -> float:
    '''
    Area of a two-dimensional polygon of floats defined as a set of
    counter-clockwise points, calculated via Green's theorem.

    *Don't ask.*
    '''

    # Calculate and return the desired area. Pretend we understand this.
    polygon_rolled = np.roll(polygon, -1, axis=0)
    return np.abs(0.5*np.sum(
        polygon[:,0]*polygon_rolled[:,1] -
        polygon_rolled[:,0]*polygon[:,1]))

Validators enforce arbitrary runtime constraints on the internal structure and contents of parameters and returns with user-defined lambda functions and nestable declarative expressions leveraging familiar “typing” syntax – all seamlessly composable with standard type hints via an expressive domain-specific language (DSL).

Validate custom project constraints now without waiting for the open-source community to officially standardize, implement, and publish those constraints. Filling in the Titanic-sized gaps between Python’s patchwork quilt of PEPs, validators accelerate your QA workflow with your greatest asset.

Yup. It’s your brain.

See Validator Showcase for comforting examples – or blithely continue for uncomfortable details you may regret reading.

Validator Overview

Beartype validators are zero-cost code generators. Like the rest of beartype (but unlike other validation frameworks), beartype validators dynamically generate optimally efficient pure-Python type-checking logic with no hidden function or method calls, undocumented costs, or runtime overhead.

Beartype validator code is thus call-explicit. Since pure-Python function and method calls are notoriously slow in CPython, the code we generate only calls the pure-Python functions and methods you specify when you subscript beartype.vale.Is* classes with those functions and methods. That’s it. We never call anything without your permission. For example:

  • The declarative validator Annotated[np.ndarray, IsAttr['dtype', IsAttr['type', IsEqual[np.float64]]]] detects NumPy arrays of 64-bit floating-point precision by generating the fastest possible inline expression for doing so:

    isinstance(array, np.ndarray) and array.dtype.type == np.float64
  • The functional validator Annotated[np.ndarray, Is[lambda array: array.dtype.type == np.float64]] also detects the same arrays by generating a slightly slower inline expression calling the lambda function you define:

    isinstance(array, np.ndarray) and your_lambda_function(array)

Beartype validators thus come in two flavours – each with its attendant tradeoffs:

  • Functional validators, created by subscripting the beartype.vale.Is class with a function accepting a single parameter and returning True only when that parameter satisfies a caller-defined constraint. Each functional validator incurs the cost of calling that function for each call to each @beartype-decorated callable annotated by that validator, but is Turing-complete and thus supports all possible validation scenarios.

  • Declarative validators, created by subscripting any other class in the beartype.vale subpackage (e.g., beartype.vale.IsEquals) with arguments specific to that class. Each declarative validator generates efficient inline code calling no hidden functions and thus incurring no function costs, but is special-purpose and thus supports only a narrow band of validation scenarios.

Wherever you can, prefer declarative validators for efficiency. Everywhere else, default to functional validators for generality.

Validator API

class beartype.vale.Is[collections.abc.Callable[[typing.Any], bool]]

Functional validator. A PEP-compliant type hint enforcing any arbitrary runtime constraint, created by subscripting (indexing) the Is type hint factory with a function accepting a single parameter and returning either:

  • True if that parameter satisfies that constraint.

  • False otherwise.

# Import the requisite machinery.
from beartype.vale import Is
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Type hint matching only strings with lengths ranging [4, 40].
LengthyString = Annotated[str, Is[lambda text: 4 <= len(text) <= 40]]

Functional validators are caller-defined and may thus validate the internal integrity, consistency, and structure of arbitrary objects ranging from simple builtin scalars like integers and strings to complex data structures defined by third-party packages like NumPy arrays and Pandas DataFrames.

See help(beartype.vale.Is) for further details.

class beartype.vale.IsAttr[str, validator]

Declarative attribute validator. A PEP-compliant type hint enforcing any arbitrary runtime constraint on any named object attribute, created by subscripting (indexing) the IsAttr type hint factory with (in order):

  1. The unqualified name of that attribute.

  2. Any other beartype validator enforcing that constraint.

# Import the requisite machinery.
from beartype.vale import IsAttr, IsEqual
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Type hint matching only two-dimensional NumPy arrays. Given this,
# @beartype generates efficient validation code resembling:
#     isinstance(array, np.ndarray) and array.ndim == 2
import numpy as np
Numpy2DArray = Annotated[np.ndarray, IsAttr['ndim', IsEqual[2]]]

The first argument subscripting this class must be a syntactically valid unqualified Python identifier string containing only alphanumeric and underscore characters (e.g., "dtype", "ndim"). Fully-qualified attributes comprising two or more dot-delimited identifiers (e.g., "dtype.type") may be validated by nesting successive IsAttr subscriptions:

# Type hint matching only NumPy arrays of 64-bit floating-point numbers.
# From this, @beartype generates an efficient expression resembling:
#     isinstance(array, np.ndarray) and array.dtype.type == np.float64
NumpyFloat64Array = Annotated[np.ndarray,
    IsAttr['dtype', IsAttr['type', IsEqual[np.float64]]]]

The second argument subscripting this class must be a beartype validator. This includes:

  • beartype.vale.Is, in which case this parent IsAttr class validates the desired object attribute to satisfy the caller-defined function subscripting that child Is class.

  • beartype.vale.IsAttr, in which case this parent IsAttr class validates the desired object attribute to contain a nested object attribute satisfying the child IsAttr class. See above example.

  • beartype.vale.IsEqual, in which case this IsAttr class validates the desired object attribute to be equal to the object subscripting that IsEqual class. See above example.

See help(beartype.vale.IsAttr) for further details.

class beartype.vale.IsEqual[typing.Any]

Declarative equality validator. A PEP-compliant type hint enforcing equality against any object, created by subscripting (indexing) the IsEqual type hint factory with that object:

# Import the requisite machinery.
from beartype.vale import IsEqual
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Type hint matching only lists equal to [0, 1, 2, ..., 40, 41, 42].
AnswerToTheUltimateQuestion = Annotated[list, IsEqual[list(range(42))]]

beartype.vale.IsEqual generalizes the comparable PEP 586-compliant typing.Literal type hint. Both check equality against user-defined objects. Despite the differing syntax, these two type hints enforce the same semantics:

# This beartype validator enforces the same semantics as...
IsStringEqualsWithBeartype = Annotated[str,
    IsEqual['Don’t you envy our pranceful bands?'] |
    IsEqual['Don’t you wish you had extra hands?']
]

# This PEP 586-compliant type hint.
IsStringEqualsWithPep586 = Literal[
    'Don’t you envy our pranceful bands?',
    'Don’t you wish you had extra hands?',
]

The similarities end there, of course:

  • beartype.vale.IsEqual permissively validates equality against objects that are instances of any arbitrary type. IsEqual doesn’t care what the types of your objects are. IsEqual will test equality against everything you tell it to, because you know best.

  • typing.Literal rigidly validates equality against objects that are instances of only six predefined types:

    • Booleans (i.e., bool objects).

    • Byte strings (i.e., bytes objects).

    • Integers (i.e., int objects).

    • Unicode strings (i.e., str objects).

    • enum.Enum members. [1]

    • The None singleton.

Wherever you can (which is mostly nowhere), prefer typing.Literal. Sure, typing.Literal is mostly useless, but it’s standardized across type checkers in a mostly useless way. Everywhere else, default to beartype.vale.IsEqual.

See help(beartype.vale.IsEqual) for further details.

class beartype.vale.IsInstance[type, …]

Declarative instance validator. A PEP-compliant type hint enforcing instancing of one or more classes, created by subscripting (indexing) the IsInstance type hint factory with those classes:

# Import the requisite machinery.
from beartype.vale import IsInstance
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Type hint matching only string and byte strings, equivalent to:
#     StrOrBytesInstance = Union[str, bytes]
StrOrBytesInstance = Annotated[object, IsInstance[str, bytes]]

beartype.vale.IsInstance generalizes isinstanceable type hints (i.e., normal pure-Python or C-based classes that can be passed as the second parameter to the isinstance() builtin). Both check instancing of classes. Despite the differing syntax, these hints enforce the same semantics:

# This beartype validator enforces the same semantics as...
IsUnicodeStrWithBeartype = Annotated[object, IsInstance[str]]

# ...this PEP 484-compliant type hint.
IsUnicodeStrWithPep484 = str

# Likewise, this beartype validator enforces the same semantics as...
IsStrWithWithBeartype = Annotated[object, IsInstance[str, bytes]]

# ...this PEP 484-compliant type hint.
IsStrWithWithPep484 = Union[str, bytes]

The similarities end there, of course:

  • beartype.vale.IsInstance permissively validates type instancing of arbitrary objects (including possibly nested attributes of parameters and returns when combined with beartype.vale.IsAttr) against one or more classes.

  • Isinstanceable classes rigidly validate type instancing of only parameters and returns against only one class.

Unlike isinstanceable type hints, instance validators support various set theoretic operators. Critically, this includes negation. Instance validators prefixed by the negation operator ~ match all objects that are not instances of the classes subscripting those validators. Wait. Wait just a hot minute there. Doesn’t a typing.Annotated type hint necessarily match instances of the class subscripting that type hint? Yup. This means type hints of the form typing.Annotated[{superclass}, ~IsInstance[{subclass}] match all instances of a superclass that are not also instances of a subclass. And… pretty sure we just invented type hint arithmetic right there.

That sounded intellectual and thus boring. Yet, the disturbing fact that Python booleans are integers yup while Python strings are infinitely recursive sequences of strings yup means that type hint arithmetic can save your codebase from Guido’s younger self. Consider this instance validator matching only non-boolean integers, which cannot be expressed with any isinstanceable type hint (e.g., int) or other combination of standard off-the-shelf type hints (e.g., unions):

# Type hint matching any non-boolean integer. Never fear integers again.
IntNonbool = Annotated[int, ~IsInstance[bool]]   # <--- bruh

Wherever you can, prefer isinstanceable type hints. Sure, they’re inflexible, but they’re inflexibly standardized across type checkers. Everywhere else, default to beartype.vale.IsInstance.

See help(beartype.vale.IsInstance) for further details.

class beartype.vale.IsSubclass[type, …]

Declarative inheritance validator. A PEP-compliant type hint enforcing subclassing of one or more superclasses (base classes), created by subscripting (indexing) the IsSubclass type hint factory with those superclasses:

# Import the requisite machinery.
from beartype.vale import IsSubclass
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Type hint matching only string and byte string subclasses.
StrOrBytesSubclass = Annotated[type, IsSubclass[str, bytes]]

beartype.vale.IsSubclass generalizes the comparable PEP 484-compliant typing.Type and PEP 585-compliant type type hints. All three check subclassing of arbitrary superclasses. Despite the differing syntax, these hints enforce the same semantics:

# This beartype validator enforces the same semantics as...
IsStringSubclassWithBeartype = Annotated[type, IsSubclass[str]]

# ...this PEP 484-compliant type hint as well as...
IsStringSubclassWithPep484 = Type[str]

# ...this PEP 585-compliant type hint.
IsStringSubclassWithPep585 = type[str]

The similarities end there, of course:

  • beartype.vale.IsSubclass permissively validates type inheritance of arbitrary classes (including possibly nested attributes of parameters and returns when combined with beartype.vale.IsAttr) against one or more superclasses.

  • typing.Type and type rigidly validates type inheritance of only parameters and returns against only one superclass.

Consider this subclass validator, which validates type inheritance of a deeply nested attribute and thus cannot be expressed with typing.Type or type:

# Type hint matching only NumPy arrays of reals (i.e., either integers
# or floats) of arbitrary precision, generating code resembling:
#    (isinstance(array, np.ndarray) and
#     issubclass(array.dtype.type, (np.floating, np.integer)))
NumpyRealArray = Annotated[
    np.ndarray, IsAttr['dtype', IsAttr['type', IsSubclass[
        np.floating, np.integer]]]]

Wherever you can, prefer type and typing.Type. Sure, they’re inflexible, but they’re inflexibly standardized across type checkers. Everywhere else, default to beartype.vale.IsSubclass.

See help(beartype.vale.IsSubclass) for further details.

Validator Syntax

Beartype validators support a rich domain-specific language (DSL) leveraging familiar Python operators. Dynamically create new validators on-the-fly from existing validators, fueling reuse and preserving DRY:

  • Negation (i.e., not). Negating any validator with the ~ operator creates a new validator returning True only when the negated validator returns False:

    # Type hint matching only strings containing *no* periods, semantically
    # equivalent to this type hint:
    #     PeriodlessString = Annotated[str, Is[lambda text: '.' not in text]]
    PeriodlessString = Annotated[str, ~Is[lambda text: '.' in text]]
  • Conjunction (i.e., and). And-ing two or more validators with the & operator creates a new validator returning True only when all of the and-ed validators return True:

    # Type hint matching only non-empty strings containing *no* periods,
    # semantically equivalent to this type hint:
    #     NonemptyPeriodlessString = Annotated[
    #         str, Is[lambda text: text and '.' not in text]]
    SentenceFragment = Annotated[str, (
         Is[lambda text: bool(text)] &
        ~Is[lambda text: '.' in text]
    )]
  • Disjunction (i.e., or). Or-ing two or more validators with the | operator creates a new validator returning True only when at least one of the or-ed validators returns True:

    # Type hint matching only empty strings *and* non-empty strings containing
    # one or more periods, semantically equivalent to this type hint:
    #     EmptyOrPeriodfullString = Annotated[
    #         str, Is[lambda text: not text or '.' in text]]
    EmptyOrPeriodfullString = Annotated[str, (
        ~Is[lambda text: bool(text)] |
         Is[lambda text: '.' in text]
    )]
  • Enumeration (i.e., ,). Delimiting two or or more validators with commas at the top level of a typing.Annotated type hint is an alternate syntax for and-ing those validators with the & operator, creating a new validator returning True only when all of those delimited validators return True.

    # Type hint matching only non-empty strings containing *no* periods,
    # semantically equivalent to the "SentenceFragment" defined above.
    SentenceFragment = Annotated[str,
         Is[lambda text: bool(text)],
        ~Is[lambda text: '.' in text],
    ]

    Since the & operator is more explicit and usable in a wider variety of syntactic contexts, the & operator is generally preferable to enumeration (all else being equal).

  • Interoperability. As PEP-compliant type hints, validators are safely interoperable with other PEP-compliant type hints and usable wherever other PEP-compliant type hints are usable. Standard type hints are subscriptable with validators, because validators are standard type hints:

    # Type hint matching only sentence fragments defined as either Unicode or
    # byte strings, generalizing "SentenceFragment" type hints defined above.
    SentenceFragment = Union[
        Annotated[bytes, Is[lambda text: b'.' in text]],
        Annotated[str,   Is[lambda text: u'.' in text]],
    ]

Standard Python precedence rules may apply. DSL: it’s not just a telecom acronym anymore.

Validator Caveats

‼ Validators require:

  • Beartype. Currently, all other static and runtime type checkers silently ignore beartype validators during type-checking. This includes mypy – which we could possibly solve by bundling a mypy plugin with beartype that extends mypy to statically analyze declarative beartype validators (e.g., beartype.vale.IsAttr, beartype.vale.IsEqual). We leave this as an exercise to the idealistic doctoral thesis candidate. Please do this for us, someone who is not us.

  • Either Python ≥ 3.9 or typing_extensions ≥ 3.9.0.0. Validators piggyback onto the typing.Annotated class first introduced with Python 3.9.0 and since backported to older Python versions by the third-party “typing_extensions” package, which beartype also transparently supports.

Validator Showcase

Observe the disturbing (yet alluring) utility of beartype validators in action as they unshackle type hints from the fetters of PEP compliance. Begone, foulest standards!

Type Hint Arithmetic

Subtitle: From Set Theory They Shall Grow

PEP 484 standardized the typing.Union factory disjunctively matching any of several equally permissible type hints ala Python’s builtin or operator or the overloaded | operator for sets. That’s great, because set theory is the beating heart behind type theory.

But that’s just disjunction. What about intersection (e.g., and, &), complementation (e.g., not, ~), or any of the vast multitude of other set theoretic operations? Can we logically connect simple type hints validating trivial constraints into complex type hints validating non-trivial constraints via PEP-standardized analogues of unary and binary operators?

Nope. They don’t exist yet. But that’s okay. You use beartype, which means you don’t have to wait for official Python developers to get there first. You’re already there. …woah

Type Hint Elision

Python’s core type hierarchy conceals an ugly history of secretive backward compatibility. In this subsection, we uncover the two filthiest, flea-infested, backwater corners of the otherwise well-lit atrium that is the Python language – and how exactly you can finalize them. Both obstruct type-checking, readable APIs, and quality assurance in the post-Python 2.7 era.

Guido doesn’t want you to know. But you want to know, don’t you? You are about to enter another dimension, a dimension not only of syntax and semantics but of shame. A journey into a hideous land of annotation wrangling. Next stop… the Beartype Zone. Because guess what?

  • Booleans are integers. They shouldn’t be. Booleans aren’t integers in most high-level languages. Wait. Are you telling me booleans are literally integers in Python? Surely you jest. That can’t be. You can’t add booleans, can you? What would that even mean if you could? Observe and cower, rigorous data engineers.

    >>> True + 3.1415
    4.141500000000001    # <-- oh. by. god.
    >>> isinstance(False, int)
    True                 # <-- when nothing is true, everything is true
  • Strings are infinitely recursive sequences of… yup, it’s strings. They shouldn’t be. Strings aren’t infinitely recursive data structures in any other language devised by incautious mortals – high-level or not. Wait. Are you telling me strings are both indistinguishable from full-blown immutable sequences containing arbitrary items and infinitely recurse into themselves like that sickening non-Euclidean Hall of Mirrors I puked all over when I was a kid? Surely you kid. That can’t be. You can’t infinitely index into strings and pass and return the results to and from callables expecting either Sequence[Any] or Sequence[str] type hints, can you? Witness and tremble, stricter-than-thou QA evangelists.

    >>> 'yougottabekiddi—'[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]
    'y'                 # <-- pretty sure we just broke the world
    >>> from collections.abc import Sequence
    >>> isinstance("Ph'nglui mglw'nafh Cthu—"[0][0][0][0][0], Sequence)
    True                # <-- ...curse you, curse you to heck and back

When we annotate a callable as accepting an int, we never want that callable to also silently accept a bool. Likewise, when we annotate another callable as accepting a Sequence[Any] or Sequence[str], we never want that callable to also silently accept a str. These are sensible expectations – just not in Python, where madness prevails.

To resolve these counter-intuitive concerns, we need the equivalent of the relative set complement (or difference). We now call this thing… type elision! Sounds pretty hot, right? We know.

Let’s first validate non-boolean integers with a beartype validator effectively declaring a new int - bool class (i.e., the subclass of all integers that are not booleans):

# Import the requisite machinery.
from beartype import beartype
from beartype.vale import IsInstance
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Type hint matching any non-boolean integer. This day all errata die.
IntNonbool = Annotated[int, ~IsInstance[bool]]   # <--- bruh

# Type-check zero or more non-boolean integers summing to a non-boolean
# integer. Beartype wills it. So it shall be.
@beartype
def sum_ints(*args: IntNonbool) -> IntNonbool:
    '''
    I cast thee out, mangy booleans!

    You plague these shores no more.
    '''

    return sum(args)

Let’s next validate non-string sequences with beartype validators effectively declaring a new Sequence - str class (i.e., the subclass of all sequences that are not strings):

# Import the requisite machinery.
from beartype import beartype
from beartype.vale import IsInstance
from collections.abc import Sequence
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Type hint matching any non-string sequence. Your day has finally come.
SequenceNonstr = Annotated[Sequence, ~IsInstance[str]]   # <--- we doin this

# Type hint matching any non-string sequence *WHOSE ITEMS ARE ALL STRINGS.*
SequenceNonstrOfStr = Annotated[Sequence[str], ~IsInstance[str]]

# Type-check a non-string sequence of arbitrary items coerced into strings
# and then joined on newline to a new string. (Beartype got your back, bro.)
@beartype
def join_objects(my_sequence: SequenceNonstr) -> str:
    '''
    Your tide of disease ends here, :class:`str` class!
    '''

    return '\n'.join(map(str, my_sequence))  # <-- no idea how that works

# Type-check a non-string sequence whose items are all strings joined on
# newline to a new string. It isn't much, but it's all you ask.
@beartype
def join_strs(my_sequence: SequenceNonstrOfStr) -> str:
    '''
    I expectorate thee up, sequence of strings.
    '''

    return '\n'.join(my_sequence)  # <-- do *NOT* do this to a string
Full-Fat O(n) Matching

Let’s validate all integers in a list of integers in O(n) time, because validators mean you no longer have to accept the QA scraps we feed you:

# Import the requisite machinery.
from beartype import beartype
from beartype.vale import Is
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Type hint matching all integers in a list of integers in O(n) time. Please
# never do this. You now want to, don't you? Why? You know the price! Why?!?
IntList = Annotated[list[int], Is[lambda lst: all(
    isinstance(item, int) for item in lst)]]

# Type-check all integers in a list of integers in O(n) time. How could you?
@beartype
def sum_intlist(my_list: IntList) -> int:
    '''
    The slowest possible integer summation over the passed list of integers.

    There goes your whole data science pipeline. Yikes! So much cringe.
    '''

    return sum(my_list)  # oh, gods what have you done

Welcome to full-fat type-checking. In our disastrous roadmap to beartype 1.0.0, we reluctantly admit that we’d like to augment the @beartype decorator with a new parameter enabling full-fat type-checking. But don’t wait on us. Force the issue now by just doing it yourself and then mocking us all over Gitter! Fight the bear, man.

There are good reasons to believe that O(1) type-checking is preferable. Violating that core precept exposes your codebase to scalability and security concerns. But you’re the Big Boss, you swear you know best, and (in any case) we can’t stop you because we already let the unneutered tomcat out of his trash bin by publishing this API into the badlands of PyPI.

Tensor Property Matching

Let’s validate the same two-dimensional NumPy array of floats of arbitrary precision as in the lead example above with an efficient declarative validator avoiding the additional stack frame imposed by the functional validator in that example:

# Import the requisite machinery.
from beartype import beartype
from beartype.vale import IsAttr, IsEqual, IsSubclass
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Type hint matching only two-dimensional NumPy arrays of floats of
# arbitrary precision. This time, do it faster than anyone has ever
# type-checked NumPy arrays before. (Cue sonic boom, Chuck Yeager.)
import numpy as np
Numpy2DFloatArray = Annotated[np.ndarray,
    IsAttr['ndim', IsEqual[2]] &
    IsAttr['dtype', IsAttr['type', IsSubclass[np.floating]]]
]

# Annotate @beartype-decorated callables with beartype validators.
@beartype
def polygon_area(polygon: Numpy2DFloatArray) -> float:
    '''
    Area of a two-dimensional polygon of floats defined as a set of
    counter-clockwise points, calculated via Green's theorem.

    *Don't ask.*
    '''

    # Calculate and return the desired area. Pretend we understand this.
    polygon_rolled = np.roll(polygon, -1, axis=0)
    return np.abs(0.5*np.sum(
        polygon[:,0]*polygon_rolled[:,1] -
        polygon_rolled[:,0]*polygon[:,1]))
Trendy String Matching

Let’s validate strings either at least 80 characters long or both quoted and suffixed by a period. Look, it doesn’t matter. Just do it already, @beartype!

# Import the requisite machinery.
from beartype import beartype
from beartype.vale import Is
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Validator matching only strings at least 80 characters in length.
IsLengthy = Is[lambda text: len(text) >= 80]

# Validator matching only strings suffixed by a period.
IsSentence = Is[lambda text: text and text[-1] == '.']

# Validator matching only single- or double-quoted strings.
def _is_quoted(text): return text.count('"') >= 2 or text.count("'") >= 2
IsQuoted = Is[_is_quoted]

# Combine multiple validators by just listing them sequentially.
@beartype
def desentence_lengthy_quoted_sentence(
    text: Annotated[str, IsLengthy, IsSentence, IsQuoted]]) -> str:
    '''
    Strip the suffixing period from a lengthy quoted sentence... 'cause.
    '''

    return text[:-1]  # this is horrible

# Combine multiple validators by just "&"-ing them sequentially. Yes, this
# is exactly identical to the prior function. We do this because we can.
@beartype
def desentence_lengthy_quoted_sentence_part_deux(
    text: Annotated[str, IsLengthy & IsSentence & IsQuoted]]) -> str:
    '''
    Strip the suffixing period from a lengthy quoted sentence... again.
    '''

    return text[:-1]  # this is still horrible

# Combine multiple validators with as many "&", "|", and "~" operators as
# you can possibly stuff into a module that your coworkers can stomach.
# (They will thank you later. Possibly much later.)
@beartype
def strip_lengthy_or_quoted_sentence(
    text: Annotated[str, IsLengthy | (IsSentence & ~IsQuoted)]]) -> str:
    '''
    Strip the suffixing character from a string that is lengthy and/or a
    quoted sentence, because your web app deserves only the best data.
    '''

    return text[:-1]  # this is frankly outrageous

Validator Alternatives

If the unbridled power of beartype validators leaves you variously queasy, uneasy, and suspicious of our core worldview, beartype also supports third-party type hints like typed NumPy arrays.

Whereas beartype validators are verbose, expressive, and general-purpose, the following hints are terse, inexpressive, and domain-specific. Since beartype internally converts these hints to their equivalent validators, similar caveats apply. Notably, these hints require:

NumPy Type Hints

Beartype conditionally supports NumPy type hints (i.e., annotations created by subscripting (indexing) various attributes of the “numpy.typing” subpackage) when these optional runtime dependencies are all satisfied:

Beartype internally converts NumPy type hints into equivalent beartype validators at decoration time. NumPy type hints currently only validate dtypes, a common but limited use case. Beartype validators validate any arbitrary combinations of array constraints – including dtypes, shapes, contents, and… well, anything. Which is alot. NumPy type hints are thus just syntactic sugar for beartype validators – albeit quasi-portable syntactic sugar also supported by mypy.

Wherever you can, prefer NumPy type hints for portability. Everywhere else, default to beartype validators for generality. Combine them for the best of all possible worlds:

# Import the requisite machinery.
from beartype import beartype
from beartype.vale import IsAttr, IsEqual
from numpy import floating
from numpy.typing import NDArray
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Beartype validator + NumPy type hint matching all two-dimensional NumPy
# arrays of floating-point numbers of any arbitrary precision.
NumpyFloat64Array = Annotated[NDArray[floating], IsAttr['ndim', IsEqual[2]]]

Rejoice! A one-liner solves everything yet again.

Typed NumPy Arrays

Type NumPy arrays by subscripting (indexing) the numpy.typing.NDArray class with one of three possible types of objects:

  • An array dtype (i.e., instance of the numpy.dtype class).

  • A scalar dtype (i.e., concrete subclass of the numpy.generic abstract base class (ABC)).

  • A scalar dtype ABC (i.e., abstract subclass of the numpy.generic ABC).

Beartype generates fundamentally different type-checking code for these types, complying with both mypy semantics (which behaves similarly) and our userbase (which demands this behaviour). May there be hope for our future…

class numpy.typing.NDArray[numpy.dtype]

NumPy array typed by array dtype. A PEP-noncompliant type hint enforcing object equality against any array dtype (i.e., numpy.dtype instance), created by subscripting (indexing) the numpy.typing.NDArray class with that array dtype.

Prefer this variant when validating the exact data type of an array:

# Import the requisite machinery.
from beartype import beartype
from numpy import dtype
from numpy.typing import NDArray

# NumPy type hint matching all NumPy arrays of 32-bit big-endian integers,
# semantically equivalent to this beartype validator:
#     NumpyInt32BigEndianArray = Annotated[
#         np.ndarray, IsAttr['dtype', IsEqual[dtype('>i4')]]]
NumpyInt32BigEndianArray = NDArray[dtype('>i4')]

class numpy.typing.NDArray[numpy.dtype.type]

NumPy array typed by scalar dtype. A PEP-noncompliant type hint enforcing object equality against any scalar dtype (i.e., concrete subclass of the numpy.generic ABC), created by subscripting (indexing) the numpy.typing.NDArray class with that scalar dtype.

Prefer this variant when validating the exact scalar precision of an array:

# Import the requisite machinery.
from beartype import beartype
from numpy import float64
from numpy.typing import NDArray

# NumPy type hint matching all NumPy arrays of 64-bit floats, semantically
# equivalent to this beartype validator:
#     NumpyFloat64Array = Annotated[
#         np.ndarray, IsAttr['dtype', IsAttr['type', IsEqual[float64]]]]
NumpyFloat64Array = NDArray[float64]

Common scalar dtypes include:

  • Fixed-precision integer dtypes (e.g., numpy.int32, numpy.int64).

  • Fixed-precision floating-point dtypes (e.g., numpy.float32, numpy.float64).

class numpy.typing.NDArray[type[numpy.dtype.type]]

NumPy array typed by scalar dtype ABC. A PEP-noncompliant type hint enforcing type inheritance against any scalar dtype ABC (i.e., abstract subclass of the numpy.generic ABC), created by subscripting (indexing) the numpy.typing.NDArray class with that ABC.

Prefer this variant when validating only the kind of scalars (without reference to exact precision) in an array:

# Import the requisite machinery.
from beartype import beartype
from numpy import floating
from numpy.typing import NDArray

# NumPy type hint matching all NumPy arrays of floats of arbitrary
# precision, equivalent to this beartype validator:
#     NumpyFloatArray = Annotated[
#         np.ndarray, IsAttr['dtype', IsAttr['type', IsSubclass[floating]]]]
NumpyFloatArray = NDArray[floating]

Common scalar dtype ABCs include:

  • numpy.integer, the superclass of all fixed-precision integer dtypes.

  • numpy.floating, the superclass of all fixed-precision floating-point dtypes.

Warnings

Beartype occasionally emits non-fatal warnings at decoration time. While most are self-explanatory, more than a few assume prior knowledge of arcane type-hinting standards or require non-trivial resolutions warranting further discussion. Let’s ELI5 this for the good of the common… good.

PEP 585 Deprecations

Beartype may occasionally emit non-fatal PEP 585 deprecation warnings under Python ≥ 3.9 resembling:

/home/kumamon/beartype/_util/hint/pep/utilpeptest.py:377:
BeartypeDecorHintPep585DeprecationWarning: PEP 484 type hint
typing.List[int] deprecated by PEP 585 scheduled for removal in the first
Python version released after October 5th, 2025. To resolve this, import
this hint from "beartype.typing" rather than "typing". See this discussion
for further details and alternatives:
    https://github.com/beartype/beartype#pep-585-deprecations

This is that discussion topic. Let’s dissect this like a mantis shrimp repeatedly punching out giant kraken.

What Does This Mean?

The PEP 585 standard first introduced by Python 3.9.0 deprecated (obsoleted) most of the PEP 484 standard first introduced by Python 3.5.0 in the official typing module. All deprecated type hints are slated to “be removed from the typing module in the first Python version released 5 years after the release of Python 3.9.0.” Spoiler: Python 3.9.0 was released on October 5th, 2020. Altogether, this means that:

Most of the “typing” module will be removed in 2025 or 2026.

If your codebase currently imports from the typing module, most of those imports will break under an upcoming Python release. This is what beartype is shouting about. Bad Changes™ are coming to dismantle your working code.

Are We on the Worst Timeline?

Season Eight of Game of Thrones previously answered this question, but let’s try again. You have three options to avert the looming disaster that threatens to destroy everything you hold dear (in ascending order of justice):

  1. Import from beartype.typing instead. The easiest (and best) solution is to globally replace all imports from the standard typing module with equivalent imports from our beartype.typing module. So:

    # Just do this...
    from beartype import typing
    
    # ...instead of this.
    #import typing
    
    # Likewise, just do this...
    from beartype.typing import Dict, FrozenSet, List, Set, Tuple, Type
    
    # ...instead of this.
    #from typing import Dict, FrozenSet, List, Set, Tuple, Type

    The public beartype.typing API is a mypy-compliant replacement for the typing API offering improved forward compatibility with future Python releases. For example:

    • beartype.typing.Set is set under Python ≥ 3.9 for PEP 585 compliance.

    • beartype.typing.Set is typing.Set under Python < 3.9 for PEP 484 compliance.

  2. Drop Python < 3.9. The next easiest (but worst) solution is to brutally drop support for Python < 3.9 by globally replacing all deprecated PEP 484-compliant type hints with equivalent PEP 585-compliant type hints (e.g., typing.List[int] with list[int]). This is really only ideal for closed-source proprietary projects with a limited userbase. All other projects should prefer saner solutions outlined below.

  3. Hide warnings. The reprehensible (but understandable) middle-finger way is to just squelch all deprecation warnings with an ignore warning filter targeting the BeartypeDecorHintPep585DeprecationWarning category. On the one hand, this will still fail in 2025 or 2026 with fiery explosions and thus only constitutes a temporary workaround at best. On the other hand, this has the obvious advantage of preserving Python < 3.9 support with minimal to no refactoring costs. The two ways to do this have differing tradeoffs depending on who you want to suffer most – your developers or your userbase:

    # Do it globally for everyone, whether they want you to or not!
    # This is the "Make Users Suffer" option.
    from beartype.roar import BeartypeDecorHintPep585DeprecationWarning
    from warnings import filterwarnings
    filterwarnings("ignore", category=BeartypeDecorHintPep585DeprecationWarning)
    ...
    
    # Do it locally only for you! (Hope you like increasing your
    # indentation level in every single codebase module.)
    # This is the "Make Yourself Suffer" option.
    from beartype.roar import BeartypeDecorHintPep585DeprecationWarning
    from warnings import catch_warnings, filterwarnings
    with catch_warnings():
        filterwarnings("ignore", category=BeartypeDecorHintPep585DeprecationWarning)
        ...
  4. Type aliases. The hardest (but best) solution is to use type aliases to conditionally annotate callables with either PEP 484 or 585 type hints depending on the major version of the current Python interpreter. Since this is life, the hard way is also the best way – but also hard. Unlike the drop Python < 3.9 approach, this approach preserves backward compatibility with Python < 3.9. Unlike the hide warnings approach, this approach also preserves forward compatibility with Python ≥ 3.14159265. Type aliases means defining a new private {your_package}._typing submodule resembling:

    # In "{your_package}._typing":
    from sys import version_info
    
    if version_info >= (3, 9):
        List = list
        Tuple = tuple
        ...
    else:
        from typing import List, Tuple, ...

    Then globally refactor all deprecated PEP 484 imports from typing to {your_package}._typing instead:

    # Instead of this...
    from typing import List, Tuple
    
    # ...just do this.
    from {your_package}._typing import List, Tuple

    What could be simpler? …gagging noises faintly heard

Coming up! A shocking revelation that cheaters prosper.

Cheatsheet

Let’s type-check like greased lightning:

# ..................{              IMPORTS               }..................
# Import the core @beartype decorator.
from beartype import beartype

# Import PEP-agnostic type hints from "beartype.typing", a stand-in
# replacement for the standard "typing" module providing improved forward
# compatibility with future Python releases. For example:
# * "beartype.typing.Set is set" under Python ≥ 3.9 to satisfy PEP 585.
# * "beartype.typing.Set is typing.Set" under Python < 3.9 to satisfy PEP 484.
from beartype import typing

# Alternately, directly import PEP 484-compliant type hints. Note PEP 585
# deprecated many hints under Python ≥ 3.9, where @beartype now emits
# non-fatal deprecation warnings at decoration time. See also:
#     https://docs.python.org/3/library/typing.html
#import typing

# Alternately, directly import PEP 585-compliant type hints. Note this
# requires Python ≥ 3.9.
from collections import abc

# Import backported PEP-compliant type hints from "typing_extensions",
# improving portability across Python versions (e.g., "typing.Literal" needs
# Python ≥ 3.9 but "typing_extensions.Literal" only needs Python ≥ 3.6).
import typing_extensions

# Import beartype-specific types to annotate callables with.
from beartype.cave import NoneType, NoneTypeOr, RegexTypes, ScalarTypes

# Import official abstract base classes (ABCs), too.
from numbers import Integral, Real

# Import user-defined classes, too.
from my_package.my_module import MyClass

# ..................{              TYPEVARS              }..................
# User-defined PEP 484-compliant type variable. Note @beartype currently
# ignores type variables, but that @beartype 1.0.0 is expected to fully
# support type variables. See also: https://github.com/beartype/beartype/issues/7
T = typing.TypeVar('T')

# ..................{              PROTOCOLS             }..................
# User-defined PEP 544-compliant protocol referenced below in type hints.
# Note this requires Python ≥ 3.8 and that protocols *MUST* be explicitly
# decorated by the @runtime_checkable decorator to be usable with @beartype.
@typing.runtime_checkable   # <---- mandatory boilerplate line. it is sad.
class MyProtocol(typing.Protocol):
    def my_method(self) -> str:
        return (
            'Objects satisfy this protocol only if their classes '
            'define a method with the same signature as this method.'
        )

# ..................{              FUNCTIONS             }..................
# Decorate functions with @beartype and...
@beartype
def my_function(
    # 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 604-compliant type unions. Unlike PEP 484-compliant
    # unions, this excludes PEP-compliant type hints.
    param_must_satisfy_pep604_union: dict | tuple | None,

    # Annotate PEP 593-compliant metatypes, indexed by a PEP-compliant type
    # hint followed by zero or more arbitrary objects.
    param_must_satisfy_pep593: typing.Annotated[
        typing.Set[int], range(5), True],

    # Annotate PEP 586-compliant literals, indexed by either a boolean, byte
    # string, integer, string, "enum.Enum" member, or "None".
    param_must_satisfy_pep586: typing.Literal[
        'This parameter must equal this string.'],

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

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

    # Annotate PEP 544-compliant protocols, either unindexed or indexed by
    # one or more type variables.
    param_must_satisfy_pep544: MyProtocol[T],

    # Annotate PEP 484-compliant non-standard container types defined by the
    # "typing" module, optionally indexed and only usable as type hints.
    # Note that these types have all been deprecated by PEP 585 under Python
    # ≥ 3.9. See also: https://docs.python.org/3/library/typing.html
    param_must_satisfy_pep484_typing: typing.List[int],

    # Annotate PEP 484-compliant type hint unions. Unlike PEP 604-compliant
    # type unions, this includes PEP-compliant type hints.
    param_must_satisfy_pep484_union: typing.Union[
        dict, T, tuple[MyClass, ...]],

    # 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 and
    # that beartype-specific absolute forward references are also supported.
    param_must_satisfy_pep484_relative_forward_ref: 'MyOtherClass',

    # Annotate PEP-compliant types indexed by relative forward references.
    # Forward references are supported everywhere standard types are.
    param_must_satisfy_pep484_indexed_relative_forward_ref: (
        typing.Union['MyPep484Generic', set['MyPep585Generic']]),

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

    # 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: ScalarTypes,

    # Annotate beartype-specific unions concatenated together.
    param_must_satisfy_beartype_union_concatenated: (
        abc.Iterator,) + 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: (
        abc.Iterable, 'my_package.my_module.MyOtherClass', NoneType),

    # Annotate PEP 484-compliant optional types. Note that parameters
    # annotated by this type typically default to the "None" singleton.
    param_must_satisfy_pep484_optional: typing.Optional[float] = None,

    # Annotate PEP 484-compliant optional unions of types.
    param_must_satisfy_pep484_optional_union: (
        typing.Optional[typing.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: ScalarTypes + (Real, 'my_package.my_module.MyScalarType'),

    # Annotate keyword-only arguments as above, too.
    param_must_be_passed_by_keyword_only: abc.Sequence[
        typing.Union[bool, list[str]]],

# Annotate return types as above, too.
) -> Union[Integral, 'MyPep585Generic', bool]:
    return 0xDEADBEEF

# Decorate coroutines as above but returning a coroutine type.
@beartype
async def my_coroutine() -> abc.Coroutine[None, None, int]:
    from async import sleep
    await sleep(0)
    return 0xDEFECA7E

# ..................{              GENERATORS            }..................
# Decorate synchronous generators as above but returning a synchronous
# generator type.
@beartype
def my_sync_generator() -> abc.Generator[int, None, None]:
    yield from range(0xBEEFBABE, 0xCAFEBABE)

# Decorate asynchronous generators as above but returning an asynchronous
# generator type.
@beartype
async def my_async_generator() -> abc.AsyncGenerator[int, None]:
    from async import sleep
    await sleep(0)
    yield 0x8BADF00D

# ..................{              CLASSES               }..................
# User-defined class referenced in forward references above.
class MyOtherClass:
    # Decorate instance methods as above without annotating "self".
    @beartype
    def __init__(self, scalar: ScalarTypes) -> None:
        self._scalar = scalar

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

    # Decorate static methods as above.
    @staticmethod
    @beartype
    def my_staticmethod(callable: abc.Callable[[str], T], text: str) -> T:
        return callable(text)

    # Decorate property getter methods as above.
    @property
    @beartype
    def my_gettermethod(self) -> abc.Iterator[int]:
        return range(0x0B00B135 + int(self._scalar), 0xB16B00B5)

    # Decorate property setter methods as above.
    @my_gettermethod.setter
    @beartype
    def my_settermethod(self, bad: Integral = 0xBAAAAAAD) -> None:
        self._scalar = bad if bad else 0xBADDCAFE

    # Decorate methods accepting or returning instances of the class
    # currently being declared with relative forward references.
    @beartype
    def my_selfreferential_method(self) -> list['MyOtherClass']:
        return [self] * 42

# ..................{              CLASSES ~ dataclass   }..................
# Import the requisite machinery. Note this requires Python ≥ 3.8.
from dataclasses import dataclass, InitVar

# User-defined dataclass. @beartype currently only type-checks the implicit
# __init__() method generated by @dataclass. Fields are type-checked *ONLY*
# at initialization time and thus *NOT* type-checked when reassigned to.
@beartype
@dataclass
class MyDataclass:
    # Annotate fields with PEP-compliant type hints.
    field_must_satisfy_builtin_type: InitVar[str]
    field_must_satisfy_pep604_union: str | None = None

    # Decorate explicit methods with @beartype as above.
    @beartype
    def __post_init__(self, field_must_satisfy_builtin_type: str) -> None:
        if self.field_must_satisfy_pep604_union is None:
            self.field_must_satisfy_pep604_union = (
                field_must_satisfy_builtin_type)

# ..................{              GENERICS              }..................
# User-defined PEP 585-compliant generic referenced above in type hints.
# Note this requires Python ≥ 3.9.
class MyPep585Generic(tuple[int, float]):
    # Decorate static class methods as above without annotating "cls".
    @beartype
    def __new__(cls, integer: int, real: float) -> tuple[int, float]:
        return tuple.__new__(cls, (integer, real))

# User-defined PEP 484-compliant generic referenced above in type hints.
class MyPep484Generic(typing.Tuple[str, ...]):
    # Decorate static class methods as above without annotating "cls".
    @beartype
    def __new__(cls, *args: str) -> typing.Tuple[str, ...]:
        return tuple.__new__(cls, args)

# ..................{             CONFIGURATION          }..................
# Import the configuration API.
from beartype import BeartypeConf, BeartypeStrategy

# Configure type-checking by passing @beartype an optional configuration.
@beartype(conf=BeartypeConf(
    # Optionally switch to a different type-checking strategy, including:
    # * "BeartypeStrategy.On", type-checking in O(n) linear time.
    #   (Currently unimplemented but roadmapped for a future release.)
    # * "BeartypeStrategy.Ologn", type-checking in O(logn) logarithmic time.
    #   (Currently unimplemented but roadmapped for a future release.)
    # * "BeartypeStrategy.O1", type-checking in O(1) constant time. This
    #   default strategy need *NOT* be explicitly enabled.
    # * "strategy=BeartypeStrategy.O0", disabling type-checking entirely.
    strategy=BeartypeStrategy.On,
    # Optionally enable developer-friendly debugging for this decoration.
    is_debug=True,
))
def my_configured_function(
    # Parameter type-checked in O(n) linear time. (Currently unimplemented.)
    param_checked_in_On_time: list[int],
# Return type-checked in O(n) linear time, too. (Currently unimplemented.)
) -> set[str]:
    return set(str(item) for item in param_checked_in_On_time)

# ..................{             VALIDATORS             }..................
# Import PEP 593-compliant beartype-specific type hints validating arbitrary
# caller constraints. Note this requires beartype ≥ 0.7.0 and either:
# * Python ≥ 3.9.0.
# * typing_extensions ≥ 3.9.0.0.
from beartype.vale import Is, IsAttr, IsEqual
from typing import Annotated   # <--------------- if Python ≥ 3.9.0
#from typing_extensions import Annotated   # <--- if Python < 3.9.0

# Import third-party packages to validate.
import numpy as np

# Validator matching only two-dimensional NumPy arrays of 64-bit floats,
# specified with a single caller-defined lambda function.
NumpyArray2DFloat = Annotated[np.ndarray, Is[
    lambda arr: arr.ndim == 2 and arr.dtype == np.dtype(np.float64)]]

# Validator matching only one-dimensional NumPy arrays of 64-bit floats,
# specified with two declarative expressions. Although verbose, this
# approach generates optimal reusable code that avoids function calls.
IsNumpyArray1D = IsAttr['ndim', IsEqual[1]]
IsNumpyArrayFloat = IsAttr['dtype', IsEqual[np.dtype(np.float64)]]
NumpyArray1DFloat = Annotated[np.ndarray, IsNumpyArray1D, IsNumpyArrayFloat]

# Validator matching only empty NumPy arrays, equivalent to but faster than:
#     NumpyArrayEmpty = Annotated[np.ndarray, Is[lambda arr: arr.size != 0]]
IsNumpyArrayEmpty = IsAttr['size', IsEqual[0]]
NumpyArrayEmpty = Annotated[np.ndarray, IsNumpyArrayEmpty]

# Validator composed with standard operators from the above validators,
# permissively matching all of the following:
# * Empty NumPy arrays of any dtype *except* 64-bit floats.
* * Non-empty one- and two-dimensional NumPy arrays of 64-bit floats.
NumpyArrayEmptyNonFloatOrNonEmptyFloat1Or2D = Annotated[np.ndarray,
    # "&" creates a new validator matching when both operands match, while
    # "|" creates a new validator matching when one or both operands match;
    # "~" creates a new validator matching when its operand does not match.
    # Group operands to enforce semantic intent and avoid precedence woes.
    (IsNumpyArrayEmpty & ~IsNumpyArrayFloat) | (
        ~IsNumpyArrayEmpty & IsNumpyArrayFloat (
            IsNumpyArray1D | IsAttr['ndim', IsEqual[2]]
        )
    )
]

# Decorate functions accepting validators like usual and...
@beartype
def my_validated_function(
    # Annotate validators just like standard type hints.
    param_must_satisfy_validator: NumpyArrayEmptyOrNonemptyFloat1Or2D,
# Combine validators with standard type hints, too.
) -> list[NumpyArrayEmptyNonFloatOrNonEmptyFloat1Or2D]:
    return (
        [param_must_satisfy_validator] * 0xFACEFEED
        if bool(param_must_satisfy_validator) else
        [np.array([i], np.dtype=np.float64) for i in range(0xFEEDFACE)]
    )

# ..................{             NUMPY                  }..................
# Import NumPy-specific type hints validating NumPy array constraints. Note:
# * These hints currently only validate array dtypes. To validate additional
#   constraints like array shapes, prefer validators instead. See above.
# * This requires NumPy ≥ 1.21.0, beartype ≥ 0.8.0, and either:
#   * Python ≥ 3.9.0.
#   * typing_extensions ≥ 3.9.0.0.
from numpy.typing import NDArray

# NumPy type hint matching all NumPy arrays of 64-bit floats. Internally,
# beartype reduces this to the equivalent validator:
#     NumpyArrayFloat = Annotated[
#         np.ndarray, IsAttr['dtype', IsEqual[np.dtype(np.float64)]]]
NumpyArrayFloat = NDArray[np.float64]

# Decorate functions accepting NumPy type hints like usual and...
@beartype
def my_numerical_function(
    # Annotate NumPy type hints just like standard type hints.
    param_must_satisfy_numpy: NumpyArrayFloat,
# Combine NumPy type hints with standard type hints, too.
) -> tuple[NumpyArrayFloat, int]:
    return (param_must_satisfy_numpy, len(param_must_satisfy_numpy))

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

0.9.0current

0.9.0current

functions

0.1.0current

0.1.0current

generators (asynchronous)

0.9.0current

0.9.0current

generators (synchronous)

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

0.10.0current

0.10.0current

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

beartype.abby

is_bearable

0.10.0current

0.10.0current

die_if_unbearable

0.10.0current

0.10.0current

beartype.typing

all

0.10.0current

0.10.0current

beartype.vale

Is

0.7.0current

0.7.0current

IsAttr

0.7.0current

0.7.0current

IsEqual

0.7.0current

0.7.0current

IsInstance

0.10.0current

0.10.0current

IsSubclass

0.9.0current

0.9.0current

builtins

None

0.6.0current

0.6.0current

NotImplemented

0.7.1current

0.7.1current

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

0.9.0current

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

0.9.0current

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

dataclasses

dataclasses.InitVar

0.10.0current

0.10.0current

dataclasses

dataclasses.dataclass

0.10.0current

none

numpy.typing

numpy.typing.NDArray

0.8.0current

0.8.0current

re

re.Match

0.5.0current

none

re.Pattern

0.5.0current

none

sphinx

sphinx.ext.autodoc

0.9.0current

0.9.0current

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

0.10.0current

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.Concatenate

none

none

typing.Container

0.2.0current

none

typing.ContextManager

0.4.0current

none

typing.Coroutine

0.2.0current

0.9.0current

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

0.10.0current

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

0.7.0current

0.7.0current

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.ParamSpec

none

none

typing.ParamSpecArgs

none

none

typing.ParamSpecKwargs

none

none

typing.Pattern

0.4.0current

none

typing.Protocol

0.4.0current

0.4.0current

typing.Reversible

0.2.0current

none

typing.Self

none

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

0.10.0current

typing.Tuple

0.2.0current

0.4.0current

typing.Type

0.2.0current

0.9.0current

typing.TypeGuard

none

none

typing.TypedDict

0.9.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

typing_extensions

all attributes

0.8.0current

0.8.0current

PEP

484

0.2.0current

none

544

0.4.0current

0.4.0current

557

0.10.0current

none

560

0.4.0current

0.4.0current

561

0.6.0current

0.6.0current

563

0.1.1current

0.7.0current

570

0.10.0current

0.10.0current

572

0.3.0current

0.4.0current

585

0.5.0current

0.5.0current

586

0.7.0current

0.7.0current

589

0.9.0current

none

591

none

none

593

0.4.0current

0.4.0current

604

0.10.0current

0.10.0current

612

none

none

647

none

none

673

none

none

3102

0.1.0current

0.1.0current

3119

0.7.0current

0.9.0current

3141

0.1.0current

0.1.0current

packages

PyPI

0.1.0current

Anaconda

0.1.0current

Gentoo Linux

0.2.0current

macOS Homebrew

0.5.1current

macOS MacPorts

0.5.1current

Python

3.5

0.1.00.3.0

3.6

0.1.00.10.3

3.7

0.1.0current

3.8

0.1.0current

3.9

0.3.2current

3.10

0.7.0current

3.11

0.11.0current

Timings

Let’s profile beartype against other runtime type-checkers with a battery of surely fair, impartial, and unbiased use cases:

$ bin/profile.bash

beartype profiler [version]: 0.0.2

python    [basename]: python3.9
python    [version]: Python 3.9.0
beartype  [version]: 0.6.0
typeguard [version]: 2.9.1

===================================== str =====================================
profiling regime:
   number of meta-loops:      3
   number of loops:           100
   number of calls each loop: 100
decoration         [none     ]: 100 loops, best of 3: 359 nsec per loop
decoration         [beartype ]: 100 loops, best of 3: 389 usec per loop
decoration         [typeguard]: 100 loops, best of 3: 13.5 usec per loop
decoration + calls [none     ]: 100 loops, best of 3: 14.8 usec per loop
decoration + calls [beartype ]: 100 loops, best of 3: 514 usec per loop
decoration + calls [typeguard]: 100 loops, best of 3: 6.34 msec per loop

=============================== Union[int, str] ===============================
profiling regime:
   number of meta-loops:      3
   number of loops:           100
   number of calls each loop: 100
decoration         [none     ]: 100 loops, best of 3: 1.83 usec per loop
decoration         [beartype ]: 100 loops, best of 3: 433 usec per loop
decoration         [typeguard]: 100 loops, best of 3: 15.6 usec per loop
decoration + calls [none     ]: 100 loops, best of 3: 17.7 usec per loop
decoration + calls [beartype ]: 100 loops, best of 3: 572 usec per loop
decoration + calls [typeguard]: 100 loops, best of 3: 10 msec per loop

=========================== List[int] of 1000 items ===========================
profiling regime:
   number of meta-loops:      1
   number of loops:           1
   number of calls each loop: 7485
decoration         [none     ]: 1 loop, best of 1: 10.1 usec per loop
decoration         [beartype ]: 1 loop, best of 1: 1.3 msec per loop
decoration         [typeguard]: 1 loop, best of 1: 41.1 usec per loop
decoration + calls [none     ]: 1 loop, best of 1: 1.24 msec per loop
decoration + calls [beartype ]: 1 loop, best of 1: 18.3 msec per loop
decoration + calls [typeguard]: 1 loop, best of 1: 104 sec per loop

============ List[Sequence[MutableSequence[int]]] of 10 items each ============
profiling regime:
   number of meta-loops:      1
   number of loops:           1
   number of calls each loop: 7485
decoration         [none     ]: 1 loop, best of 1: 11.8 usec per loop
decoration         [beartype ]: 1 loop, best of 1: 1.77 msec per loop
decoration         [typeguard]: 1 loop, best of 1: 48.9 usec per loop
decoration + calls [none     ]: 1 loop, best of 1: 1.19 msec per loop
decoration + calls [beartype ]: 1 loop, best of 1: 81.2 msec per loop
decoration + calls [typeguard]: 1 loop, best of 1: 17.3 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.

How Much Does All This Cost?

Beartype dynamically generates functions wrapping decorated callables with constant-time runtime type-checking. This separation of concerns means that beartype exhibits different cost profiles at decoration and call time. Whereas standard runtime type-checking decorators are fast at decoration time and slow at call time, beartype is the exact opposite.

At call time, wrapper functions generated by the @beartype decorator are guaranteed to unconditionally run in O(1) non-amortized worst-case time with negligible constant factors regardless of type hint complexity or nesting. This is not an amortized average-case analysis. Wrapper functions really are O(1) time in the best, average, and worst cases.

At decoration time, performance is slightly worse. Internally, beartype non-recursively iterates over type hints at decoration time with a micro-optimized breadth-first search (BFS). Since this BFS is memoized, its cost is paid exactly once per type hint per process; subsequent references to the same hint over different parameters and returns of different callables in the same process reuse the results of the previously memoized BFS for that hint. The @beartype decorator itself thus runs in:

  • O(1) amortized average-case time.

  • O(k) non-amortized worst-case time for k the number of child type hints nested in a parent type hint and including that parent.

Since we generally expect a callable to be decorated only once but called multiple times per process, we might expect the cost of decoration to be ignorable in the aggregate. Interestingly, this is not the case. Although only paid once and obviated through memoization, decoration time is sufficiently expensive and call time sufficiently inexpensive that beartype spends most of its wall-clock merely decorating callables. The actual function wrappers dynamically generated by @beartype consume comparatively little wall-clock, even when repeatedly called many times.

That’s Some Catch, That Catch-22

Beartype’s greatest strength is that it checks types in constant time.

Beartype’s greatest weakness is that it checks types in constant time.

Only so many type-checks can be stuffed into a constant slice of time with negligible constant factors. Let’s detail exactly what (and why) beartype stuffs into its well-bounded slice of the CPU pie.

Standard runtime type checkers naïvely brute-force the problem by type-checking all child objects transitively reachable from parent objects passed to and returned from callables in O(n) linear time for n such objects. This approach avoids false positives (i.e., raising exceptions for valid objects) and false negatives (i.e., failing to raise exceptions for invalid objects), which is good. But this approach also duplicates work when those objects remain unchanged over multiple calls to those callables, which is bad.

Beartype circumvents that badness by generating code at decoration time performing a one-way random tree walk over the expected nested structure of those objects at call time. For each expected nesting level of each container passed to or returned from each callable decorated by @beartype starting at that container and ending either when a check fails or all checks succeed, that callable performs these checks (in order):

  1. A shallow type-check that the current possibly nested container is an instance of the type given by the current possibly nested type hint.

  2. A deep type-check that an item randomly selected from that container itself satisfies the first check.

For example, given a parameter’s type hint list[tuple[Sequence[str]]], beartype generates code at decoration time performing these checks at call time (in order):

  1. A check that the object passed as this parameter is a list.

  2. A check that an item randomly selected from this list is a tuple.

  3. A check that an item randomly selected from this tuple is a sequence.

  4. A check that an item randomly selected from this sequence is a string.

Beartype thus performs one check for each possibly nested type hint for each annotated parameter or return object for each call to each decorated callable. This deep randomness gives us soft statistical expectations as to the number of calls needed to check everything. Specifically, it can be shown that beartype type-checks on average all child objects transitively reachable from parent objects passed to and returned from callables in O(n log n) calls to those callables for n such objects. Praise RNGesus!

Beartype avoids false positives and rarely duplicates work when those objects remain unchanged over multiple calls to those callables, which is good. Sadly, 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 homogenous (i.e., contain only items of the same type) rather than heterogenous (i.e., contain items of differing types). Examples of homogenous containers include (byte-)strings, ranges, streams, memory views, method resolution orders (MROs), generic alias parameters, lists returned by the dir builtin, iterables generated by the os.walk function, standard NumPy arrays, Pandas DataFrame columns, PyTorch tensors, NetworkX graphs, and really all scientific containers ever.

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 by these typing types:

Beartype also fully supports these third-party typing-like types:

Beartype also fully supports callables decorated by these typing decorators:

Lastly, beartype also 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 by 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:

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 the typing.Union type hint 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 collections.abc import Callable, Mapping
from numbers import Integral
from typing import Any, Union

@beartype
def toomai_of_the_elephants(memory: Union[Integral, Mapping[Any, Any]]) -> (
    Union[Integral, Callable[(Any,), Any]]):
    return memory if isinstance(memory, Integral) else lambda key: memory[key]

For genericity, the toomai_of_the_elephants function both accepts and returns any generic integer (via the standard numbers.Integral 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(len(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(
...     'Shiv, who poured the harvest and made the winds to blow,')
BeartypeCallHintPepParamException: @beartyped toomai_of_the_elephants()
parameter memory='Shiv, who poured the harvest and made the winds to blow,'
violates type hint typing.Union[numbers.Integral, collections.abc.Mapping],
as 'Shiv, who poured the harvest and made the winds to blow,' not <protocol
ABC "collections.abc.Mapping"> or <protocol "numbers.Integral">.

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 typing.Optional type hint indexed by those 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
from typing import Optional

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

For efficiency, the typing.Optional type hint creates, caches, and returns new tuples of types appending NoneType to the original types it’s indexed with. Since efficiency is good, typing.Optional is also good.

Let’s call that function with good types:

>>> 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.'
>>> 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 typing.Union type hints explicitly indexed by 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 typing.Optional:

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

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

Since typing.Optional internally reduces to typing.Union, these two approaches are semantically equivalent. The former is simply syntactic sugar simplifying the latter.

Whereas typing.Union accepts an arbitrary number of child type hints, however, typing.Optional accepts only a single child type hint. This can be circumvented by either indexing typing.Optional by typing.Union or indexing typing.Union by NoneType. Let’s exhibit the former approach by declaring 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, NoneType
from typing import Optional, Union

@beartype
def sang_them_up_the_beach(
    woe: Optional[Union[EnumType, EnumMemberType]] = None) -> (
    Optional[Union[EnumType, EnumMemberType]]):
    return woe if isinstance(woe, (EnumMemberType, NoneType)) 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 typing.Optional type hint, resplendent in its highly over-optimized cache utilization.

Implementation

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, [2] 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 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.

  • 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 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 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 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 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.

Workflow

Let’s take this from the top.

  1. Create a GitHub user account.

  2. Login to GitHub with that account.

  3. Click the “Fork” button in the upper right-hand corner of the “beartype/beartype” repository page.

  4. Click the “Code” button in the upper right-hand corner of your fork page that appears.

  5. Copy the URL that appears.

  6. Open a terminal.

  7. Change to the desired parent directory of your local fork.

  8. Clone your fork, replacing {URL} with the previously copied URL.

    git clone {URL}
  9. Add a new remote referring to this upstream repository.

    git remote add upstream https://github.com/beartype/beartype.git
  10. Uninstall all previously installed versions of beartype. For example, if you previously installed beartype with pip, manually uninstall beartype with pip.

    pip uninstall beartype
  11. Install beartype with pip in editable mode. This synchronizes changes made to your fork against the beartype package imported in Python. Note the [dev] extra installs developer-specific mandatory dependencies required at test or documentation time.

    pip3 install -e .[dev]
  12. Create a new branch to isolate changes to, replacing {branch_name} with the desired name.

    git checkout -b {branch_name}
  13. Make changes to this branch in your favourite Integrated Development Environment (IDE). Of course, this means Vim.

  14. Test these changes. Note this command assumes you have installed all major versions of both CPython and PyPy supported by the next stable release of beartype you are hacking on. If this is not the case, install these versions with pyenv. This is vital, as type hinting support varies significantly between major versions of different Python interpreters.

    ./tox

    The resulting output should ideally be suffixed by a synopsis resembling:

    ________________________________ summary _______________________________
    py36: commands succeeded
    py37: commands succeeded
    py38: commands succeeded
    py39: commands succeeded
    pypy36: commands succeeded
    pypy37: commands succeeded
    congratulations :)
  15. Stage these changes.

    git add -a
  16. Commit these changes.

    git commit
  17. Push these changes to your remote fork.

    git push
  18. Click the “Create pull request” button in the upper right-hand corner of your fork page.

  19. Afterward, routinely pull upstream changes to avoid desynchronization with the “beartype/beartype” repository.

    git checkout main && git pull upstream main

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._error.errormain” 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. A Paul Allen Discovery Center award from the Paul G. Allen Frontiers Group under the administrative purview of the Paul Allen Discovery Center at Tufts University over the period 2015—2018 preceding the untimely death of Microsoft co-founder Paul Allen, during which beartype was maintained as the private @type_check decorator in the Bioelectric Tissue Simulation Engine (BETSE). Phew!

Authors

beartype is developed with the grateful assistance of a volunteer community of enthusiasts, including (in chronological order of issue or pull request):

  1. Cecil Curry (@leycec). Hi! It’s me. From beartype’s early gestation as a nondescript @type_check decorator in the Bioelectric Tissue Simulation Engine (BETSE) to its general-audience release as a public package supported across multiple Python and platform-specific package managers, I shepherd the fastest, hardest, and deepest runtime type-checking solution in any dynamically-typed language towards a well-typed future of PEP-compliance and boundless quality assurance. Cue epic taiko drumming.

  2. Felix Hildén (@felix-hilden), the Finnish computer vision expert world-renowned for his effulgent fun-loving disposition and:

  3. @harens, the boisterous London developer acclaimed for his defense of British animals that quack pridefully as they peck you in city parks as well as:

  4. @Heliotrop3, the perennial flowering plant genus from Peru whose “primal drive for ruthless efficiency makes overcoming these opportunities for growth [and incoming world conquest] inevitable” as well as:

  5. @posita, the superpositive code luminary of superpositional genius status singularly responsible for:

See Also

External beartype resources include:

Related type-checking resources include:

Runtime Type Checkers

Runtime type checkers (i.e., third-party Python packages dynamically validating callables annotated by type hints at runtime, typically via decorators, function calls, and import hooks) include:

package

active

PEP-compliant

time multiplier [3]

beartype

yes

yes

1 ✕ beartype

enforce

no

yes

unknown

enforce_typing

no

yes

unknown

pydantic

yes

no

unknown

pytypes

no

yes

unknown

typeen

no

no

unknown

typical

yes

yes

unknown

typeguard

no

yes

20 ✕ beartype

Like static type checkers, runtime type checkers always require callables to be annotated by type hints. Unlike static type checkers, runtime type checkers do not necessarily comply with community standards; although some do require callers to annotate callables with strictly PEP-compliant type hints, others permit or even require callers to annotate callables with PEP-noncompliant type hints. Runtime type checkers that do so violate:

Runtime Data Validators

Runtime data validators (i.e., third-party Python packages dynamically validating callables decorated by caller-defined contracts, constraints, and validation routines at runtime) include:

Unlike both runtime type checkers and static type checkers, most runtime data validators do not require callables to be annotated by type hints. Like some runtime type checkers, most runtime data validators do not comply with community standards but instead require callers to either:

  • Decorate callables with package-specific decorators.

  • Annotate callables with package-specific and thus PEP-noncompliant type hints.

Static Type Checkers

Static type checkers (i.e., third-party tooling validating Python callable and/or variable types across an application stack at static analysis time 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.10.4.tar.gz (795.2 kB view hashes)

Uploaded Source

Built Distribution

beartype-0.10.4-py3-none-any.whl (578.1 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