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