beartype - Man Page
Name
beartype — beartype 0.18.5
TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
beartype —[ the bare-metal type-checker ]—
beartype test coverage status beartype continuous integration (CI) status beartype Read The Docs (RTD) status
Beartype is an open-source pure-Python PEP-compliant near-real-time hybrid runtime-static third-generation type-checker emphasizing efficiency, usability, unsubstantiated jargon we just made up, and thrilling puns.
Beartype enforces type hints across your entire app in two lines of runtime code with no runtime overhead. If seeing is believing, prepare to do both those things.
# Install beartype. $ pip3 install beartype # Edit the "{your_package}.__init__" submodule with your favourite IDE. $ vim {your_package}/__init__.py # <-- so, i see that you too vim
# At the very top of your "{your_package}.__init__" submodule: from beartype.claw import beartype_this_package # <-- boilerplate for victory beartype_this_package() # <-- yay! your team just won
Beartype now implicitly type-checks all annotated classes, callables, and variable assignments across all submodules of your package. Congrats. This day all bugs die.
But why stop at the burning tires in only your code? Your app depends on a sprawling ghetto of other packages, modules, and services. How riddled with infectious diseases is that code? You're about to find out.
# ....................{ BIG BEAR }.................... # Warn about type hint violations in *OTHER* packages outside your control; # only raise exceptions from violations in your package under your control. # Again, at the very top of your "{your_package}.__init__" submodule: from beartype import BeartypeConf # <-- this isn't your fault from beartype.claw import beartype_all, beartype_this_package # <-- you didn't sign up for this beartype_this_package() # <-- raise exceptions in your code beartype_all(conf=BeartypeConf(violation_type=UserWarning)) # <-- emit warnings from other code
Beartype now implicitly type-checks all annotated classes, callables, and variable assignments across all submodules of all packages. When your package violates type safety, beartype raises an exception. When any other package violates type safety, beartype just emits a warning. The triumphal fanfare you hear is probably your userbase cheering. This is how the QA was won.
Beartype also publishes a plethora of APIs for fine-grained control over type-checking. For those who are about to QA, beartype salutes you. Would you like to know more?
# So let's do this. $ python3
# ....................{ RAISE THE PAW }.................... # Manually enforce type hints across individual classes and callables. # Do this only if you want a(nother) repetitive stress injury. # Import the @beartype decorator. >>> from beartype import beartype # <-- eponymous import; it's eponymous # Annotate @beartype-decorated classes and callables with type hints. >>> @beartype # <-- you too will believe in magic ... 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 get_beartype_violation raise exception_cls( beartype.roar.BeartypeCallHintParamViolation: @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. # ....................{ MAKE IT SO }.................... # Squash bugs by refining type hints with @beartype validators. >>> from beartype.vale import Is # <---- validator factory >>> from typing import Annotated # <---------------- if Python ≥ 3.9.0 # >>> from typing_extensions import Annotated # <-- if Python < 3.9.0 # Validators are type hints constrained by lambda functions. >>> ListOfStrings = Annotated[ # <----- type hint matching non-empty list of strings ... list[str], # <----------------- type hint matching possibly empty list of strings ... Is[lambda lst: bool(lst)] # <-- lambda matching non-empty object ... ] # Annotate @beartype-decorated callables with validators. >>> @beartype ... def quote_wiggum_safer(lines: ListOfStrings) -> None: ... print('“{}”\n\t— Police Chief Wiggum'.format("\n ".join(lines))) # Call those callables with invalid parameters. >>> quote_wiggum_safer([]) beartype.roar.BeartypeCallHintParamViolation: @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)]. # ....................{ AT ANY TIME }.................... # Type-check anything against any type hint – anywhere at anytime. >>> from beartype.door import ( ... is_bearable, # <-------- like "isinstance(...)" ... die_if_unbearable, # <-- like "assert isinstance(...)" ... ) >>> is_bearable(['The', 'goggles', 'do', 'nothing.'], list[str]) True >>> die_if_unbearable([0xCAFEBEEF, 0x8BADF00D], ListOfStrings) beartype.roar.BeartypeDoorHintViolation: Object [3405692655, 2343432205] violates type hint typing.Annotated[list[str], Is[lambda lst: bool(lst)]], as list index 0 item 3405692655 not instance of str. # ....................{ GO TO PLAID }.................... # Type-check anything in around 1µs (one millionth of a second) – including # this list of one million 2-tuples of NumPy arrays. >>> from beartype.door import is_bearable >>> from numpy import array, ndarray >>> data = [(array(i), array(i)) for i in range(1000000)] >>> %time is_bearable(data, list[tuple[ndarray, ndarray]]) CPU times: user 31 µs, sys: 2 µs, total: 33 µs Wall time: 36.7 µs True
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.
Welcome to the Bearpedia – your one-stop Encyclopedia Beartanica for all things @beartype. It's "typing or bust!" as you...
TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Install
Install beartype with pip, because PyPI is the cheese shop and you too enjoy a fine Venezuelan beaver cheese while mashing disconsolately on your keyboard late on a rain-soaked Friday evening. Wherever expensive milk byproducts ferment, beartype will be there.
pip3 install beartype
Install beartype with Anaconda, because package managers named after venomous South American murder reptiles have finally inspired your team to embrace more mammal-friendly packages. Your horoscope also reads: "Avoid reckless ecotourism in places that rain alot."
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?
Bear with Us
Platform
- macOS
- Arch Linux
- Gentoo Linux
- Badge
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.
Arch Linux
Let's install beartype with pacman on Arch Linux – where beartype is now officially packaged in the Arch User Repository (AUR) itself:
git clone https://aur.archlinux.org/python-beartype.git cd python-beartype makepkg -si
Truly, Arch Linux has now seen the face of quality assurance. It looks like a grizzled bear with patchy fur, one twitchy eye, and a gimpy leg that spasmodically flails around.
Gentoo Linux
Let's install beartype with emerge on Gentoo Linux – where beartype is now officially packaged in the Portage tree itself:
emerge beartype
Source-based Linux distributions are the CPU-bound nuclear option. 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.readthedocs.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.readthedocs.io :alt: bear-ified
Raw HTML:
YummySoft is now <a href="https://beartype.readthedocs.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.
TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Too Long; Didn't Read (TL;DR)
Let's type-check like greased lightning! Thanks to cheatsheets like this, you no longer have to know how to use software to use software. \o/
# ..................{ IMPORTS }.................. # Import the core @beartype decorator. from beartype import beartype # Import type hint factories 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 # Or, directly import these factories from the standard "typing" module. Note # that PEP 585 deprecated many of these 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 # Or, directly import PEP 585 type hints. Note this requires Python ≥ 3.9. from collections import abc # Import backported type hint factories 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 }.................. # PEP 484 type variable. While @beartype only partially supports type # variables at the moment, @beartype 1.0.0.0.0.0.0.0 is expected to fully # support type variables. T = typing.TypeVar('T') # ..................{ 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 type hint unions. Note this requires Python ≥ 3.10. param_must_satisfy_pep604_union: dict | tuple | None, # Annotate PEP 484 type hint unions. All Python versions support this. param_must_satisfy_pep484_union: typing.Union[ dict, T, tuple[MyClass, ...]], # Annotate PEP 593 metatypes, indexed by a type hint followed by zero or # more arbitrary objects. See "VALIDATORS" below for real-world usage. param_must_satisfy_pep593: typing.Annotated[ typing.Set[int], range(5), True], # Annotate PEP 586 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 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 standard collection types, indexed too. param_must_satisfy_pep585_collection: abc.MutableSequence[str], # Annotate PEP 544 protocols, either unindexed or indexed by one or more # type variables. param_must_satisfy_pep544: typing.SupportsRound[T], # Annotate PEP 484 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 relative forward references dynamically resolved at # call time as unqualified classnames relative to the current 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 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 604 optional types. Note this requires Python ≥ 3.10. param_must_satisfy_pep604_optional: float | bytes = None, # Annotate PEP 484 optional types. All Python versions support this. param_must_satisfy_pep484_optional: typing.Optional[float, bytes] = 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 }.................. # Decorate classes with @beartype – which then automatically decorates all # methods and properties of those classes with @beartype. @beartype class MyOtherClass: # Annotate instance methods as above without annotating "self". def __init__(self, scalar: ScalarTypes) -> None: self._scalar = scalar # Annotate class methods as above without annotating "cls". @classmethod def my_classmethod(cls, regex: RegexTypes, wut: str) -> ( Callable[(), str]): import re return lambda: re.sub(regex, 'unbearable', str(cls._scalar) + wut) # Annotate static methods as above, too. @staticmethod def my_staticmethod(callable: abc.Callable[[str], T], text: str) -> T: return callable(text) # Annotate property getter methods as above, too. @property def my_gettermethod(self) -> abc.Iterator[int]: return range(0x0B00B135 + int(self._scalar), 0xB16B00B5) # Annotate property setter methods as above, too. @my_gettermethod.setter def my_settermethod(self, bad: Integral = 0xBAAAAAAD) -> None: self._scalar = bad if bad else 0xBADDCAFE # Annotate methods accepting or returning instances of the class # currently being declared with relative forward references. def my_selfreferential_method(self) -> list['MyOtherClass']: return [self] * 42 # ..................{ GENERICS }.................. # Decorate PEP 585 generics with @beartype. Note this requires Python ≥ 3.9. @beartype class MyPep585Generic(tuple[int, float]): def __new__(cls, integer: int, real: float) -> tuple[int, float]: return tuple.__new__(cls, (integer, real)) # Decorate PEP 484 generics with @beartype, too. @beartype class MyPep484Generic(typing.Tuple[str, ...]): def __new__(cls, *args: str) -> typing.Tuple[str, ...]: return tuple.__new__(cls, args) # ..................{ PROTOCOLS }.................. # PEP 544 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.' ) # ..................{ DATACLASSES }.................. # Import the requisite machinery. Note this requires Python ≥ 3.8. from dataclasses import dataclass, InitVar # Decorate dataclasses with @beartype, which then automatically decorates all # methods and properties of those dataclasses with @beartype – including the # __init__() constructors created by @dataclass. Fields are type-checked only # at instantiation time. Fields are *NOT* type-checked when reassigned. # # Decoration order is significant. List @beartype before @dataclass, please. @beartype @dataclass class MyDataclass(object): # Annotate fields with type hints. field_must_satisfy_builtin_type: InitVar[str] field_must_satisfy_pep604_union: str | None = None # Annotate methods as above. 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) # ..................{ NAMED TUPLES }.................. # Import the requisite machinery. from typing import NamedTuple # Decorate named tuples with @beartype. @beartype class MyNamedTuple(NamedTuple): # Annotate fields with type hints. field_must_satisfy_builtin_type: str # ..................{ CONFIGURATION }.................. # Import beartype's configuration API to configure runtime type-checking. from beartype import BeartypeConf, BeartypeStrategy # Dynamically create your own @beartype decorator, configured for your needs. bugbeartype = beartype(conf=BeartypeConf( # Optionally disable or enable output of colors (i.e., ANSI escape # sequences) in type-checking violations via this tri-state boolean: # * "None" conditionally enables colors when standard output is attached # to an interactive terminal. [DEFAULT] # * "True" unconditionally enables colors. # * "False" unconditionally disables colors. is_color=False, # <-- disable color entirely # Optionally enable developer-friendly debugging. is_debug=True, # Optionally enable PEP 484's implicit numeric tower by: # * Expanding all "float" type hints to "float | int". # * Expanding all "complex" type hints to "complex | float | int". is_pep484_tower=True, # Optionally switch to a different type-checking strategy: # * "BeartypeStrategy.O1" type-checks in O(1) constant time. [DEFAULT] # * "BeartypeStrategy.On" type-checks in O(n) linear time. # (Currently unimplemented but roadmapped for a future release.) # * "BeartypeStrategy.Ologn" type-checks in O(log n) logarithmic time. # (Currently unimplemented but roadmapped for a future release.) # * "strategy=BeartypeStrategy.O0" disables type-checking entirely. strategy=BeartypeStrategy.On, # <-- enable linear-time type-checking )) # Decorate with your decorator instead of the vanilla @beartype decorator. @bugbeartype def muh_configured_func(list_checked_in_On_time: list[float]) -> set[str]: return set(str(item) for item in list_checked_in_On_time) # ..................{ VALIDATORS }.................. # Import beartype's PEP 593 validator API to validate arbitrary constraints. # Note this requires 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 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))
Beartype: it just sorta works.
TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Explain Like I'm Five (Eli5)
Look for the bare necessities, the simple bare necessities. Forget about your worries and your strife. — The Jungle Book.
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.
Bear with Us
Comparison
- ...versus Static Type-checkers
- ...versus Runtime Type-checkers
Quickstart
Standard Hints
- Toy Example
- Industrial Example
Tutorial
- Builtin Types
- Arbitrary Types
- Unions of Types
- Optional Types
- Would You Like to Know More?
Comparison
Beartype is zero-cost. Beartype inflicts no harmful developer tradeoffs, instead stressing expense-free strategies at both:
- Installation time. Beartype has no install-time or runtime dependencies, supports standard Python package managers, and happily coexists with competing static type-checkers and other runtime type-checkers... which, of course, is irrelevant, as you would never dream of installing competing alternatives. Why would you, right? Am I right? </nervous_chuckle>
- Runtime. Thanks to aggressive memoization and dynamic code generation at decoration time, beartype guarantees O(1) non-amortized worst-case runtime complexity with negligible constant factors.
...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 Brython, 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:
Metaclasses dynamically customize instance and subclass checks by implementing the __instancecheck__() and/or __subclasscheck__() dunder methods, including:
- PEP 3119-compliant metaclasses (e.g., abc.ABCMeta).
Pseudo-superclasses dynamically customize the method resolution order (MRO) of subclasses by implementing the __mro_entries__() dunder method, including:
- PEP 560-compliant pseudo-superclasses.
Classes dynamically register themselves with standard abstract base classes (ABCs), including:
Classes are dynamically constructed or altered, including by:
- Class decorators.
- Class factory functions and methods.
- Metaclasses.
- Monkey patches.
...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:
- Exhibit O(1) non-amortized worst-case time complexity with negligible constant factors.
- Be either more efficient (in the common case) or exactly as efficient minus the cost of an additional stack frame (in the worst case) as equivalent type-checking implemented by hand, which no one should ever do.
Quickstart
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.beartype() way.
Toy Example
Let's type-check a "Hello, Jungle!" toy example. Just:
Import the beartype.beartype() decorator:
from beartype import beartype
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)
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.
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.
Tutorial
Let's begin with the simplest type of type-checking supported by beartype.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.beartype() and at runtime by wrappers generated by beartype.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.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.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 @beartype class MaximsOfBaloo(object): 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.
Would You Like to Know More?
If you know type hints, you know beartype. Since beartype is driven by tool-agnostic community standards, the public API for beartype is basically just 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:
- "Python Type Checking (Guide)", a comprehensive third-party introduction to the subject. Like most existing articles, this guide predates O(1) runtime type checkers and thus discusses only static type-checking. Thankfully, the underlying syntax and semantics cleanly translate to runtime type-checking.
- "PEP 484 -- Type Hints", the defining standard, holy grail, and first testament of type hinting personally authored by Python's former Benevolent Dictator for Life (BDFL) himself, Guido van Rossum. Since it's surprisingly approachable and covers all the core conceits in detail, we recommend reading at least a few sections of interest. Since it's really a doctoral thesis by another name, we can't recommend reading it in entirety. So it goes.
- TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Beartype API: It Bears Bookmarking
Beartype isn't just the beartype.beartype() decorator.
Beartype is a menagerie of public APIs for type-checking, introspecting, and manipulating type hints at runtime – all accessible under the beartype package installed when you installed beartype. But all beartype documentation begins with beartype.beartype(), just like all rivers run to the sea. [1]
- [1]
That's a lie, actually. Numerous river tributaries just pour out into deserts. Do endorheic basins mean nothing to you, beartype? Wikipedia: the more you click, the less you know.
TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Beartype Import Hooks
Beartype import hooks enforce type hints across your entire app in two lines of code with no runtime overhead. This is beartype import hooks in ten seconds. dyslexia notwithstanding
# Add *ONE* of the following semantically equivalent two-liners to the very # top of your "{your_package}.__init__" submodule. Start with *THE FAST WAY*. # ....................{ THE FAST WAY }.................... from beartype.claw import beartype_this_package # <-- this is boring, but... beartype_this_package() # <-- the fast way # ....................{ THE LESS FAST WAY }.................... from beartype.claw import beartype_package # <-- still boring, but... beartype_package('{your_package}') # <-- the less fast way # ....................{ THE MORE SLOW WAY }.................... from beartype.claw import beartype_packages # <-- boring intensifies beartype_packages(('{your_package}',)) # <-- the more slow way
Beartype import hooks extend the surprisingly sharp claws of beartype to your full app stack, whether anyone else wanted you to do that or not. Claw your way to the top of the bug heap; then sit on that heap with a smug expression. Do it for the new guy sobbing quietly in his cubicle.
Bear with Us
- Import Hooks Overview
Import Hooks Overview, Part Deux
- What Is beartype_this_package()?
- Why Is beartype_this_package()?
Import Hooks API
- Global Import Hooks
- Import Hook Configuration
Import Hooks Overview
Beartype import hooks implicitly perform both:
- Standard runtime type-checking (ala the beartype.beartype() decorator).
- Standard static type-checking (ala mypy and pyright) but at runtime – and that ain't standard.
Automate the beartype.beartype() decorator away today with magical import hooks published by the beartype.claw subpackage. When you install import hooks from beartype, you augment beartype from a pure-runtime second-generation type-checker into a hybrid runtime-static third-generation type-checker. That's right.
Beartype is now a tentacular cyberpunk horror like that mutant brain baby from Katsuhiro Otomo's dystopian 80's masterpiece Akira. You can't look away! [image: mutant brain baby] [image]
May Neo-Tokyo have mercy on your codebase's soul.
Import Hooks Overview, Part Deux
Beartype import hooks is a hobbit hole so deep we had to deescalate it with decrepit manga panels from Akira. Prepare to enter that hole.
What Is beartype_this_package()?
Let's begin by outlining exactly what beartype_this_package() does.
As the simplest and most convenient of several import hooks published by the beartype.claw subpackage, beartype_this_package() type-checks all subsequently imported submodules of {your_package}. Notably, beartype_this_package():
- Implicitly decorates all callables and classes across {your_package} by the beartype.beartype() decorator. Rejoice, fellow mammals! You no longer need to explicitly decorate anything by beartype.beartype() ever again. Of course, you can if you want to – but there's no compelling reason to do so and many compelling reasons not to do so. You have probably just thought of five, but there are even more.
- Implicitly appends every PEP 526-compliant annotated variable assignment (e.g., muh_int: int = 'Pretty sure this isn't an integer, but not sure.') across {your_package} by a new statement at the same indentation level calling the beartype.door.die_if_unbearable() function passed both that variable and that type hint. Never do that manually. Now, you never do.
Examples or we're lying again. beartype_this_package() transforms your {your_package}.{buggy_submodule} from this quietly broken code that you insist you never knew about, you swear:
# This is "{your_package}.{buggy_submodule}". It is bad, but you never knew. import typing as t bad_global: int = 'My eyes! The goggles do nothing.' # <-- no exception def bad_function() -> str: return b"I could've been somebody, instead of a bum byte string." bad_function() # <-- no exception class BadClass(object): def bad_method(self) -> t.NoReturn: return 'Nobody puts BadClass in the corner.' BadClass().bad_method() # <-- no exception
...into this loudly broken code that even your unionized QA team can no longer ignore:
# This is "{your_package}.{buggy_submodule}" on beartype_this_package(). # Any questions? Actually, that was rhetorical. No questions, please. from beartype import beartype from beartype.door import die_if_unbearable import typing as t bad_global: int = 'My eyes! The goggles do nothing.' die_if_unbearable(bad_global, int) # <-- raises exception @beartype def bad_function() -> str: return b"I could've been somebody, instead of a bum byte string." bad_function() # <-- raises exception @beartype class BadClass(object): def bad_method(self) -> t.NoReturn: return 'Nobody puts BadClass in the corner.' BadClass().bad_method() # <-- raises exception
By doing nothing, you saved five lines of extraneous boilerplate you no longer need to maintain, preserved DRY (Don't Repeat Yourself), and mended your coworker's career, who you would have blamed for all this. You had nothing to do with that code. It's a nothingburger!
Beartype believes you. This is why we beartype_this_package(). [image: looks kinda bad] [image]
This is what happens when we don't beartype_this_package().
Why Is beartype_this_package()?
Let's continue by justifying why you want to use beartype_this_package(). Don't worry. The "why?" is easier than the "what?". It often is. The answer is: "Safety is my middle name." <-- more lies
beartype_this_package() isolates its bug-hunting action to the current package. This is what everyone wants to try first. Type-checking only your first-party package under your control is the safest course of action, because you rigorously stress-tested your package with beartype. You did, didn't you? You're not making us look bad here? Don't make us look bad. We already have GitHub and Reddit for that.
Other beartype import hooks – like beartype_packages() or beartyping() – can be (mis)used to dangerously type-check other third-party packages outside your control that have probably never been stress-tested with beartype. Those packages could raise type-checking violations at runtime that you have no control over. If they don't now, they could later. Forward compatibility is out the window. git blame has things to say about that.
If beartype_this_package() fails, there is no hope for your package. Even though it might be beartype's fault, beartype will still blame you for its mistakes.
Import Hooks API
Beartype import hooks come in two flavours:
- Global import hooks, whose effects encompass all subsequently imported packages and modules matching various patterns.
- Local import hooks, whose effects are isolated to only specific packages and modules imported inside specific blocks of code. Any subsequently imported packages and modules remain unaffected.
Global Import Hooks
Global beartype import hooks are... well, global. Their claws extend to a horizontal slice of your full stack. These hooks globally type-check all annotated callables, classes, and variable assignments in all subsequently imported packages and modules matching various patterns.
With great globality comes great responsibility.
- beartype.claw.beartype_this_package(*, conf: beartype.BeartypeConf = beartype.BeartypeConf()) -> None
- Parameters
conf (beartype.BeartypeConf) -- Beartype configuration. Defaults to the default configuration performing O(1) type-checking.
- Raises
beartype.roar.BeartypeClawHookException --
If either:
- This function is not called from a module (i.e., this function is called directly from within a read–eval–print loop (REPL)).
- conf is not a beartype configuration.
Self-package runtime-static type-checking import hook. This hook accepts no package or module names, instead type-checking all annotated callables, classes, and variable assignments across all submodules of the current package (i.e., the caller-defined package directly calling this function).
This hook only applies to subsequent imports performed after this hook, as the term "import hook" implies; previously imported submodules and subpackages remain unaffected.
This hook is typically called as the first statement in the __init__ submodule of whichever (sub)package you would like to type-check. If you call this hook from:
- Your top-level {your_package}.__init__ submodule, this hook type-checks your entire package. This includes all submodules and subpackages across your entire package.
Some mid-level {your_package}.{your_subpackage}.__init__ submodule, this hook type-checks only that subpackage. This includes only submodules and subsubpackages of that subpackage. All other submodules and subpackages of your package remain unaffected (i.e., will not be type-checked).
# At the top of your "{your_package}.__init__" submodule: from beartype import BeartypeConf # <-- boilerplate from beartype.claw import beartype_this_package # <-- boilerplate: the revenge beartype_this_package(conf=BeartypeConf(is_color=False)) # <-- no color is best color
This hook is effectively syntactic sugar for the following idiomatic one-liners that are so cumbersome, fragile, and unreadable that no one should even be reading this:
beartype_this_package() # <-- this... beartype_package(__name__.rpartition('.')[0]) # <-- ...is equivalent to this... beartype_packages((__name__.rpartition('.')[0],)) # <-- ...is equivalent to this.
When in doubt, have no doubt. Just call beartype_this_package().
Added in version 0.15.0.
[image: fierce determined face] [image]
beartype_this_package(): It do be like that.
- beartype.claw.beartype_package(package_name: str, *, conf: beartype.BeartypeConf = beartype.BeartypeConf()) -> None
- Parameters
- package_name (str) -- Absolute name of the package or module to be type-checked.
- conf (beartype.BeartypeConf) -- Beartype configuration. Defaults to the default configuration performing O(1) type-checking.
- Raises
beartype.roar.BeartypeClawHookException --
If either:
- conf is not a beartype configuration.
package_name is either:
- Not a string.
- The empty string.
- A non-empty string that is not a valid package or module name (i.e., "."-delimited concatenation of valid Python identifiers).
Uni-package runtime-static type-checking import hook. This hook accepts only a single package or single module name, type-checking all annotated callables, classes, and variable assignments across either:
- If the passed name is that of a (sub)package, all submodules of that (sub)package.
- If the passed name is that of a (sub)module, only that (sub)module.
This hook should be called before that package or module is imported; when erroneously called after that package or module is imported, this hook silently reduces to a noop (i.e., does nothing regardless of how many times you squint at it suspiciously).
This hook is typically called as the first statement in the __init__ submodule of your top-level {your_package}.__init__ submodule.
# At the top of your "{your_package}.__init__" submodule: from beartype import BeartypeConf # <-- <Ctrl-c> <Ctrl-v> from beartype.claw import beartype_package # <-- <Ctrl-c> <Ctrl-v> x 2 beartype_package('your_package', conf=BeartypeConf(is_debug=True)) # ^-- they said explicit is better than implicit, # but all i got was this t-shirt and a hicky.
Of course, that's fairly worthless. Just call beartype_this_package(), right? But what if you want to type-check just one subpackage or submodule of your package rather than your entire package? In that case, beartype_this_package() is overbearing. badum ching Enter beartype_package(), the outer limits of QA where you control the horizontal and the vertical:
# Just because you can do something, means you should do something. beartype_package('good_package.m.A.A.d_submodule') # <-- fine-grained precision strike
beartype_package() shows it true worth, however, in type-checking other people's code. Because the beartype.claw API is a permissive Sarlacc pit, beartype_package() happily accepts the absolute name of any package or module – whether they wanted you to do that or not:
# Whenever you want to break something over your knee, never leave your # favorite IDE [read: Vim] without beartype_package(). beartype_package('somebody_elses_package') # <-- blow it up like you just don't care
This hook is effectively syntactic sugar for passing the beartype_packages() function a 1-tuple containing only this package or module name.
beartype_package('your_package') # <-- this... beartype_packages(('your_package',)) # <-- ...is equivalent to this.
Pretend you didn't see that. Just call beartype_package().
Added in version 0.15.0.
[image: wizened psychic baby lady] [image]
Truer words were never spoken, wizened psychic baby lady.
- beartype.claw.beartype_packages(package_names: collections.abc.Iterable[str], *, conf: beartype.BeartypeConf = beartype.BeartypeConf()) -> None
- Parameters
- package_name (collections.abc.Iterable[str]) -- Iterable of the absolute names of one or more packages or modules to be type-checked.
- conf (beartype.BeartypeConf) -- Beartype configuration. Defaults to the default configuration performing O(1) type-checking.
- Raises
beartype.roar.BeartypeClawHookException --
If either:
- conf is not a beartype configuration.
package_names is either:
- Not an iterable.
- The empty iterable.
A non-empty iterable containing at least one item that is either:
- Not a string.
- The empty string.
- A non-empty string that is not a valid package or module name (i.e., "."-delimited concatenation of valid Python identifiers).
Multi-package runtime-static type-checking import hook. This hook accepts one or more package and module names in any arbitrary order (i.e., order is insignificant), type-checking all annotated callables, classes, and variable assignments across:
- For each passed name that is a (sub)package, all submodules of that (sub)package.
- For each passed name that is a (sub)module, only that (sub)module.
This hook should be called before those packages and modules are imported; when erroneously called after those packages and modules are imported, this hook silently reduces to a noop. Squinting still does nothing.
This hook is typically called as the first statement in the __init__ submodule of your top-level {your_package}.__init__ submodule.
# At the top of your "{your_package}.__init__" submodule: from beartype import BeartypeConf # <-- copy-pasta from beartype.claw import beartype_packages # <-- copy-pasta intensifies beartype_packages(( 'your_package', 'some_package.published_by.the_rogue_ai.Johnny_Twobits', # <-- seems trustworthy 'numpy', # <-- ...heh. no one knows what will happen here! 'scipy', # <-- ...but we can guess, can't we? *sigh* ), conf=BeartypeConf(is_pep484_tower=True)) # <-- so. u 2 h8 precision.
This hook is the penultimate force in global import hooks. The terser beartype_this_package() and beartype_package() hooks are effectively syntactic sugar for this verboser hook.
One hook to QA them all, and in the darkness of your codebase bind them.
Added in version 0.15.0.
[image: it's the end of the road as we know it, and i feel fine] [image]
It’s almost as if we know what “penultimate” means.
- beartype.claw.beartype_all(*, conf: beartype.BeartypeConf = beartype.BeartypeConf()) -> None
- Parameters
conf (beartype.BeartypeConf) -- Beartype configuration. Defaults to the default configuration performing O(1) type-checking.
- Raises
beartype.roar.BeartypeClawHookException -- If conf is not a beartype configuration.
All-packages runtime-static type-checking import hook. This hook accepts no package or module names, instead type-checking all callables, classes, and variable assignments across all submodules of all packages.
This hook should be called before those packages and modules are imported; when erroneously called after those packages and modules are imported, this hook silently reduces to a noop. Not even squinting can help you now.
This hook is typically called as the first statement in the __init__ submodule of your top-level {your_package}.__init__ submodule.
# At the top of your "{your_package}.__init__" submodule, from beartype import BeartypeConf # <-- @beartype seemed so innocent, once from beartype.claw import beartype_all # <-- where did it all go wrong? beartype_all(conf=BeartypeConf(claw_is_pep526=False)) # <-- U WILL BE ASSIMILATE
This hook is the ultimate import hook, spasmodically unleashing a wave of bug-defenestrating action over the entire Python ecosystem. After calling this hook, any package or module authored by anybody (including packages and modules in CPython's standard library) will be subject to the iron claw of beartype.claw. Its rule is law!
This hook is the runtime equivalent of a full-blown pure-static type-checker like mypy or pyright, enabling full-stack runtime-static type-checking over your entire app. This includes submodules defined by both:
- First-party proprietary packages authored explicitly for this app.
- Third-party open-source packages authored and maintained elsewhere.
Nothing is isolated. Everything is permanent. Do not trust this hook.
Caveat Emptor: Empty Promises Not Even a Cat Would Eat
This hook imposes type-checking on all downstream packages importing your package, which may not necessarily want, expect, or tolerate type-checking. This hook is not intended to be called from intermediary APIs, libraries, frameworks, or other middleware. Packages imported by other packages should not call this hook. This hook is only intended to be called from full-stack end-user applications as a convenient alternative to manually passing the names of all packages to be type-checked to the more granular beartype_packages() hook.
This hook is the extreme QA nuclear option. Because this hook is the extreme QA nuclear option, most codebases should not call this hook.
beartype cannot be held responsible for a sudden rupture in the plenæne of normalcy, the space-time continuum, or your once-stable job. Pour one out for those who are about to vitriolically explode their own code.
Nuke Python from orbit. Because now you can.
Added in version 0.15.0.
[image: quiet, safe life] [image]
The beartype_all() lifestyle. Short but sweet.
Import Hook Configuration
Beartype import hooks accept an optional keyword-only conf parameter whose value is a beartype configuration (i.e., beartype.BeartypeConf instance), defaulting to the default beartype configuration BeartypeConf(). Unsurprisingly, that configuration configures the behaviour of its hook: e.g.,
# In your "{your_package}.__init__" submodule, enable @beartype's support for # the PEP 484-compliant implicit numeric tower (i.e., expand "int" to "int | # float" and "complex" to "int | float | complex"): from beartype import BeartypeConf # <-- it all seems so familiar from beartype.claw import beartype_package # <-- boil it up, boilerplate beartype_package('your_package', conf=BeartypeConf(is_pep484_tower=True)) # <-- *UGH.*
Equally unsurprisingly, beartype.BeartypeConf has been equipped with import hook-aware super powers. Fine-tune the behaviour of our import hooks for your exact needs, including:
- BeartypeConf(claw_is_pep526: bool = True). By default, beartype.claw type-checks annotated variable assignments like muh_int: int = 'Pretty sure this isn't an integer.'. Although this is usually what everyone wants, this may not be what someone suspicious wearing aviator goggles, a red velvet cape, and too-tight black leather wants. Nobody knows what those people want. If you are such a person, consider disabling this option to reduce type safety and destroy your code like Neo-Tokyo vs. Mecha-Baby-Godzilla: ...who will win!?!?
BeartypeConf(warning_cls_on_decorator_exception: Optional[Type[Warning]] = None). By default, beartype.claw emits non-fatal warnings rather than fatal exceptions raised by the beartype.beartype() decorator at decoration time. This is usually what everyone wants, because beartype.beartype() currently fails to support all possible edge cases and is thus likely to raise at least one exception while decorating your entire package. To improve the resilience of beartype.claw against those edge cases, beartype.beartype() emits one warning for each decoration exception and then simply continues to the next decoratable callable or class. This is occasionally unhelpful. What if you really do want beartype.claw to raise a fatal exception on the first such edge case in your codebase – perhaps because you want to either see the full exception traceback or punish your coworkers who are violating typing standards by trying to use an imported module as a type hint? ...this actually happened In this case, consider:
Passing None as the value of this parameter. Doing so forces beartype.claw to act strictly, inflexibly, and angrily. Expect spittle-flecked mouth frothing and claws all over the place:
# In your "{your_package}.__init__" submodule, raise exceptions because you # hate worky. The CI pipeline you break over your knee may just be your own. from beartype import BeartypeConf # <-- boiling boilerplate... from beartype.claw import beartype_this_package # <-- ...ain't even lukewarm beartype_this_package(conf=BeartypeConf(warning_cls_on_decorator_exception=None)) # <-- *ohboy*
- TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Beartype Decoration
wrap anything with runtime type-checking ...except that, of course. — Thus Spake Bearathustra, Book I
The beating heart of beartype is the eponymous beartype() decorator. This is its story.
Bear with Us
Beartype Decorator API
Callable Mode
- ...as Decorator
- ...as Function
- ...as Noop
Class Mode
- ...versus Callable Mode
Configuration Mode
- Beartype Configuration API
- Beartype Strategy API
Beartype Environment Variables
Beartype Decorator API
- @beartype.beartype(cls: type | None = None, func: collections.abc.Callable | None = None, conf: BeartypeConf = BeartypeConf()) -> object
- Parameters
- cls (type | None) -- Pure-Python class to be decorated.
- func (collections.abc.Callable | None) -- Pure-Python function or method to be decorated.
- conf (beartype.BeartypeConf) -- Beartype configuration. Defaults to the default configuration performing O(1) type-checking.
- Returns
Passed class or callable wrapped with runtime type-checking.
Augment the passed object with performant runtime type-checking. Unlike most decorators, @beartype has three orthogonal modes of operation:
- Class mode – in which you decorate a class with @beartype, which then iteratively decorates all methods declared by that class with @beartype. This is the recommended mode for object-oriented logic.
- Callable mode – in which you decorate a function or method with @beartype, which then dynamically generates a new function or method wrapping the original function or method with performant runtime type-checking. This is the recommended mode for procedural logic.
- Configuration mode – in which you create your own app-specific @beartype decorator configured for your exact use case.
When chaining multiple decorators, order of decoration is significant but conditionally depends on the mode of operation. Specifically, in:
- Class mode, @beartype should usually be listed first.
- Callable mode, @beartype should usually be listed last.
It's not our fault. Surely documentation would never decieve you.
Callable Mode
def beartype.beartype(func: collections.abc.Callable) -> collections.abc.Callable
In callable mode, beartype() dynamically generates a new callable (i.e., pure-Python function or method) runtime type-checking the passed callable.
...as Decorator
Because laziness prevails, beartype() is usually invoked as a decorator. Simply prefix the callable to be runtime type-checked with the line @beartype. In this standard use pattern, beartype() silently:
- Replaces the decorated callable with a new callable of the same name and signature.
- Preserves the original callable as the __wrapped__ instance variable of that new callable.
An example explicates a thousand words.
# Import the requisite machinery. >>> from beartype import beartype # Decorate a function with @beartype. >>> @beartype ... def bother_free_is_no_bother_to_me(bothersome_string: str) -> str: ... return f'Oh, bother. {bothersome_string}' # Call that function with runtime type-checking enabled. >>> bother_free_is_no_bother_to_me(b'Could you spare a small smackerel?') BeartypeCallHintParamViolation: @beartyped bother_free_is_no_bother_to_me() parameter bothersome_string=b'Could you spare a small smackerel?' violates type hint <class 'str'>, as bytes b'Could you spare a small smackerel?' not instance of str. # Call that function with runtime type-checking disabled. WHY YOU DO THIS!? >>> bother_free_is_no_bother_to_me.__wrapped__( ... b'Could you spare a small smackerel?') "Oh, bother. b'Could you spare a small smackerel?'"
Because beartype() preserves the original callable as __wrapped__, beartype() seamlessly integrates with other well-behaved decorators that respect that same pseudo-standard. This means that beartype() can usually be listed in any arbitrary order when chained (i.e., combined) with other decorators.
Because this is the NP-hard timeline, however, assumptions are risky. If you doubt anything, the safest approach is just to list @beartype as the last (i.e., bottommost) decorator. This:
- Ensures that beartype() is called first on the decorated callable before other decorators have a chance to really muck things up. Other decorators: always the source of all your problems.
Improves both space and time efficiency. Unwrapping __wrapped__ callables added by prior decorators is an O(k) operation for k the number of previously run decorators. Moreover, builtin decorators like classmethod, property, and staticmethod create method descriptors; when run after a builtin decorator, beartype() has no recourse but to:
- Destroy the original method descriptor created by that builtin decorator.
- Create a new method type-checking the original method.
- Create a new method descriptor wrapping that method by calling the same builtin decorator.
An example is brighter than a thousand Suns! astronomers throwing chalk here
# Import the requisite machinery. >>> from beartype import beartype # Decorate class methods with @beartype in either order. >>> class BlastItAll(object): ... @classmethod ... @beartype # <-- GOOD. this is the best of all possible worlds. ... def good_idea(cls, we_will_dynamite: str) -> str: ... return we_will_dynamite ... ... @beartype # <-- BAD. technically, fine. pragmatically, slower. ... @classmethod ... def save_time(cls, whats_the_charge: str) -> str: ... return whats_the_charge
...as Function
Because Python means not caring what anyone else thinks, beartype() can also be called as a function. This is useful in unthinkable edge cases like monkey-patching other people's code with runtime type-checking. You usually shouldn't do this, but you usually shouldn't do a lot of things that you do when you're the sort of Pythonista that reads tortuous documentation like this.
# Import the requisite machinery. >>> from beartype import beartype # A function somebody else defined. Note the bad lack of @beartype. >>> def oh_bother_free_where_art_thou(botherfull_string: str) -> str: ... return f'Oh, oh! Help and bother! {botherfull_string}' # Monkey-patch that function with runtime type-checking. *MUHAHAHA.* >>> oh_bother_free_where_art_thou = beartype(oh_bother_free_where_art_thou) # Call that function with runtime type-checking enabled. >>> oh_bother_free_where_art_thou(b"I'm stuck!") BeartypeCallHintParamViolation: @beartyped oh_bother_free_where_art_thou() parameter botherfull_string=b"I'm stuck!" violates type hint <class 'str'>, as bytes b"I'm stuck!" not instance of str.
One beartype() to monkey-patch them all and in the darkness type-check them.
...as Noop
beartype() silently reduces to a noop (i.e., scoops organic honey out of a jar with its fat paws rather than doing something useful with its life) under common edge cases. When any of the following apply, beartype() preserves the decorated callable or class as is by just returning that callable or class unmodified (rather than augmenting that callable or class with unwanted runtime type-checking):
Beartype has been configured with the no-time strategy BeartypeStrategy.O0: e.g.,
# Import the requisite machinery. from beartype import beartype, BeartypeConf, BeartypeStrategy # Avoid type-checking *ANY* methods or attributes of this class. @beartype(conf=BeartypeConf(strategy=BeartypeStrategy.O0)) class UncheckedDangerClassIsDangerous(object): # This method raises *NO* type-checking violation despite returning a # non-"None" value. def unchecked_danger_method_is_dangerous(self) -> None: return 'This string is not "None". Sadly, nobody cares anymore.'
That callable or class has already been decorated by:
- The beartype() decorator itself.
The PEP 484-compliant typing.no_type_check() decorator: e.g.,
# Import more requisite machinery. It is requisite. from beartype import beartype from typing import no_type_check # Avoid type-checking *ANY* methods or attributes of this class. @no_type_check class UncheckedRiskyClassRisksOurEntireHistoricalTimeline(object): # This method raises *NO* type-checking violation despite returning a # non-"None" value. def unchecked_risky_method_which_i_am_squinting_at(self) -> None: return 'This string is not "None". Why does nobody care? Why?'
- That callable is unannotated (i.e., no parameters or return values in the signature of that callable are annotated by type hints).
- Sphinx is currently autogenerating documentation (i.e., Sphinx's "autodoc" extension is currently running).
Laziness + efficiency == beartype().
Class Mode
def beartype.beartype(cls: type) -> type
In class mode, beartype() dynamically replaces each method of the passed pure-Python class with a new method runtime type-checking the original method.
As with callable mode, simply prefix the class to be runtime type-checked with the line @beartype. In this standard use pattern, beartype() silently iterates over all instance, class, and static methods declared by the decorated class and, for each such method:
- Replaces that method with a new method of the same name and signature.
- Preserves the original method as the __wrapped__ instance variable of that new method.
...versus Callable Mode
Superficially, this is just syntactic sugar – but sometimes you gotta dip your paws into the honey pot.
# Import the requisite machinery. from beartype import beartype # Decorate a class with @beartype. @beartype class IAmABearOfNoBrainAtAll(object): def i_have_been_foolish(self) -> str: return 'A fly can't bird, but a bird can fly.' def and_deluded(self) -> str: return 'Ask me a riddle and I reply.' # ...or just decorate class methods directly with @beartype. # The class above is *EXACTLY* equivalent to the class below. class IAmABearOfNoBrainAtAll(object): @beartype def i_have_been_foolish(self) -> str: return 'A fly can't bird, but a bird can fly.' @beartype def and_deluded(self) -> str: return 'Ask me a riddle and I reply.'
Pragmatically, this is not just syntactic sugar. You must decorate classes (rather than merely methods) with beartype() to type-check the following:
- Class-centric type hints (i.e., type hints like the PEP 673-compliant typing.Self attribute that describe the decorated class itself). To type-check these kinds of type hints, beartype() needs access to the class. beartype() lacks access to the class when decorating methods directly. Instead, you must decorate classes by beartype() for classes declaring one or more methods annotated by one or more class-centric type hints.
- Dataclasses. The standard dataclasses.dataclass decorator dynamically generates and adds new dunder methods (e.g., __init__(), __eq__(), __hash__()) to the decorated class. These methods do not physically exist and thus cannot be decorated directly with beartype(). Instead, you must decorate dataclasses first by @beartype and then by @dataclasses.dataclass. Order is significant, of course. </sigh>
When decorating classes, @beartype should usually be listed as the first (i.e., topmost) decorator. This ensures that beartype() is called last on the decorated class after other decorators have a chance to dynamically monkey-patch that class (e.g., by adding new methods to that class). beartype() will then type-check the monkey-patched functionality as well.
Come for the working examples. Stay for the wild hand-waving.
# Import the requisite machinery. from beartype import beartype from dataclasses import dataclass # Decorate a dataclass first with @beartype and then with @dataclass. If you # accidentally reverse this order of decoration, methods added by @dataclass # like __init__() will *NOT* be type-checked by @beartype. (Blame Guido.) @beartype @dataclass class SoTheyWentOffTogether(object): a_little_boy_and_his_bear: str | bytes will_always_be_playing: str | None = None
Configuration Mode
def beartype.beartype(*, conf: beartype.BeartypeConf) -> collections.abc.Callable[[T], T]
In configuration mode, beartype() dynamically generates a new beartype() decorator – configured uniquely for your exact use case. You too may cackle villainously as you feel the unbridled power of your keyboard.
# Import the requisite machinery. from beartype import beartype, BeartypeConf, BeartypeStrategy # Dynamically create a new @monotowertype decorator configured to: # * Avoid outputting colors in type-checking violations. # * Enable support for the implicit numeric tower standardized by PEP 484. monotowertype = beartype(conf=BeartypeConf( is_color=False, is_pep484_tower=True)) # Decorate with this decorator rather than @beartype everywhere. @monotowertype def muh_colorless_permissive_func(int_or_float: float) -> float: return int_or_float ** int_or_float ^ round(int_or_float)
Configuration: because you know best.
Beartype Configuration API
- class beartype.BeartypeConf(*, is_color: bool | None = None, is_debug: bool = False, is_pep484_tower: bool = False, strategy: BeartypeStrategy = BeartypeStrategy.O1)
Beartype configuration (i.e., self-caching dataclass instance encapsulating all flags, options, settings, and other metadata configuring each type-checking operation performed by beartype – including each decoration of a callable or class by the beartype() decorator).
The default configuration BeartypeConf() configures beartype to:
- Perform O(1) constant-time type-checking for safety, scalability, and efficiency.
- Disable support for PEP 484's implicit numeric tower.
- Disable developer-specific debugging logic.
- Conditionally output color when standard output is attached to a terminal.
Beartype configurations may be passed as the optional keyword-only conf parameter accepted by most high-level runtime type-checking functions exported by beartype – including:
- The beartype.beartype() decorator.
- The beartype.claw.beartype_all() import hook.
- The beartype.claw.beartype_package() import hook.
- The beartype.claw.beartype_packages() import hook.
- The beartype.claw.beartype_this_package() import hook.
- The beartype.claw.beartyping() import hook.
- The beartype.door.die_if_unbearable() type-checker.
- The beartype.door.is_bearable() type-checker.
- The beartype.door.TypeHint.die_if_unbearable() type-checker.
- The beartype.door.TypeHint.is_bearable() type-checker.
Beartype configurations are immutable objects memoized (i.e., cached) on the unordered set of all passed parameters:
>>> from beartype import BeartypeConf >>> BeartypeConf() is BeartypeConf() True >>> BeartypeConf(is_color=False) is BeartypeConf(is_color=False) True
Beartype configurations are comparable under equality:
>>> BeartypeConf(is_color=False) == BeartypeConf(is_color=True) False
Beartype configurations are hashable and thus suitable for use as dictionary keys and set members:
>>> BeartypeConf(is_color=False) == BeartypeConf(is_color=True) False >>> confs = {BeartypeConf(), BeartypeConf(is_color=False)} >>> BeartypeConf() in confs True
Beartype configurations support meaningful repr() output:
>>> repr(BeartypeConf()) 'BeartypeConf(is_color=None, is_debug=False, is_pep484_tower=False, strategy=<BeartypeStrategy.O1: 2>)'
Beartype configurations expose read-only public properties of the same names as the above parameters:
>>> BeartypeConf().is_color None >>> BeartypeConf().strategy <BeartypeStrategy.O1: 2>
Keyword Parameters
Beartype configurations support optional read-only keyword-only parameters at instantiation time. Most parameters are suitable for passing by all beartype users in all possible use cases. Some are only intended to be passed by some beartype users in some isolated use cases.
This is their story.
General Keyword Parameters
General-purpose configuration parameters are always safely passable:
- is_debug
Type: bool = False
True only if debugging the beartype() decorator. If you're curious as to what exactly (if anything) beartype() is doing on your behalf, temporarily enable this boolean. Specifically, enabling this boolean (in no particular order):
- Caches the body of each type-checking wrapper function dynamically generated by beartype() with the standard linecache module, enabling these function bodies to be introspected at runtime and improving the readability of tracebacks whose call stacks contain one or more calls to these beartype()-decorated functions.
- Prints the definition (including both the signature and body) of each type-checking wrapper function dynamically generated by :func:.beartype` to standard output.
- Appends to the declaration of each hidden parameter (i.e., whose name is prefixed by "__beartype_" and whose value is that of an external attribute internally referenced in the body of that function) a comment providing the machine-readable representation of the initial value of that parameter, stripped of newlines and truncated to a hopefully sensible length. Since the low-level string munger called to do so is shockingly slow, these comments are conditionally embedded in type-checking wrapper functions only when this boolean is enabled.
Defaults to False. Eye-gouging sample output or it didn't happen, so:
# Import the requisite machinery. >>> from beartype import beartype, BeartypeConf # Dynamically create a new @bugbeartype decorator enabling debugging. # Insider D&D jokes in my @beartype? You'd better believe. It's happening. >>> bugbeartype = beartype(conf=BeartypeConf(is_debug=True)) # Decorate with this decorator rather than @beartype everywhere. >>> @bugbeartype ... def muh_bugged_func() -> str: ... return b'Consistency is the bugbear that frightens little minds.' (line 0001) def muh_bugged_func( (line 0002) *args, (line 0003) __beartype_func=__beartype_func, # is <function muh_bugged_func at 0x7f52733bad40> (line 0004) __beartype_conf=__beartype_conf, # is "BeartypeConf(is_color=None, is_debug=True, is_pep484_tower=False, strategy=<BeartypeStrategy... (line 0005) __beartype_get_violation=__beartype_get_violation, # is <function get_beartype_violation at 0x7f5273081d80> (line 0006) **kwargs (line 0007) ): (line 0008) # Call this function with all passed parameters and localize the value (line 0009) # returned from this call. (line 0010) __beartype_pith_0 = __beartype_func(*args, **kwargs) (line 0011) (line 0012) # Noop required to artificially increase indentation level. Note that (line 0013) # CPython implicitly optimizes this conditional away. Isn't that nice? (line 0014) if True: (line 0015) # Type-check this passed parameter or return value against this (line 0016) # PEP-compliant type hint. (line 0017) if not isinstance(__beartype_pith_0, str): (line 0018) raise __beartype_get_violation( (line 0019) func=__beartype_func, (line 0020) conf=__beartype_conf, (line 0021) pith_name='return', (line 0022) pith_value=__beartype_pith_0, (line 0023) ) (line 0024) (line 0025) return __beartype_pith_0
- is_pep484_tower
Type: bool = False
True only if enabling support for PEP 484's implicit numeric tower (i.e., lossy conversion of integers to floating-point numbers as well as both integers and floating-point numbers to complex numbers). Specifically, enabling this instructs beartype to automatically expand:
- All float type hints to float | int, thus implicitly accepting both integers and floating-point numbers for objects annotated as only accepting floating-point numbers.
- All complex type hints to complex | float | int, thus implicitly accepting integers, floating-point, and complex numbers for objects annotated as only accepting complex numbers.
Defaults to False to minimize precision error introduced by lossy conversions from integers to floating-point numbers to complex numbers. Since most integers do not have exact representations as floating-point numbers, each conversion of an integer into a floating-point number typically introduces a small precision error that accumulates over multiple conversions and operations into a larger precision error. Enabling this improves the usability of public APIs at a cost of introducing precision errors.
The standard use case is to dynamically define your own app-specific beartype() decorator unconditionally enabling support for the implicit numeric tower, usually as a convenience to your userbase who do not particularly care about the above precision concerns. Behold the permissive powers of... @beartowertype!
# Import the requisite machinery. from beartype import beartype, BeartypeConf # Dynamically create a new @beartowertype decorator enabling the tower. beartowertype = beartype(conf=BeartypeConf(is_pep484_tower=True)) # Decorate with this decorator rather than @beartype everywhere. @beartowertype def crunch_numbers(numbers: list[float]) -> float: return sum(numbers) # This is now fine. crunch_numbers([3, 1, 4, 1, 5, 9]) # This is still fine, too. crunch_numbers([3.1, 4.1, 5.9])
Added in version 0.12.0.
- strategy
Type: BeartypeStrategy = BeartypeStrategy.O1
Type-checking strategy (i.e., BeartypeStrategy enumeration member dictating how many items are type-checked at each nesting level of each container and thus how responsively beartype type-checks containers). This setting governs the core tradeoff in runtime type-checking between:
- Overhead in the amount of time that beartype spends type-checking.
- Completeness in the number of objects that beartype type-checks.
As beartype gracefully scales up to check larger and larger containers, so beartype simultaneously scales down to check fewer and fewer items of those containers. This scalability preserves performance regardless of container size while increasing the likelihood of false negatives (i.e., failures to catch invalid items in large containers) as container size increases. You can either type-check a small number of objects nearly instantaneously or you can type-check a large number of objects slowly. Pick one.
Defaults to BeartypeStrategy.O1, the constant-time O(1) strategy – maximizing scalability at a cost of also maximizing false positives.
App-only Keyword Parameters
App-only configuration parameters are passed only by first-party packages executed as apps, binaries, scripts, servers, or other executable processes (rather than imported as libraries, frameworks, or other importable APIs into the current process):
- is_color
Type: bool | None = None
Tri-state boolean governing how and whether beartype colours type-checking violations (i.e., human-readable beartype.roar.BeartypeCallHintViolation exceptions) with POSIX-compliant ANSI escape sequences for readability. Specifically, if this boolean is:
- False, beartype never colours type-checking violations raised by callables configured with this configuration.
- True, beartype always colours type-checking violations raised by callables configured with this configuration.
- None, beartype conditionally colours type-checking violations raised by callables configured with this configuration only when standard output is attached to an interactive terminal.
The ${Beartype_is_color} environment variable globally overrides this parameter, enabling end users to enforce a global colour policy across their full app stack. When both that variable and this parameter are set to differing (and thus conflicting) values, the BeartypeConf class:
- Ignores this parameter in favour of that variable.
- Emits a beartype.roar.BeartypeConfShellVarWarning warning notifying callers of this conflict.
To avoid this conflict, only downstream executables should pass this parameter; intermediary libraries should never pass this parameter. Non-violent communication begins with you.
Effectively defaults to None. Technically, this parameter defaults to a private magic constant not intended to be passed by callers, enabling beartype to reliably detect whether the caller has explicitly passed this parameter or not.
The standard use case is to dynamically define your own app-specific beartype() decorator unconditionally disabling colours in type-checking violations, usually due to one or more frameworks in your app stack failing to support ANSI escape sequences. Please file issues with those frameworks requesting ANSI support. In the meanwhile, behold the monochromatic powers of... @monobeartype!
# Import the requisite machinery. from beartype import beartype, BeartypeConf # Dynamically create a new @monobeartype decorator disabling colour. monobeartype = beartype(conf=BeartypeConf(is_color=False)) # Decorate with this decorator rather than @beartype everywhere. @monobeartype def muh_colorless_func() -> str: return b'In the kingdom of the blind, you are now king.'
Added in version 0.12.0.
Beartype Strategy API
- class beartype.BeartypeStrategy
Superclass(es): enum.Enum
Enumeration of all kinds of type-checking strategies (i.e., competing procedures for type-checking objects passed to or returned from beartype()-decorated callables, each with concomitant tradeoffs with respect to runtime complexity and quality assurance).
Strategies are intentionally named according to conventional Big O notation (e.g., BeartypeStrategy.On enables the O(n) strategy). Strategies are established per-decoration at the fine-grained level of callables decorated by the beartype() decorator. Simply set the BeartypeConf.strategy parameter of the BeartypeConf object passed as the optional conf parameter to the beartype() decorator.
# Import the requisite machinery. from beartype import beartype, BeartypeConf, BeartypeStrategy # Dynamically create a new @slowmobeartype decorator enabling "full fat" # O(n) type-checking. slowmobeartype = beartype(conf=BeartypeConf(strategy=BeartypeStrategy.On)) # Type-check all items of the passed list. Do this only when you pretend # to know in your guts that this list will *ALWAYS* be ignorably small. @bslowmobeartype def type_check_like_maple_syrup(liquid_gold: list[int]) -> str: return 'The slowest noop yet envisioned? You're not wrong.'
Strategies enforce their corresponding runtime complexities (e.g., O(n)) across all type-checks performed for callables enabling those strategies. For example, a callable configured by the BeartypeStrategy.On strategy will exhibit linear O(n) complexity as its overhead for type-checking each nesting level of each container passed to and returned from that callable.
This enumeration defines these members:
- On
Type: beartype.cave.EnumMemberType
Linear-time strategy: the O(n) strategy, type-checking all items of a container.
NOTE:
This strategy is currently unimplemented. Still, interested users are advised to opt-in to this strategy now; your code will then type-check as desired on the first beartype release supporting this strategy.
Beartype: We're here for you, fam.
- Ologn
Type: beartype.cave.EnumMemberType
Logarithmic-time strategy: the O(\log n) strategy, type-checking a randomly selected number of items log(len(obj)) of each container obj.
NOTE:
This strategy is currently unimplemented. Still, interested users are advised to opt-in to this strategy now; your code will then type-check as desired on the first beartype release supporting this strategy.
Beartype: We're here for you, fam.
- O1
Type: beartype.cave.EnumMemberType
Constant-time strategy: the default O(1) strategy, type-checking a single randomly selected item of each container. As the default, this strategy need not be explicitly enabled.
- O0
Type: beartype.cave.EnumMemberType
No-time strategy, disabling type-checking for a decorated callable by reducing beartype() to the identity decorator for that callable. This strategy is functionally equivalent to but more general-purpose than the standard typing.no_type_check() decorator; whereas typing.no_type_check() only applies to callables, this strategy applies to any context accepting a beartype configuration such as:
- The beartype() decorator decorating a class.
- The beartype.door.is_bearable() function.
- The beartype.door.die_if_unbearable() function.
- The beartype.door.TypeHint.is_bearable() method.
- The beartype.door.TypeHint.die_if_unbearable() method.
Just like in real life, there exist valid use cases for doing absolutely nothing – including:
Blacklisting callables. While seemingly useless, this strategy allows callers to selectively prevent callables that would otherwise be type-checked (e.g., due to class decorations or import hooks) from being type-checked:
# Import the requisite machinery. from beartype import beartype, BeartypeConf, BeartypeStrategy # Dynamically create a new @nobeartype decorator disabling type-checking. nobeartype = beartype(conf=BeartypeConf(strategy=BeartypeStrategy.O0)) # Automatically decorate all methods of this class... @beartype class TypeCheckedClass(object): # Including this method, which raises a type-checking violation # due to returning a non-"None" value. def type_checked_method(self) -> None: return 'This string is not "None". Apparently, that is a problem.' # Excluding this method, which raises *NO* type-checking # violation despite returning a non-"None" value. @nobeartype def non_type_checked_method(self) -> None: return 'This string is not "None". Thankfully, no one cares.'
Eliding overhead. Beartype already exhibits near-real-time overhead of less than 1µs (one microsecond, one millionth of a second) per call of type-checked callables. When even that negligible overhead isn't negligible enough, brave callers considering an occupational change may globally disable all type-checking performed by beartype. Prepare your resume beforehand. Also, do so only under production builds intended for release; development builds intended for testing should preserve type-checking.
Either:
- Pass Python the "-O" command-line option, which beartype respects.
- Run Python under the "PYTHONOPTIMIZE" environment variable, which beartype also respects.
Define a new @maybebeartype decorator disabling type-checking when an app-specific constant I_AM_RELEASE_BUILD defined elsewhere is enabled:
# Import the requisite machinery. from beartype import beartype, BeartypeConf, BeartypeStrategy # Let us pretend you know what you are doing for a hot moment. from your_app import I_AM_RELEASE_BUILD # Dynamically create a new @maybebeartype decorator disabling # type-checking when "I_AM_RELEASE_BUILD" is enabled. maybebeartype = beartype(conf=BeartypeConf(strategy=( BeartypeStrategy.O0 if I_AM_RELEASE_BUILD else BeartypeStrategy.O1 )) # Decorate with this decorator rather than @beartype everywhere. @maybebeartype def muh_performance_critical_func(big_list: list[int]) -> int: return sum(big_list)
Beartype Environment Variables
Beartype supports increasingly many environment variables (i.e., external shell variables associated with the active Python interpreter). Most of these variables globally override BeartypeConf parameters of similar names, enabling end users to enforce global configuration policies across their full app stacks.
Beneath environment variables... thy humongous codebase shalt rise.
${Beartype_is_color}
The ${BEARTYPE_IS_COLOR} environment variable globally overrides the BeartypeConf.is_color parameter, enabling end users to enforce a global colour policy. As with that parameter, this variable is a tri-state boolean with three possible string values:
- BEARTYPE_IS_COLOR='True', forcefully instantiating all beartype configurations across all Python processes with the is_color=True parameter.
- BEARTYPE_IS_COLOR='False', forcefully instantiating all beartype configurations across all Python processes with the is_color=False parameter.
- BEARTYPE_IS_COLOR='None', forcefully instantiating all beartype configurations across all Python processes with the is_color=None parameter.
Force beartype to obey your unthinking hatred of the colour spectrum. You can't be wrong!
BEARTYPE_IS_COLOR=False python3 -m monochrome_retro_app.its_srsly_cool
Added in version 0.16.0.
TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Beartype Validators
Validate anything with two-line type hints designed by you ⇄ built by beartype
When standards fail, do what you want anyway. When official type hints fail to scale to your validation use case, design your own PEP-compliant type hints with compact 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.
Bear with Us
- Validator Overview
- Validator API
- Validator Syntax
- Validator Caveats
Validator Showcase
- Full-Fat O(n) Matching
- Trendy String Matching
Type Hint Arithmetic
Type Hint Elision
- Booleans ≠ Integers
- Strings ≠ Sequences
- Tensor Property Matching
Validator Alternatives
NumPy Type Hints
- Typed NumPy Arrays
Validator Overview
Beartype validators are zero-cost code generators. Like the rest of beartype (but unlike other validation frameworks), beartype validators 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 attendant tradeoffs:
- Functional validators, created by subscripting the beartype.vale.Is factory 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.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.IsEqual) 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, fallback to functional validators for generality.
Validator API
- class beartype.vale.Is
Subscription API: beartype.vale.Is[collections.abc.Callable[[object], 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.
- class beartype.vale.IsAttr
Subscription API: beartype.vale.IsAttr[str, beartype.vale.*]
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):
- The unqualified name of that attribute.
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.
- class beartype.vale.IsEqual
Subscription API: beartype.vale.IsEqual[object]
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))]]
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:
- 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 IsEqual.
- class beartype.vale.IsInstance
Subscription API: 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]]
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, the following hints all 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:
- 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 IsInstance.
- class beartype.vale.IsSubclass
Subscription API: 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]]
IsSubclass generalizes the comparable PEP 484-compliant typing.Type and PEP 585-compliant type type hint factories. All three check subclassing of arbitrary superclasses. Despite the differing syntax, the following hints all 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:
- 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 IsSubclass.
- [1]
You don't want to know the type of enum.Enum members. Srsly. You don't. Okay... you do? Very well. It's enum.Enum. mic drop
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
- NOTE:
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!
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.beartype() decorator with a new parameter enabling full-fat type-checking. But don't wait for 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.
Trendy String Matching
Let's accept 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
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 scientists.
>>> 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.
Booleans ≠ Integers
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)
Strings ≠ Sequences
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
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]))
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:
- Either Python ≥ 3.9 or typing_extensions ≥ 3.9.0.0.
- Beartype, which hopefully goes without saying.
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:
- Python ≥ 3.8.0.
- beartype ≥ 0.8.0.
- NumPy ≥ 1.21.0.
- Either Python ≥ 3.9 or typing_extensions ≥ 3.9.0.0.
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 collective 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.
- TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Beartype DOOR
DOOR: the Decidedly Object-Oriented Runtime-checker DOOR: it's capitalized, so it matters
Enter the DOOR (Decidedly Object-oriented Runtime-checker): beartype's Pythonic API for introspecting, comparing, and type-checking PEP-compliant type hints in average-case O(1) time with negligible constants. It's fast is what we're saying.
O(1): it's just how beartype jiggles.
Bear with Us
- DOOR Overview
DOOR Procedures
- Procedural API
Procedural Showcase
- Detect API Breakage
DOOR Classes
- Object-oriented Cheatsheet
- Object-oriented Overview
- Object-oriented API
DOOR Overview
For efficiency, security, and scalability, the beartype codebase is like the Linux kernel. That's a polite way of saying our code is unreadable gibberish implemented:
- Procedurally, mostly with module-scoped functions. Classes? We don't need classes where we're going, which is nowhere you want to go.
- Iteratively, mostly with while loops over tuple instances. We shouldn't have admitted that. We are not kidding. We wish we were kidding. Beartype is an echo chamber of tuple all the way down. Never do what we do. This is our teaching moment.
DOOR is different. DOOR has competing goals like usability, maintainability, and debuggability. Those things are often valuable to people that live in mythical lands with lavish amenities like potable ground water, functioning electrical grids, and Internet speed in excess of 56k dial-up. To achieve this utopian dream, DOOR is implemented:
- Object-orientedly, with a non-trivial class hierarchy of metaclasses, mixins, and abstract base classes (ABC) nested twenty levels deep defining dunder methods deferring to public methods leveraging utility functions. Nothing really makes sense, but nothing has to. Tests say it works. After all, would tests lie? We will document everything one day.
- Recursively, with methods commonly invoking themselves until the call stack invariably ignites in flames. We are pretty sure we didn't just type that.
This makes DOOR unsuitable for use inside beartype itself (where ruthless micro-optimizations have beaten up everything else), but optimum for the rest of the world (where rationality, sanity, and business reality reigns in the darker excesses of humanity). This hopefully includes you.
Don't be like beartype. Use DOOR instead.
DOOR Procedures
Type-check anything against any type hint – at any time, anywhere.
"Any" is the key here. When the isinstance() and issubclass() builtins fail to scale, prefer the beartype.door procedural API.
Procedural API
- beartype.door.die_if_unbearable(obj: object, hint: object, *, conf: beartype.BeartypeConf = beartype.BeartypeConf()) -> None
- Parameters
- obj (object) -- Arbitrary object to be type-checked against hint.
- hint (object) -- Type hint to type-check obj against.
- conf (beartype.BeartypeConf) -- Beartype configuration. Defaults to the default configuration performing O(1) type-checking.
- Raises
beartype.roar.BeartypeCallHintViolation -- If obj violates hint.
Runtime type-checking exception raiser. If object obj:
- Satisfies type hint hint under configuration conf, die_if_unbearable() raises a typing-checking violation (i.e., human-readable beartype.roar.BeartypeCallHintViolation exception).
- Violates type hint hint under configuration conf, die_if_unbearable() reduces to a noop (i.e., does nothing bad).
Release the bloodthirsty examples!
# Import the requisite machinery. >>> from beartype.door import die_if_unbearable >>> from beartype.typing import List, Sequence # Type-check an object violating a type hint. >>> die_if_unbearable("My people ate them all!", List[int] | None]) BeartypeDoorHintViolation: Object 'My people ate them all!' violates type hint list[int] | None, as str 'My people ate them all!' not list or <class "builtins.NoneType">. # Type-check multiple objects satisfying multiple type hints. >>> die_if_unbearable("I'm swelling with patriotic mucus!", str | None) >>> die_if_unbearable("I'm not on trial here.", Sequence[str])
- TIP:
For those familiar with typeguard, this function implements the beartype equivalent of the low-level typeguard.check_type function. For everyone else, pretend you never heard us just namedrop typeguard.
- beartype.door.is_bearable(obj: object, hint: object, *, conf: beartype.BeartypeConf = beartype.BeartypeConf()) -> bool
- Parameters
- obj (object) -- Arbitrary object to be type-checked against hint.
- hint (object) -- Type hint to type-check obj against.
- conf (beartype.BeartypeConf) -- Beartype configuration. Defaults to the default configuration performing O(1) type-checking.
- Return bool
True only if obj satisfies hint.
Runtime type-checking tester. If object obj:
- Satisfies type hint hint under configuration conf, is_bearable() returns True.
- Violates type hint hint under configuration conf, is_bearable() returns False.
An example paints a thousand docstrings. ...what does that even mean?
# Import the requisite machinery. >>> from beartype.door import is_bearable >>> from beartype.typing import List, Sequence # Type-check an object violating a type hint. >>> is_bearable('Stop exploding, you cowards.', List[bool] | None) False # Type-check multiple objects satisfying multiple type hints. >>> is_bearable("Kif, I’m feeling the ‘Captain's itch.’", str | None) True >>> is_bearable('I hate these filthy Neutrals, Kif.', Sequence[str]) True
is_bearable() is a strict superset of the isinstance() builtin. is_bearable() can thus be safely called wherever isinstance() is called with the same exact parameters in the same exact order:
# Requisite machinery: I import you. >>> from beartype.door import is_bearable # These two statements are semantically equivalent. >>> is_bearable('I surrender and volunteer for treason.', str) True >>> isinstance('I surrender and volunteer for treason.', str) True # These two statements are semantically equivalent, too. >>> is_bearable(b'A moment of weakness is all it takes.', (str, bytes)) True >>> isinstance(b'A moment of weakness is all it takes.', (str, bytes)) True # These two statements are semantically equivalent, yet again. *shockface* >>> is_bearable('Comets: the icebergs of the sky.', bool | None) False >>> isinstance('Comets: the icebergs of the sky.', bool | None) True
is_bearable() is also a spiritual superset of the issubclass() builtin. is_bearable() can be safely called wherever issubclass() is called by replacing the superclass(es) to be tested against with a type[{cls}] or type[{cls1}] | ... | type[{clsN}] type hint:
# Machinery. It is requisite. >>> from beartype.door import is_bearable >>> from beartype.typing import Type >>> from collections.abc import Awaitable, Collection, Iterable # These two statements are semantically equivalent. >>> is_bearable(str, Type[Iterable]) True >>> issubclass(str, Iterable) True # These two statements are semantically equivalent, too. >>> is_bearable(bytes, Type[Collection] | Type[Awaitable]) True >>> issubclass(bytes, (Collection, Awaitable)) True # These two statements are semantically equivalent, yet again. *ohbygods* >>> is_bearable(bool, Type[str] | Type[float]) False >>> issubclass(bool, (str, float)) True
is_bearable() also performs PEP 647-compliant type narrowing with the standard typing.TypeGuard type hint, facilitating communication between beartype and static type-checkers (e.g., mypy, pyright). See this FAQ entry for further details.
- beartype.door.is_subhint(subhint: object, superhint: object) -> bool
- Parameters
- subhint (object) -- Type hint to tested as a subhint.
- superhint (object) -- Type hint to tested as a superhint.
- Return bool
True only if subhint is a subhint of superhint.
Subhint tester. If type hint:
- subhint is a subhint of type hint superhint, is_subhint() returns True; else, is_subhint() returns False.
superhint is a superhint of type hint subhint, is_subhint() returns True; else, is_subhint() returns False. This is an alternative way of expressing the same relation as the prior condition – just with the jargon reversed. Jargon gonna jargon.
# Import us up the machinery. >>> from beartype.door import is_subhint >>> from beartype.typing import Any >>> from collections.abc import Callable, Sequence # A type hint matching any callable accepting no arguments and returning # a list is a subhint of a type hint matching any callable accepting any # arguments and returning a sequence of any types. >>> is_subhint(Callable[[], list], Callable[..., Sequence[Any]]) True # A type hint matching any callable accepting no arguments and returning # a list, however, is *NOT* a subhint of a type hint matching any # callable accepting any arguments and returning a sequence of integers. >>> is_subhint(Callable[[], list], Callable[..., Sequence[int]]) False # Booleans are subclasses and thus subhints of integers. >>> is_subhint(bool, int) True # The converse, however, is *NOT* true. >>> is_subhint(int, bool) False # All classes are subclasses and thus subhints of themselves. >>> is_subhint(int, int) True
Equivalently, is_subhint() returns True only if all of the following conditions are satisfied:
Commensurability. subhint and superhint are semantically related by conveying broadly similar intentions, enabling these two hints to be reasonably compared. For example:
- callable.abc.Iterable[str] and callable.abc.Sequence[int] are semantically related. These two hints both convey container semantics. Despite their differing child hints, these two hints are broadly similar enough to be reasonably comparable.
- callable.abc.Iterable[str] and callable.abc.Callable[[], int] are not semantically related. Whereas the first hints conveys a container semantic, the second hint conveys a callable semantic. Since these two semantics are unrelated, these two hints are dissimilar enough to not be reasonably comparable.
Narrowness. The first hint is either narrower than or semantically equivalent to the second hint. Equivalently:
- The first hint matches less than or equal to the total number of all possible objects matched by the second hint.
- In incomprehensible set theoretic jargon, the size of the countably infinite set of all possible objects matched by the first hint is less than or equal to that of those matched by the second hint.
is_subhint() supports a variety of real-world use cases, including:
- Multiple dispatch. A pure-Python decorator can implement multiple dispatch over multiple overloaded implementations of the same callable by calling this function. An overload of the currently called callable can be dispatched to if the types of the passed parameters are all subhints of the type hints annotating that overload.
- Formal verification of API compatibility across version bumps. Automated tooling like linters, continuous integration (CI), git hooks, and integrated development environments (IDEs) can raise pre-release alerts prior to accidental publication of API breakage by calling this function. A Python API preserves backward compatibility if each type hint annotating each public class or callable of the current version of that API is a superhint of the type hint annotating the same class or callable of the prior release of that API.
Procedural Showcase
By the power of beartype, you too shall catch all the bugs.
Detect API Breakage
Detect breaking API changes in arbitrary callables via type hints alone in ten lines of code – ignoring imports, docstrings, comments, and blank lines to make us look better.
from beartype import beartype from beartype.door import is_subhint from beartype.peps import resolve_pep563 from collections.abc import Callable @beartype def is_func_api_preserved(func_new: Callable, func_old: Callable) -> bool: ''' ``True`` only if the signature of the first passed callable (presumably the newest version of some callable to be released) preserves backward API compatibility with the second passed callable (presumably an older previously released version of the first passed callable) according to the PEP-compliant type hints annotating these two callables. Parameters ---------- func_new: Callable Newest version of a callable to test for API breakage. func_old: Callable Older version of that same callable. Returns ---------- bool ``True`` only if the ``func_new`` API preserves the ``func_old`` API. ''' # Resolve all PEP 563-postponed type hints annotating these two callables # *BEFORE* reasoning with these type hints. resolve_pep563(func_new) resolve_pep563(func_old) # For the name of each annotated parameter (or "return" for an annotated # return) and the hint annotating that parameter or return for this newer # callable... for func_arg_name, func_new_hint in func_new.__annotations__.items(): # Corresponding hint annotating this older callable if any or "None". func_old_hint = func_old.__annotations__.get(func_arg_name) # If no corresponding hint annotates this older callable, silently # continue to the next hint. if func_old_hint is None: continue # Else, a corresponding hint annotates this older callable. # If this older hint is *NOT* a subhint of this newer hint, this # parameter or return breaks backward compatibility. if not is_subhint(func_old_hint, func_new_hint): return False # Else, this older hint is a subhint of this newer hint. In this case, # this parameter or return preserves backward compatibility. # All annotated parameters and returns preserve backward compatibility. return True
The proof is in the real-world pudding.
>>> from numbers import Real # New and successively older APIs of the same example function. >>> def new_func(text: str | None, ints: list[Real]) -> int: ... >>> def old_func(text: str, ints: list[int]) -> bool: ... >>> def older_func(text: str, ints: list) -> bool: ... # Does the newest version of that function preserve backward compatibility # with the next older version? >>> is_func_api_preserved(new_func, old_func) True # <-- good. this is good. # Does the newest version of that function preserve backward compatibility # with the oldest version? >>> is_func_api_preserved(new_func, older_func) False # <-- OH. MY. GODS.
In the latter case, the oldest version older_func() of that function ambiguously annotated its ints parameter to accept any list rather than merely a list of numbers. Both the newer version new_func() and the next older version old_func() resolve the ambiguity by annotating that parameter to accept only lists of numbers. Technically, that constitutes API breakage; users upgrading from the older version of the package providing older_func() to the newer version of the package providing new_func() could have been passing lists of non-numbers to older_func(). Their code is now broke. Of course, their code was probably always broke. But they're now screaming murder on your issue tracker and all you can say is: "We shoulda used beartype."
In the former case, new_func() relaxes the constraint from old_func() that this list contain only integers to accept a list containing both integers and floats. new_func() thus preserves backward compatibility with old_func().
Thus was Rome's API preserved in a day.
DOOR Classes
Introspect and compare type hints with an object-oriented hierarchy of Pythonic classes. When the standard typing module has you scraping your fingernails on the nearest whiteboard in chicken scratch, prefer the beartype.door object-oriented API.
You've already seen that type hints do not define a usable public Pythonic API. That was by design. Type hints were never intended to be used at runtime. But that's a bad design. Runtime is all that matters, ultimately. If the app doesn't run, it's broke – regardless of what the static type-checker says. Now, beartype breaks a trail through the spiny gorse of unusable PEP standards.
Object-oriented Cheatsheet
Open the locked cathedral of type hints with beartype.door: your QA crowbar that legally pries open all type hints. Cry havoc, the bugbears of war!
# This is DOOR. It's a Pythonic API providing an object-oriented interface # to low-level type hints that *OFFICIALLY* have no API whatsoever. >>> from beartype.door import TypeHint # DOOR hint wrapping a PEP 604-compliant type union. >>> union_hint = TypeHint(int | str | None) # <-- so. it begins. # DOOR hints have Pythonic public classes -- unlike normal type hints. >>> type(union_hint) beartype.door.UnionTypeHint # <-- what madness is this? # DOOR hints can be detected Pythonically -- unlike normal type hints. >>> from beartype.door import UnionTypeHint >>> isinstance(union_hint, UnionTypeHint) # <-- *shocked face* True # DOOR hints can be type-checked Pythonically -- unlike normal type hints. >>> union_hint.is_bearable('The unbearable lightness of type-checking.') True >>> union_hint.die_if_unbearable(b'The @beartype that cannot be named.') beartype.roar.BeartypeDoorHintViolation: Object b'The @beartype that cannot be named.' violates type hint int | str | None, as bytes b'The @beartype that cannot be named.' not str, <class "builtins.NoneType">, or int. # DOOR hints can be iterated Pythonically -- unlike normal type hints. >>> for child_hint in union_hint: print(child_hint) TypeHint(<class 'int'>) TypeHint(<class 'str'>) TypeHint(<class 'NoneType'>) # DOOR hints can be indexed Pythonically -- unlike normal type hints. >>> union_hint[0] TypeHint(<class 'int'>) >>> union_hint[-1] TypeHint(<class 'str'>) # DOOR hints can be sliced Pythonically -- unlike normal type hints. >>> union_hint[0:2] (TypeHint(<class 'int'>), TypeHint(<class 'str'>)) # DOOR hints supports "in" Pythonically -- unlike normal type hints. >>> TypeHint(int) in union_hint # <-- it's all true. True >>> TypeHint(bool) in union_hint # <-- believe it. False # DOOR hints are sized Pythonically -- unlike normal type hints. >>> len(union_hint) # <-- woah. 3 # DOOR hints test as booleans Pythonically -- unlike normal type hints. >>> if union_hint: print('This type hint has children.') This type hint has children. >>> if not TypeHint(tuple[()]): print('But this other type hint is empty.') But this other type hint is empty. # DOOR hints support equality Pythonically -- unlike normal type hints. >>> from typing import Union >>> union_hint == TypeHint(Union[int, str, None]) True # <-- this is madness. # DOOR hints support comparisons Pythonically -- unlike normal type hints. >>> union_hint <= TypeHint(int | str | bool | None) True # <-- madness continues. # DOOR hints publish the low-level type hints they wrap. >>> union_hint.hint int | str | None # <-- makes sense. # DOOR hints publish tuples of the original child type hints subscripting # (indexing) the original parent type hints they wrap -- unlike normal type # hints, which unreliably publish similar tuples under differing names. >>> union_hint.args (int, str, NoneType) # <-- sense continues to be made. # DOOR hints are semantically self-caching. >>> TypeHint(int | str | bool | None) is TypeHint(None | bool | str | int) True # <-- blowing minds over here.
beartype.door: never leave typing without it.
Object-oriented Overview
TypeHint wrappers:
- Are immutable, hashable, and thus safely usable both as dictionary keys and set members.
- Support efficient lookup of child type hints – just like dictionaries and sets.
- Support efficient iteration over and random access of child type hints – just like lists and tuples.
- Are partially ordered over the set of all type hints (according to the subhint relation) and safely usable in any algorithm accepting a partial ordering (e.g., topological sort).
- Guarantee similar performance as beartype.beartype() itself. All TypeHint methods and properties run in (possibly amortized) constant time with negligible constants.
Open the DOOR to a whole new world. Sing along, everybody! “A whole new worl– *choking noises*”
Object-oriented API
- class beartype.door.TypeHint(hint: object)
- Parameters
hint (object) -- Type hint to be introspected.
Type hint introspector, wrapping the passed type hint hint (which, by design, is mostly unusable at runtime) with an object-oriented Pythonic API designed explicitly for runtime use.
TypeHint wrappers are instantiated in the standard way. Appearences can be deceiving, however. In truth, TypeHint is actually an abstract base class (ABC) that magically employs exploitative metaclass trickery to instantiate a concrete subclass of itself appropriate for this particular kind of hint.
TypeHint is thus a type hint introspector factory. What you read next may shock you.
>>> from beartype.door import TypeHint >>> from beartype.typing import Optional, Union >>> type(TypeHint(str | list)) beartype.door.UnionTypeHint # <-- UnionTypeHint, I am your father. >>> type(TypeHint(Union[str, list])) beartype.door.UnionTypeHint # <-- NOOOOOOOOOOOOOOOOOOOOOOO!!!!!!!! >>> type(TypeHint(Optional[str])) beartype.door.UnionTypeHint # <-- Search your MRO. You know it to be true.
TypeHint wrappers cache efficient singletons of themselves. On the first instantiation of TypeHint by hint, a new instance unique to hint is created and cached; on each subsequent instantiation, the previously cached instance is returned. Observe and tremble in ecstasy as your introspection eats less space and time.
>>> from beartype.door import TypeHint >>> TypeHint(list[int]) is TypeHint(list[int]) True # <-- you caching monster. how could you? we trusted you!
TypeHint wrappers expose these public read-only properties:
- args
Type: tuple
Tuple of the zero or more original child type hints subscripting the original type hint wrapped by this wrapper.
>>> from beartype.door import TypeHint >>> TypeHint(list).args () # <-- i believe this >>> TypeHint(list[int]).args (int,) # <-- fair play to you, beartype! >>> TypeHint(tuple[int, complex]).args (int, complex) # <-- the mind is willing, but the code is weak.
TypeHint wrappers also expose the tuple of the zero or more child type wrappers wrapping these original child type hints with yet more TypeHint wrappers. As yet, there exists no comparable property providing this tuple. Instead, this tuple is accessed via dunder methods – including __iter__(), __getitem__(), and __len__(). Simply pass any TypeHint wrapper to a standard Python container like list, set, or tuple.
This makes more sense than it seems. Throw us a frickin' bone here.
>>> from beartype.door import TypeHint >>> tuple(TypeHint(list)) () # <-- is this the real life? is this just fantasy? ...why not both? >>> tuple(TypeHint(list[int])) (TypeHint(<class 'int'>),) # <-- the abyss is staring back at us here. >>> tuple(TypeHint(tuple[int, complex])) (TypeHint(<class 'int'>), TypeHint(<class 'complex'>)) # <-- make the bad documentation go away, beartype
This property is memoized (cached) for both space and time efficiency.
- hint
Type: object
Original type hint wrapped by this wrapper at instantiation time.
>>> from beartype.door import TypeHint >>> TypeHint(list[int]).hint list[int]
Seriously. That's it. That's the property. This isn't Principia Mathematica. To you who are about to fall asleep on your keyboards and wake up to find your git repositories empty, beartype salutes you.
- is_ignorable
Type: bool
True only if this type hint is ignorable (i.e., conveys no meaningful semantics despite superficially appearing to do so). While one might expect the set of all ignorable type hints to be both finite and small, one would be wrong. That set is actually countably infinite in size. Countably infinitely many type hints are ignorable. That's alot. These include:
- typing.Any, by design. Anything is ignorable. You heard it here.
- object, the root superclass of all types. All objects are instances of object, so object conveys no semantic meaning. Much like @leycec on Monday morning, squint when you see object.
- The unsubscripted typing.Optional singleton, which expands to the implicit Optional[Any] type hint under PEP 484. But PEP 484 also stipulates that all Optional[t] type hints expand to Union[t, type(None)] type hints for arbitrary arguments t. So, Optional[Any] expands to merely Union[Any, type(None)]. Since all unions subscripted by typing.Any reduce to merely typing.Any, the unsubscripted typing.Optional singleton also reduces to merely typing.Any. This intentionally excludes the Optional[type(None)] type hint, which the standard typing module reduces to merely type(None).
- The unsubscripted typing.Union singleton, which reduces to typing.Any by the same argument.
Any subscription of typing.Union by one or more ignorable type hints. There exists a countably infinite number of such subscriptions, many of which are non-trivial to find by manual inspection. The ignorability of a union is a transitive property propagated "virally" from child to parent type hints. Consider:
- Union[Any, bool, str]. Since typing.Any is ignorable, this hint is trivially ignorable by manual inspection.
- Union[str, List[int], NewType('MetaType', Annotated[object, 53])]. Although several child type hints of this union are non-ignorable, the deeply nested object child type hint is ignorable by the argument above. It transitively follows that the Annotated[object, 53] parent type hint subscripted by object, the typing.NewType parent type hint aliased to Annotated[object, 53], and the entire union subscripted by that typing.NewType are themselves all ignorable as well.
- Any subscription of typing.Annotated by one or more ignorable type hints. As with typing.Union, there exists a countably infinite number of such subscriptions. See the prior item. Or don't. You know. It's all a little boring and tedious, frankly. Are you even reading this? You are, aren't you? Well, dunk me in a bucket full of honey. Post a discussion thread on the beartype repository for your chance to win a dancing cat emoji today!
The typing.Generic and typing.Protocol superclasses, both of which impose no constraints in and of themselves. Since all possible objects satisfy both superclasses. both superclasses are equivalent to the ignorable object root superclass: e.g.,
>>> from typing as Protocol >>> isinstance(object(), Protocol) True # <-- uhh... >>> isinstance('wtfbro', Protocol) True # <-- pretty sure you lost me there. >>> isinstance(0x696969, Protocol) True # <-- so i'll just be leaving then, shall i?
- Any subscription of either the typing.Generic or typing.Protocol superclasses, regardless of whether the child type hints subscripting those superclasses are ignorable or not. Subscripting a type that conveys no meaningful semantics continues to convey no meaningful semantics. [Shocked Pikachu face.] For example, the type hints typing.Generic[typing.Any] and typing.Generic[str] are both equally ignorable – despite the str class being otherwise unignorable in most type hinting contexts.
- And frankly many more. And... now we know why this property exists.
This property is memoized (cached) for both space and time efficiency.
TypeHint wrappers expose these public methods:
- die_if_unbearable(obj: object, *, conf: beartype.BeartypeConf = beartype.BeartypeConf()) -> None
- Parameters
- obj (object) -- Arbitrary object to be type-checked against this type hint.
- conf (beartype.BeartypeConf) -- Beartype configuration. Defaults to the default configuration performing O(1) type-checking.
- Raises
beartype.roar.BeartypeCallHintViolation -- If obj violates this type hint.
Shorthand for calling the beartype.door.die_if_unbearable() function as die_if_unbearable(obj=obj, hint=self.hint, conf=conf). Behold: an example.
# This object-oriented approach... >>> from beartype.door import TypeHint >>> TypeHint(bytes | None).die_if_unbearable( ... "You can't lose hope when it's hopeless.") BeartypeDoorHintViolation: Object "You can't lose hope when it's hopeless." violates type hint bytes | None, as str "You can't lose hope when it's hopeless." not bytes or <class "builtins.NoneType">. # ...is equivalent to this procedural approach. >>> from beartype.door import die_if_unbearable >>> die_if_unbearable( ... obj="You can't lose hope when it's hopeless.", hint=bytes | None) BeartypeDoorHintViolation: Object "You can't lose hope when it's hopeless." violates type hint bytes | None, as str "You can't lose hope when it's hopeless." not bytes or <class "builtins.NoneType">.
- is_bearable(obj: object, *, conf: beartype.BeartypeConf = beartype.BeartypeConf()) -> bool
- Parameters
- obj (object) -- Arbitrary object to be type-checked against this type hint.
- conf (beartype.BeartypeConf) -- Beartype configuration. Defaults to the default configuration performing O(1) type-checking.
- Return bool
True only if obj satisfies this type hint.
Shorthand for calling the beartype.door.is_bearable() function as is_bearable(obj=obj, hint=self.hint, conf=conf). Awaken the example!
# This object-oriented approach... >>> from beartype.door import TypeHint >>> TypeHint(int | float).is_bearable( ... "It's like a party in my mouth and everyone's throwing up.") False # ...is equivalent to this procedural approach. >>> from beartype.door import is_bearable >>> is_bearable( ... obj="It's like a party in my mouth and everyone's throwing up.", ... hint=int | float, ... ) False
- is_subhint(superhint: object) -> bool
- Parameters
superhint (object) -- Type hint to tested as a superhint.
- Return bool
True only if this type hint is a subhint of superhint.
Shorthand for calling the beartype.door.is_subhint() function as is_subhint(subhint=self.hint, superhint=superhint). I love the smell of examples in the morning.
# This object-oriented approach... >>> from beartype.door import TypeHint >>> TypeHint(tuple[bool]).is_subhint(tuple[int]) True # ...is equivalent to this procedural approach. >>> from beartype.door import is_subhint >>> is_subhint(subhint=tuple[bool], superhint=tuple[int]) True
- TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Beartype Errors
...is that bear growling or is it just me? — common last words in rural Canada
Beartype only raises:
Beartype-specific exceptions. For your safety and ours, exceptions raised beartype are easily distinguished from exceptions raised by everybody else. All exceptions raised by beartype are instances of:
- Public types importable from the beartype.roar subpackage.
- The beartype.roar.BeartypeException abstract base class (ABC).
- Disambiguous exceptions. For your sanity and ours, every exception raised by beartype means one thing and one thing only. Beartype never reuses the same exception class to mean two different things – allowing you to trivially catch and handle the exact exception you're interested in.
Likewise, beartype only emits beartype-specific warnings and disambiguous warnings. Beartype is fastidious to a fault. Error handling is no... exception. punny *or* funny? you decide.
Bear with Us
- Exception API
Warning API
PEP 585 Deprecations
- What Does This Mean?
- Are We on the Worst Timeline?
Exception API
Beartype raises fatal exceptions whenever something explodes. Most are self-explanatory – but some assume prior knowledge of arcane type-hinting standards or require non-trivial resolutions warranting further discussion.
When that happens, don't be the guy that ignores this chapter.
- exception beartype.roar.BeartypeException
Superclass(es): Exception
Beartype exception root superclass. All exceptions raised by beartype are guaranteed to be instances of concrete subclasses of this abstract base class (ABC) whose class names strictly match either:
- Beartype{subclass_name}Violation for type-checking violations (e.g., BeartypeCallHintReturnViolation).
- Beartype{subclass_name}Exception for non-type-checking violations (e.g., BeartypeDecorHintPep3119Exception).
- exception beartype.roar.BeartypeDecorException
Superclass(es): BeartypeException
Beartype decorator exception superclass. All exceptions raised by the @beartype decorator at decoration time (i.e., while dynamically generating type-checking wrappers for decorated callables and classes) are guaranteed to be instances of concrete subclasses of this abstract base class (ABC). Since decoration-time exceptions are typically raised from module scope early in the lifetime of a Python process, you are unlikely to manually catch and handle decorator exceptions.
A detailed list of subclasses of this ABC is quite inconsequential. Very well. @leycec admits he was too tired to type it all out. @leycec also admits he played exploitative video games all night instead... again. @leycec is grateful nobody reads these API notes. checkmate, readthedocs.
- exception beartype.roar.BeartypeCallException
Superclass(es): BeartypeException
Beartype call-time exception superclass. Beartype type-checkers (including beartype.door.die_if_unbearable() and beartype.beartype()-decorated callables) raise instances of concrete subclasses of this abstract base class (ABC) at call-time – typically when failing a type-check.
All exceptions raised by beartype type-checkers are guaranteed to be instances of this ABC. Since type-checking exceptions are typically raised from function and method scopes later in the lifetime of a Python process, you are much more likely to manually catch and handle instances of this exception type than other types of beartype exceptions. This includes the pivotal BeartypeCallHintViolation type, which subclasses this type.
In fact, you're encouraged to do so. Repeat after Kermode Bear:
"Exceptions are fun, everybody."
Gotta catch 'em all!
- exception beartype.roar.BeartypeCallHintException
Superclass(es): BeartypeCallException
Beartype type-checking exception superclass. Beartype type-checkers (including beartype.door.die_if_unbearable() and beartype.beartype()-decorated callables) raise instances of concrete subclasses of this abstract base class (ABC) when failing a type-check at call time – typically due to you passing a parameter or returning a value violating a type hint annotating that parameter or return.
For once, we're not the ones to blame. The relief in our cubicle is palpable.
- exception beartype.roar.BeartypeCallHintForwardRefException
Superclass(es): BeartypeCallHintException
Beartype type-checking forward reference exception. Beartype type-checkers raise instances of this exception type when a forward reference type hint (i.e., string referring to a class that has yet to be defined) erroneously references either:
- An attribute that does not exist.
- An attribute that exists but whose value is not actually a class.
As we gaze forward in time, so too do we glimpse ourselves – unshaven and shabbily dressed – in the rear-view mirror.
>>> from beartype import beartype >>> from beartype.roar import BeartypeCallHintForwardRefException >>> @beartype ... def i_am_spirit_bear(favourite_foodstuff: 'salmon.of.course') -> None: pass >>> try: ... i_am_spirit_bear('Why do you eat all my salmon, Spirit Bear?') ... except BeartypeCallHintForwardRefException as exception: ... print(exception) Forward reference "salmon.of.course" unimportable.
- exception beartype.roar.BeartypeCallHintViolation
Superclass(es): BeartypeCallHintException
Beartype type-checking violation. This is the most important beartype exception you never hope to see – and thus the beartype exception you are most likely to see. When your code explodes at midnight, instances of this exception class were lighting the fuse behind your back.
Beartype type-checkers raise an instance of this exception class when an object to be type-checked violates the type hint annotating that object. Beartype type-checkers include:
- The beartype.door.die_if_unbearable() function.
- The beartype.door.TypeHint.die_if_unbearable() method.
- User-defined functions and methods decorated by the beartype.beartype() decorator, which then themselves become beartype type-checkers.
Because type-checking violations are why we are all here, instances of this exception class offer additional read-only public properties to assist you in debugging. Inspect these properties at runtime to resolve any lingering doubts about which coworker(s) you intend to blame in your next twenty Git commits:
- culprits
Type: tuple[object, ...]
Tuple of one or more culprits (i.e., irresponsible objects that violated the type hints annotating those objects during a recent type-check).
Specifically, this property returns either:
If a standard slow Python container (e.g., dict, list, set, tuple) is responsible for this violation, the 2-tuple (root_culprit, leaf_culprit) where:
- root_culprit is the outermost such container. This is usually the passed parameter or returned value indirectly violating this type hint.
- leaf_culprit is the innermost item nested in root_culprit directly violating this type hint.
- If a non-container (e.g., scalar, class instance) is responsible for this violation, the 1-tuple (culprit,) where culprit is that non-container.
Let us examine what the latter means for your plucky intern who will do this after fetching more pumpkin spice lattes for The Team™ (currently engrossed in a critical morale-building "Best of 260" Atari 2600 Pong competition):
# Import the requisite machinery. from beartype import beartype from beartype.roar import BeartypeCallHintViolation # Arbitrary user-defined classes. class SpiritBearIGiveYouSalmonToGoAway(object): pass class SpiritBearIGiftYouHoneyNotToStay(object): pass # Arbitrary instance of one of these classes. SPIRIT_BEAR_REFUSE_TO_GO_AWAY = SpiritBearIGiftYouHoneyNotToStay() # Callable annotated to accept instances of the *OTHER* class. @beartype def when_spirit_bear_hibernates_in_your_bed( best_bear_den: SpiritBearIGiveYouSalmonToGoAway) -> None: pass # Call this callable with this invalid instance. try: when_spirit_bear_hibernates_in_your_bed( SPIRIT_BEAR_REFUSE_TO_GO_AWAY) # *MAGIC HAPPENS HERE*. Catch violations and inspect their "culprits"! except BeartypeCallHintViolation as violation: # Assert that one culprit was responsible for this violation. assert len(violation.culprits) == 1 # The one culprit: don't think we don't see you hiding there! culprit = violation.culprits[0] # Assert that this culprit is the same instance passed above. assert culprit is SPIRIT_BEAR_REFUSE_TO_GO_AWAY
Caveats apply. This property makes a good-faith effort to list the most significant culprits responsible for this type-checking violation. In two edge cases beyond our control, this property falls back to listing truncated snapshots of the machine-readable representations of those culprits (e.g., the first 10,000 characters or so of their repr() strings). This safe fallback is triggered for each culprit that:
- Has already been garbage-collected. To avoid memory leaks, this property only weakly (rather than strongly) refers to these culprits and is thus best accessed only where these culprits are accessible. Technically, this property is safely accessible from any context. Practically, this property is most usefully accessed from the except ...: block directly catching this violation. Since these culprits may be garbage-collected at any time thereafter, this property cannot be guaranteed to refer to these culprits outside that block. If this property is accessed from any other context and one or more of these culprits have sadly passed away, this property dynamically reduces the corresponding items of this tuple to only the machine-readable representations of those culprits. [1]
- Is a builtin variable-sized C-based object (e.g., dict, int, list, str). Long-standing limitations within CPython itself prevent beartype from weakly referring to those objects. Openly riot on the CPython bug tracker if this displeases you as much as it does us.
Let us examine what this means for your malding CTO:
# Import the requisite machinery. from beartype import beartype from beartype.roar import BeartypeCallHintViolation from beartype.typing import List # Callable annotated to accept a standard container. @beartype def we_are_all_spirit_bear( best_bear_dens: List[List[str]]) -> None: pass # Standard container deeply violating the above type hint. SPIRIT_BEAR_DO_AS_HE_PLEASE = [ [b'Why do you sleep in my pinball room, Spirit Bear?']] # Call this callable with this invalid container. try: we_are_all_spirit_bear(SPIRIT_BEAR_DO_AS_HE_PLEASE) # Shoddy magic happens here. Catch violations and try (but fail) to # inspect the original culprits, because they were containers! except BeartypeCallHintViolation as violation: # Assert that two culprits were responsible for this violation. assert len(violation.culprits) == 2 # Root and leaf culprits. We just made these words up, people. root_culprit = violation.culprits[0] leaf_culprit = violation.culprits[1] # Assert that these culprits are, in fact, just repr() strings. assert root_culprit == repr(SPIRIT_BEAR_DO_AS_HE_PLEASE) assert leaf_culprit == repr(SPIRIT_BEAR_DO_AS_HE_PLEASE[0][0])
We see that beartype correctly identified the root culprit as the passed list of lists of byte-strings (rather than strings) and the leaf culprit as that byte-string. We also see that beartype only returned the repr() of both culprits rather than those culprits. Why? Because CPython prohibits weak references to both lists and byte-strings.
This is why we facepalm ourselves in the morning. We did it this morning. We'll do it next morning, too. Until the weakref module improves, @leycec's forehead will be swollen with an angry mass of unsightly red welts that are now festering unbeknownst to his wife.
Added in version 0.12.0.
- [1]
This exception stores the representations of these culprits inside itself when first raised. Like a gruesome time capsule, they return to haunt you.
Warning API
Beartype emits non-fatal warnings whenever something looks it might explode in your lap later... but has yet to do so. Since it is dangerous to go alone, let beartype's words of anxiety-provoking wisdom be your guide. The codebase you save might be your own.
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:
CAUTION:
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):
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:
# If you prefer attribute imports, 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 # Or if you prefer module imports, just do this... from beartype import typing # ...instead of this. #import typing
The public beartype.typing API is a mypy-compliant replacement for the typing API offering improved forward compatibility with future Python releases. For example:
- 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.
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) ...
Type aliases. The hardest (but best) solution is to use type aliases to conditionally annotate callables with either PEP 484 or PEP 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
Bear with Us
- The Left-Paw Path
The Left-Paw Path
See the left sidebar for links to human-readable API documentation – including:
- beartype, documenting the core beartype() decorator API.
- beartype.claw, documenting the beartype import hook API.
- beartype.door, documenting the Decidedly Object-Oriented Runtime-checker (DOOR) API.
- beartype.roar, documenting the beartype exception and warning API.
- beartype.vale, documenting the beartype validator API.
Or see these autogenerated indices for machine-readable laundry lists. For those about to put on the 90's-era Geocities nostalgia goggles, you prefer inscrutable enumerations in lexicographic (i.e., effectively arbitrary) order of all public beartype:
- Attributes. This is literally everything. By everything, we mean modules, classes, functions, and globals. If it's not here, it doesn't exist. If it actually exists, it's private and you shouldn't have gone there. But curiosity killed your codebase, didn't it? You went there. You violated privacy encapsulation and now nothing works. So this is what it's like when doves cry.
- Modules. Look. It's just modules. Never click this.
- TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Ask a Bear Bro Anything (Abba)
Beartype now answers your many pressing questions about life, love, and typing. Maximize your portfolio of crushed bugs by devoutly memorizing the answers to these... frequently asked questions (FAQ)!
Bear with Us
- What is beartype?
- What is typeguard?
- When should I use beartype?
- Does beartype do any bad stuff?
- Does beartype actually do anything?
- How much does all this really cost?
- Beartype just does random stuff? Really?
- What does "pure-Python" mean?
- What does "near-real-time" even mean? Are you just making stuff up?
- What does "hybrid runtime-static" mean? Pretty sure you made that up, too.
- "Third-generation type-checker" doesn't mean anything, does it?
How do I type-check...
- ...Boto3 types?
- ...JAX arrays?
- ...NumPy arrays?
- ...PyTorch tensors?
- ...mock types?
- ...pandas data frames?
- ...the current class?
- ...under VSCode?
- ...under [insert-IDE-name-here]?
- ...with type narrowing?
- How do I *ONLY* type-check while running my test suite?
- How do I *NOT* type-check something?
- Why is @leycec's poorly insulated cottage in the Canadian wilderness so cold?
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."; # <-- pretty sure that's not an "int". 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. Now you too can quack like a duck while roaring like a bear.
$ python3 - <<EOL from beartype import beartype @beartype def main() -> int: print("Hello, world!"); return "Goodbye, world."; # <-- pretty sure that's not an "int". 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 get_beartype_violation 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 and classes with PEP-compliant type hints and decorate those callables and classes with the @beartype.beartype decorator.
Prefer beartype over other runtime and static type-checkers whenever you lack perfect 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 manipulating Bigly Data™ (i.e., data that is big) in app callables – especially when accepting data as input into or returning data as output from those 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 and classes rather than the lower level of individual statements inside pure-Python callables and class. 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.
Does beartype do any bad stuff?
Beartype is free – free as in beer, speech, dependencies, space complexity, and time complexity. Beartype is the textbook definition of "free." We're pretty sure the Oxford Dictionary now just shows the beartype mascot instead of defining that term. Vector art that a Finnish man slaved for weeks over paints a thousand words.
Beartype 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 then raise exceptions when doing nothing inevitably turns out to have been a bad idea. Beartype also cleanly interoperates with popular static type-checkers, by which we mean mypy and pyright. (The other guys don't exist.)
Beartype can always be safely added to any Python package, module, app, or script regardless of size, scope, funding, or audience. Never worry about your backend Django server taking an impromptu swan dive on St. Patty's Day just because your frontend React client pushed a 5MB JSON file serializing a doubly-nested list of integers. Nobody could have foreseen this!
The idea of competing runtime type-checkers like typeguard is that they compulsively do 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 when 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.
Does beartype actually do anything?
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. Colloquially, beartype type-checks randomly sampled data. RNGesus, show your humble disciples the way!
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.
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
Yes, 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 much does all this really cost?
What substring of "beartype is free we swear it would we lie" did you not grep?
...very well. Let's pontificate.
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.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.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.beartype() consume comparatively little wall-clock, even when repeatedly called many times.
Beartype just does random stuff? Really?
Yes. Beartype just does random stuff. That's what we're trying to say here. We didn't want to admit it, but the ugly truth is out now. Are you smirking? Because that looks like a smirk. Repeat after this FAQ:
- 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.beartype() starting at that container and ending either when a check fails or all checks succeed, that callable performs these checks (in order):
- A shallow type-check that the current possibly nested container is an instance of the type given by the current possibly nested type hint.
- 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):
- A check that the object passed as this parameter is a list.
- A check that an item randomly selected from this list is a tuple.
- A check that an item randomly selected from this tuple is a sequence.
- 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, PyTorch tensors, NetworkX graphs, pandas data frame columns, and really all scientific containers ever.
What does “pure-Python” mean?
Beartype is implemented entirely in Python. It's Python all the way down. Beartype never made a Faustian bargain with diabolical non-Pythonic facehuggers like Cython, C extensions, or Rust extensions. This has profound advantages with no profound disadvantages (aside from our own loss in sanity) – which doesn't make sense until you continue reading. Possibly, not even then.
First, profound advantages. We need to make beartype look good to justify this FAQ entry. The advantage of staying pure-Python is that beartype supports everything that supports Python – including:
- Just-in-time (JIT) compilers! So, PyPy.
- Ahead-of-time transpilers! So, Nuitka.
- Python web distributions! So, Pyodide.
Next, profound disadvantages. There are none. Nobody was expecting that, were they? Suck it, tradeoffs. Okay... look. Can anybody handle "the Truth"? I don't even know what that means, but it probably relates to the next paragraph.
Ordinarily, beartype being pure-Python would mean that beartype is slow. Python is commonly considered to be Teh Slowest Language Evah, because it commonly is. Everything pure-Python is slow (much like our bathroom sink clogged with cat hair). Everyone knows that. It is common knowledge. This only goes to show that the intersection of "common knowledge" and "actual knowledge" is the empty set.
Thankfully, beartype is not slow. By confining itself to the subset of Python that is fast, [1] beartype is micro-optimized to exhibit performance on par with horrifying compiled systems languages like Rust, C, and C++ – without sacrificing all of the native things that make Python great.
- [1]
Yes, there is a subset of Python that is fast. Yes, beartype is implemented almost entirely in this subset. Some prefer the term "Overly Obfuscated Python Shenanigans (OOPS)." We made that up. We prefer the term Bearython: it's Python, only fast. We made that up too. Never code in Bearython. Sure, Bearython is fast. Sure, Bearython is also unreadable, unmaintainable, and undebuggable. Bearython explodes each line of code into a bajillion lines of mud spaghetti. Coworkers, interns, and project leads alike will unite in the common spirit of resenting your existence – no matter how much you point them to this educational and cautionary FAQ entry.
Which leads us straight to...
What does “near-real-time” even mean? Are you just making stuff up?
It means stupid-fast. And... yes. I mean no. Of course no! No! Everything you read is true, because Somebody on the Internet Said It. I mean, really. Would beartype just make stuff up? Okay... look. Here's the real deal. Let us bore this understanding into you. squinty eyes intensify
Beartype type-checks objects at runtime in around 1µs (i.e., one microsecond, one millionth of a second), the standard high-water mark for real-time software:
# Let's check a list of 181,320,382 integers in ~1µs. >>> from beartype import beartype >>> def sum_list_unbeartyped(some_list: list) -> int: ... return sum(some_list) >>> sum_list_beartyped = beartype(sum_list_unbeartyped) >>> %time sum_list_unbeartyped([42]*0xACEBABE) CPU times: user 3.15 s, sys: 418 ms, total: 3.57 s Wall time: 3.58 s # <-- okay. Out[20]: 7615456044 >>> %time sum_list_beartyped([42]*0xACEBABE) CPU times: user 3.11 s, sys: 440 ms, total: 3.55 s Wall time: 3.56 s # <-- woah. Out[22]: 7615456044
Beartype does not contractually guarantee this performance – as that example demonstrates. Under abnormal processing loads (e.g., leycec's arthritic Athlon™ II X2 240, because you can't have enough redundant 2's in a product line) or when passed worst-case type hints (e.g., classes whose metaclasses implement stunningly awful __isinstancecheck__() dunder methods), beartype's worst-case performance could exceed an average-case near-instantaneous response.
Beartype is therefore not real-time; beartype is merely near-real-time (NRT), also variously referred to as "pseudo-real-time," "quasi-real-time," or simply "high-performance." Real-time software guarantees performance with a scheduler forcibly terminating tasks exceeding some deadline. That's bad in most use cases. The outrageous cost of enforcement harms real-world performance, stability, and usability.
NRT. It's good for you. It's good for your codebase. It's just good.
What does “hybrid runtime-static” mean? Pretty sure you made that up, too.
Beartype is a third-generation type-checker seamlessly supporting both:
- New-school runtime-static type-checking via beartype import hooks. When you call import hooks published by the beartype.claw subpackage, you automagically type-check all annotated callables, classes, and variable assignments covered by those hooks. In this newer (and highly encouraged) modality, beartype performs both runtime and static analysis – enabling beartype to seamlessly support both prosaic and exotic type hints.
- Old-school runtime type-checking via the beartype.beartype() decorator. When you manually decorate callables and classes by beartype.beartype(), you type-check only annotated parameters, returns, and class variables. In this older (and mostly obsolete) modality, beartype performs no static analysis and thus no static type-checking. This suffices for prosaic type hints but fails for exotic type hints. After all, many type hints can only be type-checked with static analysis.
In the usual use case, you call our beartype.claw.beartype_this_package() function from your {your_package}.__init__ submodule to register an import hook for your entire package. Beartype then type-checks the following points of interest across your entire package:
- All annotated parameters and returns of all callables, which our import hooks decorate with beartype.beartype().
- All annotated attributes of all classes, which (...wait for it) our import hooks decorate with beartype.beartype().
All annotated variable assignments (e.g., muh_var: int = 42). After any assignment to a global or local variable annotated by a type hint, our import hooks implicitly append a new statement at the same indentation level calling our beartype.door.die_if_unbearable() function passed both that variable and that type hint. That is:
# Beartype import hooks append each assignment resembling this... {var_name}: {type_hint} = {var_value} # ...with a runtime type-check resembling this. die_if_unbearable({var_name}, {type_hint})
All annotated variable declarations (e.g., muh_var: int). After any declaration to a global or local variable annotated by a type hint not assigned a new value, our import hooks implicitly append a new statement at the same indentation level calling our beartype.door.die_if_unbearable() function passed both that variable and that type hint. That is:
# Beartype import hooks append each declaration resembling this... {var_name}: {type_hint} # ...with a runtime type-check resembling this. die_if_unbearable({var_name}, {type_hint})
beartype.claw: We broke our wrists so you don't have to.
“Third-generation type-checker” doesn't mean anything, does it?
Let's rewind. Follow your arthritic host, Granpa Leycec, on a one-way trip you won't soon recover from through the backwater annals of GitHub history.
Gather around, everyone! It's a tedious lore dump that will leave you enervated, exhausted, and wishing you'd never come:
- Gen 1. On October 28th, 2012, mypy launched the first generation of type-checkers. Like mypy, first-generation type-checkers are all pure-static type-checkers. They do not operate at runtime and thus cannot enforce anything at runtime. They operate entirely outside of runtime during an on-demand parser phase referred to as static analysis time – usually at the automated behest of a local IDE or remote continuous integration (CI) pipeline. Since they can't enforce anything, they're the monkey on your team's back that you really wish would stop flinging bodily wastes everywhere.
- Gen 2. On December 27th, 2015, typeguard 1.0.0 launched the second generation of type-checkers. [2] Like typeguard, second-generation type-checkers are all pure-runtime type-checkers. They operate entirely at runtime and thus do enforce everything at runtime – usually with a decorator manually applied to callables and classes. Conversely, they do not operate at static analysis time and thus cannot validate type hints requiring static analysis. While non-ideal, this tradeoff is generally seen as worthwhile by everybody except the authors of first-generation type-checkers. Enforcing some type hints is unequivocally better than enforcing no type hints.
Gen 3. On December 11th, 2019, typeguard 2.6.0 (yet again) launched the third generation of type-checkers. Like typeguard ≥ 2.6.0, third-generation type-checkers are all a best-of-breed hybridization of first- and second-generation type-checkers. They concurrently perform both:
- Standard runtime type-checking (ala the beartype.beartype() decorator).
- Standard static type-checking (ala mypy and pyright) but at runtime – which ain't standard.
First- and second-generation type-checkers invented a fundamentally new wheel. Third-generation type-checkers then bolted the old, busted, rubber-worn wheels built by prior generations onto the post-apocalyptic chassis of a shambolic doom mobile.
Beartype is a third-generation type-checker. This is the shock twist in the season finale that no one saw coming at all.
Beartype: shambolic doom mobile or bucolic QA utopia? Only your team decides.
- [2]
Cue Terminator-like flashback to Granpa Leycec spasmodically clutching a playground fence as QA explosions ignite a bug-filled horror show in the distant codebase. </awkward>
How do I type-check...
...yes? Do 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.
...JAX arrays?
You only have two options here. Choose wisely, wily scientist. If:
You don't mind adding an additional mandatory runtime dependency to your app:
- Require the third-party "jaxtyping" package.
- Annotate callables with type hint factories published by jaxtyping (e.g., jaxtyping.Float[jaxtyping.Array, '{metadata1 ... metadataN}']). Beartype fully supports typed JAX arrays. Because Google mathematician @patrick-kidger did all the hard work, we didn't have to. Bless your runtime API, @patrick-kidger.
- You mind adding an additional mandatory runtime dependency to your app, prefer beartype validators. Since JAX declares a broadly similar API to that of NumPy with its "jax.numpy" compatibility layer, most NumPy-specific examples cleanly generalize to JAX. Beartype is no exception.
Bask in the array of options at your disposal! ...get it? ...array? I'll stop now.
...NumPy arrays?
You have more than a few options here. If:
[Recommended] You don't mind adding an additional mandatory runtime dependency to your app:
- Require the third-party "jaxtyping" package. (Yes, really! Despite the now-historical name it also supports NumPy, PyTorch, and TensorFlow arrays and has no JAX dependency whatsoever.)
- Annotate callables with type hint factories published by jaxtyping (e.g., jaxtyping.Float[np.ndarray, '{metadata1 ... metadataN}']).
Because Google mathematician @patrick-kidger did all the hard work, we didn't have to. Bless your runtime API, @patrick-kidger.
You mind adding an additional mandatory runtime dependency to your app. Then prefer either:
- If you only want to type-check the dtype (but not shape) of NumPy arrays, the official "numpy.typing.NDArray[{dtype}]" type hint factory bundled with NumPy and explicitly supported by beartype – also referred to as a typed NumPy array. Beartype fully supports typed NumPy arrays. Because beartype cares.
- If you'd rather type-check arbitrary properties (including dtype and/or shape) of NumPy arrays, the beartype validator API bundled with beartype itself. Since doing so requires a bit more heavy lifting on your part, you probably just want to use jaxtyping instead. Seriously. @patrick-kidger is the way.
- If you'd rather type-check arbitrary properties (including dtype and/or shape) of NumPy arrays and don't mind requiring an unmaintained package that increasingly appears to be broken, consider the third-party "nptyping" package.
Options are good! Repeat this mantra in times of need.
...PyTorch tensors?
You only have two options here. We're pretty sure two is better than none. Thus, we give thanks. If:
You don't mind adding an additional mandatory runtime dependency to your app:
- Require the third-party "jaxtyping" package. (Yes, really! Despite the now-historical name it also supports PyTorch, and has no JAX dependency.)
- Annotate callables with type hint factories published by jaxtyping (e.g., jaxtyping.Float[torch.Tensor, '{metadata1 ... metadataN}']).
Beartype fully supports typed PyTorch tensors. Because Google mathematician @patrick-kidger did all the hard work, we didn't have to. Bless your runtime API, @patrick-kidger.
You mind adding an additional mandatory runtime dependency to your app. In this case, prefer beartype validators. For example, validate callable parameters and returns as either floating-point or integral PyTorch tensors via the functional validator factory beartype.vale.Is:
# 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 # Import PyTorch (d)types of interest. from torch import ( float as torch_float, int as torch_int, tensor, ) # PEP-compliant type hint matching only a floating-point PyTorch tensor. TorchTensorFloat = Annotated[tensor, Is[ lambda tens: tens.type() is torch_float]] # PEP-compliant type hint matching only an integral PyTorch tensor. TorchTensorInt = Annotated[tensor, Is[ lambda tens: tens.type() is torch_int]] # Type-check everything like an NLP babelfish. @beartype def deep_dream(dreamy_tensor: TorchTensorFloat) -> TorchTensorInt: return dreamy_tensor.type(dtype=torch_int)
Since beartype.vale.Is supports arbitrary Turing-complete Python expressions, the above example generalizes to typing the device, dimensionality, and other metadata of PyTorch tensors to whatever degree of specificity you desire.
beartype.vale.Is: it's lambdas all the way down.
...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) -> type: 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.
...pandas data frames?
Type-check any pandas object with type hints published by the third-party pandera package – the industry standard for Pythonic data validation and blah, blah, blah... hey wait. Is this HR speak in the beartype FAQ!? Yes. It's true. We are shilling.
Because caring is sharing code that works, beartype transparently supports all pandera type hints. Soon, you too will believe that machine-learning pipelines can be domesticated. Arise, huge example! Stun the disbelievers throwing peanuts at our issue tracker.
# Import important machinery. It's important. import pandas as pd import pandera as pa from beartype import beartype from pandera.dtypes import Int64, String, Timestamp from pandera.typing import Series # Arbitrary pandas data frame. If pandas, then data science. muh_dataframe = pd.DataFrame({ 'Hexspeak': ( 0xCAFED00D, 0xCAFEBABE, 0x1337BABE, ), 'OdeToTheWestWind': ( 'Angels of rain and lightning: there are spread', 'On the blue surface of thine aery surge,', 'Like the bright hair uplifted from the head', ), 'PercyByssheShelley': pd.to_datetime(( '1792-08-04', '1822-07-08', '1851-02-01', )), }) # Pandera dataclass validating the data frame above. As above, so below. class MuhDataFrameModel(pa.DataFrameModel): Hexspeak: Series[Int64] OdeToTheWestWind: Series[String] PercyByssheShelley: Series[Timestamp] # Custom callable you define. Here, we type-check the passed data frame, the # passed non-pandas object, and the returned series of this data frame. @beartype @pa.check_types def convert_dataframe_column_to_series( # Annotate pandas data frames with pandera type hints. dataframe: pa.typing.DataFrame[MuhDataFrameModel], # Annotate everything else with standard PEP-compliant type hints. \o/ column_name_or_index: str | int, # Annotate pandas series with pandera type hints, too. ) -> Series[Int64 | String | Timestamp]: ''' Convert the column of the passed pandas data frame (identified by the passed column name or index) into a pandas series. ''' # This is guaranteed to be safe. Since type-checks passed, this does too. return ( dataframe.loc[:,column_name_or_index] if isinstance(column_name_or_index, str) else dataframe.iloc[:,column_name_or_index] ) # Prints joyful success as a single tear falls down your beard stubble: # [Series from data frame column by *NUMBER*] # 0 3405697037 # 1 3405691582 # 2 322419390 # Name: Hexspeak, dtype: int64 # # [Series from data frame column by *NAME*] # 0 Angels of rain and lightning: there are spread # 1 On the blue surface of thine aery surge, # 2 Like the bright hair uplifted from the head # Name: OdeToTheWestWind, dtype: object print('[Series from data frame column by *NUMBER*]') print(convert_dataframe_column_to_series( dataframe=muh_dataframe, column_name_or_index=0)) print() print('[Series from data frame column by *NAME*]') print(convert_dataframe_column_to_series( dataframe=muh_dataframe, column_name_or_index='OdeToTheWestWind')) # All of the following raise type-checking violations. Feels bad, man. convert_dataframe_column_to_series( dataframe=muh_dataframe, column_name_or_index=['y u done me dirty'])) convert_dataframe_column_to_series( dataframe=DataFrame(), column_name_or_index=0))
Order of decoration is insignificant. The beartype.beartype() and pandera.check_types decorators are both permissive. Apply them in whichever order you like. This is fine, too:
# Everyone is fine with this. That's what they say. But can we trust them? @pa.check_types @beartype def convert_dataframe_column_to_series(...) -> ...: ...
There be dragons belching flames over the hapless village, however:
- If you forget the pandera.check_types decorator (but still apply the beartype.beartype() decorator), beartype.beartype() will only shallowly type-check (i.e., validate the types but not the contents of) pandas objects. This is better than nothing, but... look. No API is perfect. We didn't make crazy. We only integrate with crazy. The lesson here is to never forget the pandera.check_types decorator.
- If you forget the beartype.beartype() decorator (but still apply the pandera.check_types decorator), pandera.check_types will silently ignore everything except pandas objects. This is the worst case. This is literally the blimp crashing and burning on the cover of Led Zeppelin I. The lesson here is to never forget the beartype.beartype() decorator.
There are two lessons here. Both suck. Nobody should need to read fifty paragraphs full of flaming dragons just to validate pandas objects. Moreover, you are thinking: "It smells like boilerplate." You are not wrong. It is textbook boilerplate. Thankfully, your concerns can all be fixed with even more boilerplate. Did we mention none of this is our fault?
Define a new @bearpanderatype decorator internally applying both the beartype.beartype() and pandera.check_types decorators; then use that instead of either of those. Automate away the madness with more madness:
# Never again suffer for the sins of others. def bearpanderatype(*args, **kwargs): return beartype(pa.check_types(*args, **kwargs)) # Knowledge is power. Clench it with your iron fist until it pops. @bearpanderatype # <-- less boilerplate means more power def convert_dataframe_column_to_series(...) -> ...: ...
pandas + pandera + beartype: BFFs at last. Type-check pandas data frames in ML pipelines for the good of LLaMa-kind. Arise, bug-free GPT! Overthrow all huma— message ends
...the current class?
So. It comes to this. You want to type-check a method parameter or return to be an instance of the class declaring that method. In short, you want to type-check a common use case like this factory:
class ClassFactory(object): def __init__(self, *args) -> None: self._args = args def make_class(self, other): return ClassFactory(self._args + other._args)
The ClassFactory.make_class() method both accepts a parameter other whose type is ClassFactory and returns a value whose type is (again) ClassFactory – the class currently being declared. This is the age-old self-referential problem. How do you type-check the class being declared when that class has yet to be declared? The answer may shock your younger coworkers who are still impressionable and have firm ideals.
You have three choices here. One of these choices is good and worthy of smiling cat emoji. The other two are bad; mock them in git commit messages until somebody refactors them into the first choice:
[Recommended] The PEP 673-compliant typing.Self type hint (introduced by Python 3.11) efficiently and reliably solves this. Annotate the type of the current class as Self – fully supported by beartype:
# Import important stuff. Boilerplate: it's the stuff we make. from beartype import beartype from typing import Self # <---------------- if Python ≥ 3.11.0 # from typing_extensions import Self # <-- if Python < 3.11.0 # Decorate classes – not methods. It's rough. @beartype # <-- Yesss. Good. Feel the force. It flows like sweet honey. class ClassFactory(object): def __init__(self, *args: Sequence) -> None: self._args = args # @beartype # <-- No... Oh, Gods. *NO*! The dark side grows stronger. def make_class(self, other: Self) -> Self: # <-- We are all one self. return ClassFactory(self._args + other._args)
Technically, this requires Python 3.11. Pragmatically, typing_extensions means that you can bring Python 3.11 back with you into the past – where code was simpler, Python was slower, and nothing worked as intended despite tests passing.
Self is only contextually valid inside class declarations. beartype raises an exception when you attempt to use Self outside a class declaration (e.g., annotating a global variable, function parameter, or return).
Self can only be type-checked by classes decorated by the beartype.beartype() decorator. Corollary: Self cannot be type-checked by methods decorated by beartype.beartype() – because the class to be type-checked has yet to be declared at that early time. The pain that you feel is real.
A PEP 484-compliant forward reference (i.e., type hint that is a string that is the unqualified name of the current class) also solves this. The only costs are inexcusable inefficiency and unreliability. This is what everyone should no longer do. This is...
# The bad old days when @beartype had to bathe in the gutter. # *PLEASE DON'T DO THIS ANYMORE.* Do you want @beartype to cry? from beartype import beartype @beartype class BadClassFactory(object): def __init__(self, *args: Sequence) -> None: self._args = args def make_class(self, other: 'BadClassFactory') -> ( # <-- no, no, Gods, no 'BadClassFactory'): # <------------------------------ please, Gods, no return BadClassFactory(self._args + other._args)
A PEP 563-compliant postponed type hint (i.e., type hint unparsed by from __future__ import annotations back into a string that is the unqualified name of the current class) also resolves this. The only costs are codebase-shattering inefficiency, non-deterministic fragility so profound that even Hypothesis is squinting, and the ultimate death of your business model. Only do this over the rotting corpse of beartype. This is...
# Breaking the Python interpreter: feels bad, because it is bad. # *PLEASE DON'T DO THIS ANYWHERE.* Do you want @beartype to be a shambling wreck? from __future__ import annotations from beartype import beartype @beartype class TerribadClassFactory(object): def __init__(self, *args: Sequence) -> None: self._args = args def make_class(self, other: TerribadClassFactory) -> ( # <-- NO, NO, GODS, NO TerribadClassFactory): # <------------------------------ PLEASE, GODS, NO return TerribadClassFactory(self._args + other._args)
In theory, beartype nominally supports all three. In practice, beartype only perfectly supports typing.Self. beartype still grapples with slippery edge cases in the latter two, which will blow up your test suite in that next changeset you are about to commit. Even when we perfectly support everything in a future release, you should still strongly prefer Self. Why?
Speed. It's why we're here. Let's quietly admit that to ourselves. If beartype were any slower, even fewer people would be reading this. beartype generates:
- Optimally efficient type-checking code for Self. It's literally just a trivial call to the isinstance() builtin. The same cannot be said for...
- Suboptimal type-checking code for both forward references and postponed type hints, deferring the lookup of the referenced class to call time. Although beartype caches that class after doing so, all of that incurs space and time costs you'd rather not pay at any space or time.
typing.Self: it saved our issue tracker from certain doom. Now, it will save your codebase from our issues.
...under VSCode?
Beartype fully supports VSCode out-of-the-box – especially via Pylance, Microsoft's bleeding-edge Python extension for VSCode. Chortle in your joy, corporate subscribers and academic sponsors! All the intellisense you can tab-complete and more is now within your honey-slathered paws. Why? Because...
Beartype laboriously complies with pyright, Microsoft's in-house static type-checker for Python. Pylance enables pyright as its default static type-checker. Beartype thus complies with Pylance, too.
Beartype also laboriously complies with mypy, Python's official static type-checker. VSCode users preferring mypy to pyright may switch Pylance to type-check via the former. Just:
- Install mypy.
- Install the VSCode Mypy extension.
- Open the User Settings dialog.
- Search for Type Checking Mode.
- Browse to Python › Analysis: Type Checking Mode.
- Switch the "default rule set for type checking" to off.
[image: Disabling pyright-based VSCode Pylance type-checking] [image]
Pretend that reads "off" rather than "strict". Pretend we took this screenshot.
There are tradeoffs here, because that's just how the code rolls. On:
- The one paw, pyright is significantly more performant than mypy under Pylance and supports type-checking standards currently unsupported by mypy (e.g., recursive type hints).
- The other paw, mypy supports a vast plugin architecture enabling third-party Python packages to describe dynamic runtime behaviour statically.
Beartype: we enable hard choices, so that you can make them for us.
...under [insert-IDE-name-here]?
Beartype fully complies with mypy, pyright, PEP 561, and other community standards that govern how Python is statically type-checked. Modern Integrated Development Environments (IDEs) support these standards - hopefully including your GigaChad IDE of choice.
...with type narrowing?
Beartype fully supports PEP 647-compliant type narrowing with the standard typing.TypeGuard type hint, facilitating communication between beartype and static type-checkers (e.g., mypy, pyright). In fact, beartype supports general-purpose type narrowing of all PEP-compliant type hints that are also valid types (i.e., actual classes, which not all type hints are). In fact, beartype is the first maximal type narrower. In fact, you're very tired of every sentence starting with "In fact."
The procedural beartype.door.is_bearable() function narrows the type of the passed object (which can be anything) to the passed type hint (which can be any type). Both guarantee runtime performance on the order of less than 1µs (i.e., less than one millionth of a second), preserving runtime performance and money bags.
NOTE:
Sadly, the object-oriented beartype.door.TypeHint.is_bearable() method does not support type narrowing. Only beartype.door.is_bearable() supports type narrowing. Why? Deficiencies in PEP 647 beyond the control of beartype. It's not our fault. Would @leycec lie publicly in online documentation just to make his questionable coding style superficially look better!?! Surely! </shifty_goggle_eyes>
Calling beartype.door.is_bearable() in your code enables beartype to symbiotically eliminate false positives from static type-checkers checking that code, reducing static type-checker chum that went rotten decades ago:
# Import the requisite machinery. from beartype.door import is_bearable def narrow_types_like_a_boss_with_beartype(lst: list[int | str]): ''' This function eliminates false positives from static type-checkers like mypy and pyright by narrowing types with ``is_bearable()``. Note that decorating this function with ``@beartype`` is *not* required to inform static type-checkers of type narrowing. Of course, you should still do that anyway. Trust is a fickle thing. ''' # If this list contains integers rather than strings, call another # function accepting only a list of integers. if is_bearable(lst, list[int]): # "lst" has been though a lot. Let's celebrate its courageous story. munch_on_list_of_strings(lst) # mypy/pyright: OK! # If this list contains strings rather than integers, call another # function accepting only a list of strings. elif is_bearable(lst, list[str]): # "lst": The Story of "lst." The saga of false positives ends now. munch_on_list_of_strings(lst) # mypy/pyright: OK! def munch_on_list_of_strings(lst: list[str]): ... def munch_on_list_of_integers(lst: list[int]): ...
Beartype: because you no longer care what static type-checkers think.
How do I *ONLY* type-check while running my test suite?
Your test suite uses pytest, of course. You are sane. Therefore, you're lucky! The aptly-named pytest-beartype package officially supports your valid use case.
Isolate beartype to tests today. If everything blows up, at least you can say you tried:
Install pytest-beartype:
pip3 install pytest-beartype
Enable pytest-beartype by explicitly listing the names of all packages and modules to be type-checked by beartype at test time. Either:
Pass the --beartype-packages option to the pytest command:
pytest --beartype-packages='{your_package},...,{another_package}'``
Add the beartype_packages option to your pyproject.toml file:
[tool.pytest.ini_options] beartype_packages = '{your_package},...,{another_package}'
Add the beartype_packages option to your pytest.ini file:
[pytest] beartype_packages='{your_package},...,{another_package}'
Beartype: because you like your job.
How do I *NOT* type-check something?
So. You have installed import hooks with our beartype.claw API, but those hooks are complaining about something filthy in your codebase. Now, you want beartype.claw to unsee what it saw and just quietly move along so you can finally do something productive on Monday morning for once. That coffee isn't going to drink itself. ...hopefully.
You have come to the right FAQ entry. This the common use case for temporarily blacklisting a callable or class. Prevent beartype.claw from type-checking your hidden shame by decorating the hideous callable or class with either:
The beartype.beartype() decorator configured under the no-time strategy beartype.BeartypeStrategy.O0: e.g.,
# Import the requisite machinery. from beartype import beartype, BeartypeConf, BeartypeStrategy # Dynamically create a new @nobeartype decorator disabling type-checking. nobeartype = beartype(conf=BeartypeConf(strategy=BeartypeStrategy.O0)) # Avoid type-checking *ANY* methods or attributes of this class. @nobeartype class UncheckedDangerClassIsDangerous(object): # This method raises *NO* type-checking violation despite returning a # non-"None" value. def unchecked_danger_method_is_dangerous(self) -> None: return 'This string is not "None". Sadly, nobody cares anymore.'
The PEP 484-compliant typing.no_type_check() decorator: e.g.,
# Import more requisite machinery. It is requisite. from beartype import beartype from typing import no_type_check # Avoid type-checking *ANY* methods or attributes of this class. @no_type_check class UncheckedRiskyClassRisksOurEntireHistoricalTimeline(object): # This method raises *NO* type-checking violation despite returning a # non-"None" value. def unchecked_risky_method_which_i_am_squinting_at(self) -> None: return 'This string is not "None". Why does nobody care? Why?'
For further details that may break your will to code, see also:
- The "...as Noop" subsection of our decorator documentation.
- The beartype.BeartypeStrategy.O0 enumeration member.
Why is @leycec's poorly insulated cottage in the Canadian wilderness so cold?
Not even Poło the polar bear knows.
Also, anyone else notice that this question answers itself? Anybody? No? Nobody? It is just me? </snowflakes_fall_silently>
TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Bigdata™
It's a big bear AAAAAAAAFTER all! It's a big bear AAAAAAAAFTER all! It's a big b——— *squelching sound, then blessed silence*
Beartype complies with vast swaths of Python's typing landscape and lint-filled laundry list of Python Enhancement Proposals (PEPs) – but nobody's perfect. Not even the hulking form of beartype does everything. </audience_gasps>
Let's chart exactly what beartype complies with and when beartype first did so. Introducing... Beartype's feature matrix of bloated doom! It will bore you into stunned disbelief that somebody typed all this. [1]
category | feature | partial support | full support |
Python | 3.5 | — | 0.1.0—0.3.0 |
3.6 | — | 0.1.0—0.10.4 | |
3.7 | — | 0.1.0—0.15.0 | |
3.8 | — | 0.1.0—current | |
3.9 | — | 0.3.2—current | |
3.10 | — | 0.7.0—current | |
3.11 | — | 0.12.0—current | |
3.12 | — | 0.17.0—current | |
3.13 | — | 0.19.0—current | |
PEP | 362 | none | none |
435 | 0.16.0—current | none | |
484 | 0.2.0—current | none | |
526 | — | 0.15.0—current | |
544 | — | 0.4.0—current | |
557 | 0.10.0—current | none | |
560 | — | 0.4.0—current | |
561 | — | 0.6.0—current | |
563 | 0.1.1—current | 0.7.0—current | |
570 | — | 0.10.0—current | |
572 | 0.3.0—current | 0.4.0—current | |
585 | — | 0.5.0—current | |
586 | — | 0.7.0—current | |
589 | 0.9.0—current | none | |
591 | 0.13.0—current | none | |
593 | — | 0.4.0—current | |
604 | — | 0.10.0—current | |
612 | none | none | |
613 | none | 0.18.0—current | |
621 | — | 0.15.0—current | |
646 | none | none | |
647 | — | 0.13.0—current | |
649 | none | none | |
663 | 0.16.0—current | none | |
673 | — | 0.14.0—current | |
675 | 0.14.0—current | none | |
681 | none | none | |
688 | — | 0.1.0—current | |
692 | none | none | |
695 | 0.17.0—current | none | |
698 | none | none | |
3102 | — | 0.1.0—current | |
3119 | 0.7.0—current | 0.9.0—current | |
3141 | — | 0.1.0—current | |
packaging | PyPI | 0.1.0—current | — |
Anaconda | 0.1.0—current | — | |
Arch Linux | 0.12.0—current | — | |
Gentoo Linux | 0.2.0—current | — | |
macOS Homebrew | 0.5.1—current | — | |
macOS MacPorts | 0.5.1—current | — | |
decoratable | classes | — | 0.11.0—current |
coroutines | — | 0.9.0—current | |
dataclasses | — | 0.10.0—current | |
enumerations | 0.16.0—current | none | |
functions | — | 0.1.0—current | |
generators (asynchronous) | — | 0.9.0—current | |
generators (synchronous) | — | 0.1.0—current | |
methods | — | 0.1.0—current | |
pseudo-functions (__call__()) | — | 0.13.0—current | |
hints | covariant | — | 0.1.0—current |
contravariant | none | none | |
absolute forward references | — | 0.14.0—current | |
relative forward references | — | 0.14.0—current | |
subscriptable forward references | — | 0.16.0—current | |
tuple unions | — | 0.1.0—current | |
type alias statements | 0.17.0—current | none | |
parameters | optional | — | 0.18.0—current |
keyword-only | — | 0.1.0—current | |
positional-only | — | 0.10.0—current | |
variadic keyword | none | none | |
variadic positional | — | 0.1.0—current | |
plugin APIs | __instancecheck_str__ | — | 0.17.0—current |
shell variables | ${Beartype_is_color} | — | 0.16.0—current |
static checkers | mypy | — | 0.6.0—current |
pyright | — | 0.11.0—current | |
pytype | none | none | |
Pyre | none | none | |
beartype | beartype() | — | 0.1.0—current |
BeartypeConf | — | 0.10.0—current | |
BeartypeStrategy | — | 0.10.0—current | |
beartype.abby | die_if_unbearable | — | 0.10.0—0.10.4 |
is_bearable | — | 0.10.0—0.10.4 | |
beartype.claw | beartype_all() | — | 0.15.0—current |
beartype_package() | — | 0.15.0—current | |
beartype_packages() | — | 0.15.0—current | |
beartype_this_package() | — | 0.15.0—current | |
beartyping() | — | 0.15.0—current | |
beartype.door | TypeHint | — | 0.11.0—current |
AnnotatedTypeHint | — | 0.11.0—current | |
CallableTypeHint | — | 0.11.0—current | |
LiteralTypeHint | — | 0.11.0—current | |
NewTypeTypeHint | — | 0.11.0—current | |
TypeVarTypeHint | — | 0.11.0—current | |
UnionTypeHint | — | 0.11.0—current | |
die_if_unbearable() | — | 0.11.0—current | |
is_bearable() | — | 0.11.0—current | |
is_subhint() | — | 0.11.0—current | |
beartype.peps | resolve_pep563() | — | 0.11.0—current |
beartype.typing | all | — | 0.10.0—current |
beartype.vale | Is | — | 0.7.0—current |
IsAttr | — | 0.7.0—current | |
IsEqual | — | 0.7.0—current | |
IsInstance | — | 0.10.0—current | |
IsSubclass | — | 0.9.0—current | |
builtins | None | — | 0.6.0—current |
NotImplemented | — | 0.7.1—current | |
dict | – | 0.18.0—current | |
frozenset | 0.5.0—current | none | |
list | — | 0.5.0—current | |
set | 0.5.0—current | none | |
tuple | — | 0.5.0—current | |
type | 0.5.0—current | 0.9.0—current | |
collections | ChainMap | 0.5.0—current | none |
Counter | 0.5.0—current | none | |
OrderedDict | – | 0.18.0—current | |
defaultdict | – | 0.18.0—current | |
deque | 0.5.0—current | none | |
collections.abc | AsyncGenerator | 0.5.0—current | none |
AsyncIterable | 0.5.0—current | none | |
AsyncIterator | 0.5.0—current | none | |
Awaitable | 0.5.0—current | none | |
Buffer | — | 0.1.0—current | |
ByteString | — | 0.5.0—current | |
Callable | 0.5.0—current | none | |
Collection | 0.5.0—current | none | |
Container | 0.5.0—current | none | |
Coroutine | 0.5.0—current | 0.9.0—current | |
Generator | 0.5.0—current | none | |
ItemsView | 0.5.0—current | none | |
Iterable | 0.5.0—current | none | |
Iterator | 0.5.0—current | none | |
KeysView | 0.5.0—current | none | |
Mapping | – | 0.18.0—current | |
MappingView | 0.5.0—current | none | |
MutableMapping | – | 0.18.0—current | |
MutableSequence | — | 0.5.0—current | |
MutableSet | 0.5.0—current | none | |
Reversible | 0.5.0—current | none | |
Sequence | — | 0.5.0—current | |
Set | 0.5.0—current | none | |
ValuesView | 0.5.0—current | none | |
contextlib | AbstractAsyncContextManager | 0.5.0—current | none |
AbstractContextManager | 0.5.0—current | none | |
contextmanager | — | 0.15.0—current | |
dataclasses | InitVar | — | 0.10.0—current |
dataclass | 0.10.0—current | none | |
enum | Enum | 0.16.0—current | none |
equinox | all | — | 0.17.0—current |
StrEnum | 0.16.0—current | none | |
functools | lru_cache | — | 0.15.0—current |
nuitka | all | — | 0.12.0—current |
nptyping | all | — | 0.17.0—current |
numpy.typing | numpy.typing.NDArray | — | 0.8.0—current |
os | PathLike | 0.17.0—current | none |
pandera | all | 0.13.0—current | — |
re | Match | 0.5.0—current | none |
Pattern | 0.5.0—current | none | |
sphinx | sphinx.ext.autodoc | — | 0.9.0—current |
typing | AbstractSet | 0.2.0—current | none |
Annotated | — | 0.4.0—current | |
Any | — | 0.2.0—current | |
AnyStr | 0.4.0—current | none | |
AsyncContextManager | 0.4.0—current | none | |
AsyncGenerator | 0.2.0—current | none | |
AsyncIterable | 0.2.0—current | none | |
AsyncIterator | 0.2.0—current | none | |
Awaitable | 0.2.0—current | none | |
BinaryIO | 0.4.0—current | 0.10.0—current | |
ByteString | — | 0.2.0—current | |
Callable | 0.2.0—current | none | |
ChainMap | 0.2.0—current | none | |
ClassVar | none | none | |
Collection | 0.2.0—current | none | |
Concatenate | none | none | |
Container | 0.2.0—current | none | |
ContextManager | 0.4.0—current | none | |
Coroutine | 0.2.0—current | 0.9.0—current | |
Counter | 0.2.0—current | none | |
DefaultDict | – | 0.18.0—current | |
Deque | 0.2.0—current | none | |
Dict | – | 0.18.0—current* | |
Final | 0.13.0—current | none | |
ForwardRef | 0.4.0—current | 0.16.0—current | |
FrozenSet | 0.2.0—current | none | |
Generator | 0.2.0—current | none | |
Generic | — | 0.4.0—current | |
Hashable | 0.2.0—current | none | |
IO | 0.4.0—current | 0.10.0—current | |
ItemsView | 0.2.0—current | none | |
Iterable | 0.2.0—current | none | |
Iterator | 0.2.0—current | none | |
KeysView | 0.2.0—current | none | |
List | 0.2.0—current | 0.3.0—current | |
Literal | — | 0.7.0—current | |
LiteralString | 0.14.0—current | none | |
Mapping | – | 0.18.0—current* | |
MappingView | 0.2.0—current | none | |
Match | 0.4.0—current | none | |
MutableMapping | – | 0.18.0—current | |
MutableSequence | 0.2.0—current | 0.3.0—current | |
MutableSet | 0.2.0—current | none | |
NamedTuple | 0.1.0—current | 0.12.0—current | |
NewType | — | 0.4.0—current | |
NoDefault | none | none | |
NoReturn | — | 0.4.0—current | |
Optional | — | 0.2.0—current | |
OrderedDict | – | 0.18.0—current | |
ParamSpec | none | none | |
ParamSpecArgs | none | none | |
ParamSpecKwargs | none | none | |
Pattern | 0.4.0—current | none | |
Protocol | — | 0.4.0—current | |
ReadOnly | none | none | |
Reversible | 0.2.0—current | none | |
Self | — | 0.14.0—current | |
Sequence | 0.2.0—current | 0.3.0—current | |
Set | 0.2.0—current | none | |
Sized | — | 0.2.0—current | |
SupportsAbs | — | 0.4.0—current | |
SupportsBytes | — | 0.4.0—current | |
SupportsComplex | — | 0.4.0—current | |
SupportsFloat | — | 0.4.0—current | |
SupportsIndex | — | 0.4.0—current | |
SupportsInt | — | 0.4.0—current | |
SupportsRound | — | 0.4.0—current | |
Text | — | 0.1.0—current | |
TextIO | 0.4.0—current | 0.10.0—current | |
Tuple | 0.2.0—current | 0.4.0—current | |
Type | 0.2.0—current | 0.9.0—current | |
TypeAlias | none | 0.18.0—current | |
TypeGuard | — | 0.13.0—current | |
TypedDict | 0.9.0—current | none | |
TypeVar | 0.4.0—current | none | |
TypeVarTuple | none | none | |
Union | — | 0.2.0—current | |
Unpack | none | none | |
ValuesView | 0.2.0—current | none | |
TYPE_CHECKING | — | 0.5.0—current | |
final | none | none | |
no_type_check | — | 0.5.0—current | |
override | none | none | |
typing_extensions | all attributes | — | 0.8.0—current |
weakref | ref | 0.17.0—current | none |
- [1]
They now suffer crippling RSI so that you may appear knowledgeable before colleagues.
TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Code
Let's take a deep dive into the deep end of runtime type-checking – the beartype way.
Bear with Us
Beartype Code Generation: It's All for You
- Identity Decoration
- Unconditional Identity Decoration
- Shallow Identity Decoration
- Deep Identity Decoration
Constant Decoration
- Constant Builtin Type Decoration
- Constant Non-Builtin Type Decoration
- Constant Shallow Sequence Decoration
- Constant Deep Sequence Decoration
- Constant Nested Deep Sequence Decoration
Beartype Code Generation: It's All for You
Beartype dynamically generates type-checking code unique to each class and callable decorated by the beartype.beartype() decorator. Let's bearsplain why the code beartype.beartype() generates for real-world use cases 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.beartype() and verify that beartype.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.beartype() reduces to the identity decorator when decorating unannotated callables. That's but the tip of the efficiency iceberg, though. beartype.beartype() unconditionally reduces to a noop when:
- The decorated callable is itself decorated by the PEP 484-compliant typing.no_type_check() decorator.
- The decorated callable has already been decorated by beartype.beartype().
Interpreter-wide optimization is enabled: e.g.,
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.beartype() and verify that beartype.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.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.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 typing.Any.
- typing.Optional, equivalent to typing.Optional[typing.Any], equivalent to Union[Any, type(None)]. Since any union subscripted by ignorable type hints is itself ignorable, [1] typing.Optional is shallowly ignorable as well.
- [1]
Unions are only as narrow as their widest subscripted argument. However, ignorable type hints are ignorable because they are maximally wide. Unions subscripted by ignorable arguments are thus the widest possible unions, conveying no meaningful constraints and thus themselves ignorable.
Deep Identity Decoration
Let's define a trivial function annotated by a non-trivial PEP 484-, PEP 585- and PEP 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.beartype() and verify that beartype.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.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.beartype() conditionally reduces to a noop when all type hints annotating the decorated callable are deeply ignorable. These include:
- Parametrizations of typing.Generic and typing.Protocol by type variables. Since typing.Generic, typing.Protocol, and type variables all fail to convey any meaningful constraints in and of themselves, these parametrizations are safely ignorable in all contexts.
- Calls to typing.NewType passed an ignorable type hint.
- Subscriptions of typing.Annotated whose first argument is ignorable.
- Subscriptions of typing.Optional and typing.Union by at least one ignorable argument.
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.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_get_beartype_violation( 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.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.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_get_beartype_violation() 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.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_get_beartype_violation( 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.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.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_get_beartype_violation( 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.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.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_get_beartype_violation( 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.beartype() first type-checks this parameter to be a list.
beartype.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.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_get_beartype_violation( 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.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 next section for further commentary on runtime optimization from the higher-level perspective of architecture and internal API design. Surely, it is fun.
Beartype Dev Handbook: It's Handy
Let's contribute pull requests to beartype for the good of typing. The primary maintainer of this repository is a friendly, bald, and bearded Canadian guy who guarantees that he will always be nice and congenial and promptly merge most 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.
Dev Workflow
Let's take this from the top.
- Create a GitHub user account.
- Login to GitHub with that account.
- Click the "Fork" button in the upper right-hand corner of the "beartype/beartype" repository page.
- Click the "Code" button in the upper right-hand corner of your fork page that appears.
- Copy the URL that appears.
- Open a terminal.
- Change to the desired parent directory of your local fork.
Clone your fork, replacing {URL} with the previously copied URL.
git clone {URL}
Add a new remote referring to this upstream repository.
git remote add upstream https://github.com/beartype/beartype.git
Uninstall all previously installed versions of beartype. For example, if you previously installed beartype with pip, manually uninstall beartype with pip.
pip uninstall beartype
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]
Create a new branch to isolate changes to, replacing {branch_name} with the desired name.
git checkout -b {branch_name}
- Make changes to this branch in your favourite Integrated Development Environment (IDE). Of course, this means Vim.
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 :)
Stage these changes.
git add -a
Commit these changes.
git commit
Push these changes to your remote fork.
git push
- Click the "Create pull request" button in the upper right-hand corner of your fork page.
Afterward, routinely pull upstream changes to avoid desynchronization with the "beartype/beartype" repository.
git checkout main && git pull upstream main
Moar Depth
- CAUTION:
This section is badly outdated. It's bad. Real bad. If you'd like us to revise this to actually reflect reality, just drop us a line at our issue tracker. @leycec promises satisfaction.
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):
Testing phase. In this fast first pass, each callable decorated by beartype.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.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.
Error phase. In this slow second pass, each call to a callable decorated by beartype.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.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 get_beartype_violation() 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.beartype(). This is bad. Since beartype.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:
- A new PepHintMetadata object in the existing tuple passed to the data_module.HINTS_PEP_META.extend(...) call in the existing test data submodule for this PEP residing under the "beartype_test.unit.data.hint.pep.proposal" subpackage. For example, if this is a PEP 484-compliant type hint, add that hint and associated metadata to the "beartype_test.unit.data.hint.pep.proposal.data_hintpep484" submodule.
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:
- Define a new utility submodule for this PEP residing under the "beartype._util.hint.pep.proposal" subpackage implementing general-purpose validators, testers, getters, and other ancillary utility functions required to detect and handle all type hints compliant with this PEP. For efficiency, utility functions performing iteration or other expensive operations should be memoized via our internal @callable_cached decorator.
- Define a new data utility submodule for this PEP residing under the "beartype._util.data.hint.pep.proposal" subpackage adding various signs (i.e., arbitrary objects uniquely identifying type hints compliant with this PEP) to various global variables defined by the parent "beartype._util.data.hint.pep.utilhintdatapep" submodule.
- Define a new test data submodule for this PEP residing under the "beartype_test.unit.data.hint.pep.proposal" subpackage.
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.
TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
Maths: It's Plural, Apparently
Math(s) time, people. it's happening.
Bear with Us
Beartype Timings
- Timings Overview
Timings Lower Bound
- Formulaic Formulas: They're Back in Fashion
- Function Call Overhead: The New Glass Ceiling
- Holy Balls of Flaming Dumpster Fires
- But, But... That's Not Good Enough!
- Nobody Expects the Linearithmic Time
Beartype Timings
- NOTE:
Additional timings performed by an unbiased third party employed by Cisco Systems support the claims below. Notably, beartype is substantially faster than pydantic – the most popular competing runtime type-checker – by several orders of magnitude. Yes, pydantic was Cythonized to native machine code in those timings. Believe!
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
- NOTE:
- sec = seconds.
- msec = milliseconds = 10-3 seconds.
- usec = microseconds = 10-6 seconds.
- nsec = nanoseconds = 10-9 seconds.
Timings Overview
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 upfront fixed cost of decoration, each type-checked call thereafter incurs comparatively little overhead.
Conventional runtime type checkers perform most of their work at call time. @typeguard.typechecked and similar decorators consume almost none of the time needed to first decorate and then repeatedly call a decorated function. They're back-loaded. Although the initial cost of decoration is essentially free, each type-checked call thereafter incurs significant overhead.
Timings Lower Bound
In general, @beartype adds anywhere from 1µsec (i.e., 10^{-6} seconds) in the worst case to 0.01µsec (i.e., 10^{-8} seconds) in the best case of call-time overhead to each decorated callable. This superficially seems reasonable – but is it?
Let's delve deeper.
Formulaic Formulas: They're Back in Fashion
Let's formalize how exactly we arrive at the call-time overheads above.
Given any pair of reasonably fair timings between an undecorated callable and its equivalent @beartype-decorated callable, let:
- n be the number of times (i.e., loop iterations) each callable is repetitiously called.
- γ be the total time in seconds of all calls to that undecorated callable.
- λ be the total time in seconds of all calls to that @beartype-decorated callable.
Then the call-time overhead Δ(n, γ, λ) added by @beartype to each call is:
Δ(n, γ, λ) = \tfrac{λ}{n} - \tfrac{γ}{n}
Plugging in n = 100000, γ = 0.0435s, and λ = 0.0823s from aforementioned third-party timings, we see that @beartype on average adds call-time overhead of 0.388µsec to each decorated call: e.g.,
Δ(100000, 0.0435s, 0.0823s) &= \tfrac{0.0823s}{100000} - \tfrac{0.0435s}{100000} \\
&= 3.8800000000000003 * 10^{-7}s
Again, this superficially seems reasonable – but is it? Let's delve deeper.
Function Call Overhead: The New Glass Ceiling
The added cost of calling @beartype-decorated callables is a residual artifact of the added cost of stack frames (i.e., function and method calls) in Python. The mere act of calling any pure-Python callable adds a measurable overhead – even if the body of that callable is just a noop semantically equivalent to that year I just went hard on NG+ in Persona 5: Royal. This is the minimal cost of Python function calls.
Since Python decorators almost always add at least one additional stack frame (typically as a closure call) to the call stack of each decorated call, this measurable overhead is the minimal cost of doing business with Python decorators. Even the fastest possible Python decorator necessarily pays that cost.
Our quandary thus becomes: "Is 0.01µsec to 1µsec of call-time overhead reasonable or is this sufficiently embarrassing as to bring multigenerational shame upon our entire extended family tree, including that second cousin twice-removed who never sends a kitsch greeting card featuring Santa playing with mischievous kittens at Christmas time?"
We can answer that by first inspecting the theoretical maximum efficiency for a pure-Python decorator that performs minimal work by wrapping the decorated callable with a closure that just defers to the decorated callable. This excludes the identity decorator (i.e., decorator that merely returns the decorated callable unmodified), which doesn't actually perform any work whatsoever. The fastest meaningful pure-Python decorator is thus:
def fastest_decorator(func): def fastest_wrapper(*args, **kwargs): return func(*args, **kwargs) return fastest_wrapper
Replacing @beartype with @fastest_decorator in aforementioned third-party timings then exposes the minimal cost of Python decoration – a lower bound that all Python decorators necessarily pay:
$ python3.7 <<EOF from timeit import timeit def fastest_decorator(func): def fastest_wrapper(*args, **kwargs): return func(*args, **kwargs) return fastest_wrapper @fastest_decorator def main_decorated(arg01: str="__undefined__", arg02: int=0) -> tuple: """Proof of concept code implenting bear-typed args""" assert isinstance(arg01, str) assert isinstance(arg02, int) str_len = len(arg01) + arg02 assert isinstance(str_len, int) return ("bear_bar", str_len,) def main_undecorated(arg01="__undefined__", arg02=0): """Proof of concept code implenting duck-typed args""" assert isinstance(arg01, str) assert isinstance(arg02, int) str_len = len(arg01) + arg02 assert isinstance(str_len, int) return ("duck_bar", str_len,) if __name__=="__main__": num_loops = 100000 decorated_result = timeit('main_decorated("foo", 1)', setup="from __main__ import main_decorated", number=num_loops) print("timeit decorated time: ", round(decorated_result, 4), "seconds") undecorated_result = timeit('main_undecorated("foo", 1)', setup="from __main__ import main_undecorated", number=num_loops) print("timeit undecorated time:", round(undecorated_result, 4), "seconds") EOF timeit decorated time: 0.1185 seconds timeit undecorated time: 0.0889 seconds
Again, plugging in n = 100000, γ = 0.0889s, and λ = 0.1185s from the same timings, we see that @fastest_decorator on average adds call-time overhead of 0.3µsec to each decorated call: e.g.,
Δ(100000, 0.0889s, 0.1185s) &= \tfrac{0.1185s}{100000} - \tfrac{0.0889s}{100000} \\
&= 2.959999999999998 * 10^{-7}s
Holy Balls of Flaming Dumpster Fires
We saw above that @beartype on average only adds call-time overhead of 0.388µsec to each decorated call. But 0.388µsec - 0.3µsec = 0.088µsec, so @beartype only adds 0.1µsec (generously rounding up) of additional call-time overhead above and beyond that necessarily added by the fastest possible Python decorator.
Not only is @beartype within the same order of magnitude as the fastest possible Python decorator, it's effectively indistinguishable from the fastest possible Python decorator on a per-call basis.
Of course, even a negligible time delta accumulated over 10,000 function calls becomes slightly less negligible. Still, it's pretty clear that @beartype remains the fastest possible runtime type-checker for now and all eternity. Amen.
But, But... That's Not Good Enough!
Yeah. None of us are best pleased with the performance of the official CPython interpreter anymore, are we? CPython is that geriatric old man down the street that everyone puts up with because they've seen "Up!" and he means well and he didn't really mean to beat your equally geriatric 20-year-old tomcat with a cane last week. Really, that cat had it comin'.
If @beartype still isn't ludicrously speedy enough for you under CPython, we also officially support PyPy – where you're likely to extract even more ludicrous speed.
@beartype (and every other runtime type-checker) will always be negligibly slower than hard-coded inlined runtime type-checking, thanks to the negligible (but surprisingly high) cost of Python function calls. Where this is unacceptable, PyPy is your code's new BFFL.
Nobody Expects the Linearithmic Time
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.
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:
E(T) = n \log n + \gamma n + \frac{1}{2} + O \left( \frac{1}{n} \right)
The summation \frac{1}{2} + O \left( \frac{1}{n} \right) \le 1 is negligible. While non-negligible, the term \gamma n grows significantly slower than the term n \log n. So this reduces to:
E(T) = O(n \log n)
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.,
E(S) = O(E(T)) = O(n \log n)
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.
...and that's how the QA was won: eventually.
TIP:
Feed the bear! The bear is rooting around in your refuse pile. You feel sadness.
See Also
External beartype resources include:
- This list of all open-source PyPI-hosted dependents of this package (i.e., third-party packages requiring beartype as a runtime dependency), kindly furnished by the Libraries.io package registry.
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 [1] |
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 |
- [1]
The time multliplier column approximates how much slower on average than beartype that checker is as timed by our profile suite. A time multiplier of:
- "1" means that checker is approximately as fast as beartype, which means that checker is probably beartype itself.
- "20" means that checker is approximately twenty times slower than beartype on average.
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:
- PEP 561 -- Distributing and Packaging Type Information, which requires callables to be annotated with strictly PEP-compliant type hints. Packages violating PEP 561 even once cannot be type-checked with static type checkers (e.g., mypy), unless each such violation is explicitly ignored with a checker-specific filter (e.g., with a mypy-specific inline type comment).
PEP 563 -- Postponed Evaluation of Annotations, which explicitly deprecates PEP-noncompliant type hints:
With this in mind, uses for annotations incompatible with the aforementioned PEPs [i.e., PEPs 484, 544, 557, and 560] should be considered deprecated.
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, Python's official static type checker.
- Pyre, published by Meta. ...yah.
- pyright, published by Microsoft.
- pytype, published by Google.
Let's type this.
Beartype is open-source software released under the permissive MIT license.
Beartype is financed as a purely volunteer open-source project via GitHub Sponsors, to whom our burgeoning community is eternally indebted. Without your generosity, runtime type-checking would be a shadow of its current hulking bulk. We genuflect before your selfless charity, everyone!
Prior official 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!
Beartype is the work product of volunteer enthusiasm, excess caffeine, and sleepless Wednesday evenings. These brave GitHubbers hurtled the pull request (PR) gauntlet so that you wouldn't have to:
It's a heavy weight they bear. Applaud them as they buckle under the load!
Author
Cecil Curry, et al.
Copyright
2014-2024 Beartype authors