# Install beartype.
$pip3installbeartype
# 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:frombeartype.clawimportbeartype_this_package# <-- boilerplate for victorybeartype_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:frombeartypeimportBeartypeConf# <-- this isn't your faultfrombeartype.clawimportbeartype_all,beartype_this_package# <-- you didn't sign up for thisbeartype_this_package()# <-- raise exceptions in your codebeartype_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.
# ....................{ 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.>>> frombeartypeimportbeartype# <-- eponymous import; it's eponymous# Annotate @beartype-decorated classes and callables with type hints.>>> @beartype# <-- you too will believe in magic... defquote_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_violationraiseexception_cls(beartype.roar.BeartypeCallHintParamViolation: @beartypedquote_wiggum() parameter lines=[b'Oh, my God! A horrible planecrash!', b'Hey, everybody! Get a load of thi...'] violates type hintlist[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.>>> frombeartype.valeimportIs# <---- validator factory>>> fromtypingimportAnnotated# <---------------- 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[lambdalst:bool(lst)]# <-- lambda matching non-empty object... ]# Annotate @beartype-decorated callables with validators.>>> @beartype... defquote_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: @beartypedquote_wiggum_safer() parameter lines=[] violates type hinttyping.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.>>> frombeartype.doorimport(... 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.>>> frombeartype.doorimportis_bearable>>> fromnumpyimportarray,ndarray>>> data=[(array(i),array(i))foriinrange(1000000)]>>> %timeis_bearable(data,list[tuple[ndarray,ndarray]]) CPU times: user 31 µs, sys: 2 µs, total: 33 µs Wall time: 36.7 µsTrue
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.
pip3installbeartype
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."
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.
If you're feeling the quality assurance and want to celebrate, consider
signaling that you're now publicly bear-ified:
YummySoft is now !
All this magic and possibly more can be yours with:
Markdown:
YummySoft is now [](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 <ahref="https://beartype.readthedocs.io"><imgsrc="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.
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.frombeartypeimportbeartype# 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.frombeartypeimporttyping# 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.htmlimporttyping# Or, directly import PEP 585 type hints. Note this requires Python ≥ 3.9.fromcollectionsimportabc# 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).importtyping_extensions# Import beartype-specific types to annotate callables with.frombeartype.caveimportNoneType,NoneTypeOr,RegexTypes,ScalarTypes# Import official abstract base classes (ABCs), too.fromnumbersimportIntegral,Real# Import user-defined classes, too.frommy_package.my_moduleimportMyClass# ..................{ 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...@beartypedefmy_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.htmlparam_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]:return0xDEADBEEF# Decorate coroutines as above but returning a coroutine type.@beartypeasyncdefmy_coroutine()->abc.Coroutine[None,None,int]:fromasyncimportsleepawaitsleep(0)return0xDEFECA7E# ..................{ GENERATORS }..................# Decorate synchronous generators as above but returning a synchronous# generator type.@beartypedefmy_sync_generator()->abc.Generator[int,None,None]:yield fromrange(0xBEEFBABE,0xCAFEBABE)# Decorate asynchronous generators as above but returning an asynchronous# generator type.@beartypeasyncdefmy_async_generator()->abc.AsyncGenerator[int,None]:fromasyncimportsleepawaitsleep(0)yield0x8BADF00D# ..................{ CLASSES }..................# Decorate classes with @beartype – which then automatically decorates all# methods and properties of those classes with @beartype.@beartypeclassMyOtherClass:# 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".@classmethoddefmy_classmethod(cls,regex:RegexTypes,wut:str)->(Callable[(),str]):importrereturnlambda:re.sub(regex,'unbearable',str(cls._scalar)+wut)# Annotate static methods as above, too.@staticmethoddefmy_staticmethod(callable:abc.Callable[[str],T],text:str)->T:returncallable(text)# Annotate property getter methods as above, too.@propertydefmy_gettermethod(self)->abc.Iterator[int]:returnrange(0x0B00B135+int(self._scalar),0xB16B00B5)# Annotate property setter methods as above, too.@my_gettermethod.setterdefmy_settermethod(self,bad:Integral=0xBAAAAAAD)->None:self._scalar=badifbadelse0xBADDCAFE# Annotate methods accepting or returning instances of the class# currently being declared with relative forward references.defmy_selfreferential_method(self)->list['MyOtherClass']:return[self]*42# ..................{ GENERICS }..................# Decorate PEP 585 generics with @beartype. Note this requires Python ≥ 3.9.@beartypeclassMyPep585Generic(tuple[int,float]):def__new__(cls,integer:int,real:float)->tuple[int,float]:returntuple.__new__(cls,(integer,real))# Decorate PEP 484 generics with @beartype, too.@beartypeclassMyPep484Generic(typing.Tuple[str,...]):def__new__(cls,*args:str)->typing.Tuple[str,...]:returntuple.__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.classMyProtocol(typing.Protocol):defmy_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.fromdataclassesimportdataclass,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@dataclassclassMyDataclass(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:ifself.field_must_satisfy_pep604_unionisNone:self.field_must_satisfy_pep604_union=(field_must_satisfy_builtin_type)# ..................{ NAMED TUPLES }..................# Import the requisite machinery.fromtypingimportNamedTuple# Decorate named tuples with @beartype.@beartypeclassMyNamedTuple(NamedTuple):# Annotate fields with type hints.field_must_satisfy_builtin_type:str# ..................{ CONFIGURATION }..................# Import beartype's configuration API to configure runtime type-checking.frombeartypeimportBeartypeConf,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.@bugbeartypedefmuh_configured_func(list_checked_in_On_time:list[float])->set[str]:returnset(str(item)foriteminlist_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.frombeartype.valeimportIs,IsAttr,IsEqualfromtypingimportAnnotated# <--------------- if Python ≥ 3.9.0#from typing_extensions import Annotated # <--- if Python < 3.9.0# Import third-party packages to validate.importnumpyasnp# Validator matching only two-dimensional NumPy arrays of 64-bit floats,# specified with a single caller-defined lambda function.NumpyArray2DFloat=Annotated[np.ndarray,Is[lambdaarr:arr.ndim==2andarr.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...@beartypedefmy_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]*0xFACEFEEDifbool(param_must_satisfy_validator)else[np.array([i],np.dtype=np.float64)foriinrange(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.fromnumpy.typingimportNDArray# 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...@beartypedefmy_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))
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 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>
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:
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:
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.
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.
Decorate any annotated function with that decorator:
fromsysimportstderr,stdoutfromtypingimportTextIO@beartypedefhello_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:
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() parametersep=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 seena byte-string separator before?" not str.
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:
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() parametershape=([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.
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:
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.,
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 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:
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.''',... })>>> formaximininform_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:
>>> formaximininform_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 hailthem 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.
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:
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 <protocolABC "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.
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
orNone and returning either an enumeration member orNone:
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:
>>> fromenumimportEnum>>> classLukannon(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:
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.Unionor 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:
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.
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]
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 }....................frombeartype.clawimportbeartype_this_package# <-- this is boring, but...beartype_this_package()# <-- the fast way# ....................{ THE LESS FAST WAY }....................frombeartype.clawimportbeartype_package# <-- still boring, but...beartype_package('{your_package}')# <-- the less fast way# ....................{ THE MORE SLOW WAY }....................frombeartype.clawimportbeartype_packages# <-- boring intensifiesbeartype_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.
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!
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 everyPEP 526-compliant annotated variable
assignment (e.g., muh_int:int='Prettysurethisisn'taninteger,butnotsure.') 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.importtypingastbad_global:int='My eyes! The goggles do nothing.'# <-- no exceptiondefbad_function()->str:returnb"I could've been somebody, instead of a bum byte string."bad_function()# <-- no exceptionclassBadClass(object):defbad_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.frombeartypeimportbeartypefrombeartype.doorimportdie_if_unbearableimporttypingastbad_global:int='My eyes! The goggles do nothing.'die_if_unbearable(bad_global,int)# <-- raises exception@beartypedefbad_function()->str:returnb"I could've been somebody, instead of a bum byte string."bad_function()# <-- raises exception@beartypeclassBadClass(object):defbad_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!
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. gitblame 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.
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 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.
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:frombeartypeimportBeartypeConf# <-- boilerplatefrombeartype.clawimportbeartype_this_package# <-- boilerplate: the revengebeartype_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.
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:frombeartypeimportBeartypeConf# <-- <Ctrl-c> <Ctrl-v>frombeartype.clawimportbeartype_package# <-- <Ctrl-c> <Ctrl-v> x 2beartype_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.
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:frombeartypeimportBeartypeConf# <-- copy-pastafrombeartype.clawimportbeartype_packages# <-- copy-pasta intensifiesbeartype_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.
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,frombeartypeimportBeartypeConf# <-- @beartype seemed so innocent, oncefrombeartype.clawimportbeartype_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-stackruntime-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.
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"):frombeartypeimportBeartypeConf# <-- it all seems so familiarfrombeartype.clawimportbeartype_package# <-- boil it up, boilerplatebeartype_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='Prettysurethisisn'taninteger.'. 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.frombeartypeimportBeartypeConf# <-- boiling boilerplate...frombeartype.clawimportbeartype_this_package# <-- ...ain't even lukewarmbeartype_this_package(conf=BeartypeConf(warning_cls_on_decorator_exception=None))# <-- *ohboy*
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.
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.>>> frombeartypeimportbeartype# Decorate a function with @beartype.>>> @beartype... defbother_free_is_no_bother_to_me(bothersome_string:str)->str:... returnf'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?' violatestype hint <class 'str'>, as bytes b'Could you spare a small smackerel?' notinstance 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.>>> frombeartypeimportbeartype# Decorate class methods with @beartype in either order.>>> classBlastItAll(object):... @classmethod... @beartype# <-- GOOD. this is the best of all possible worlds.... defgood_idea(cls,we_will_dynamite:str)->str:... returnwe_will_dynamite...... @beartype# <-- BAD. technically, fine. pragmatically, slower.... @classmethod... defsave_time(cls,whats_the_charge:str)->str:... returnwhats_the_charge
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.>>> frombeartypeimportbeartype# A function somebody else defined. Note the bad lack of @beartype.>>> defoh_bother_free_where_art_thou(botherfull_string:str)->str:... returnf'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.
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 strategyBeartypeStrategy.O0: e.g.,
# Import the requisite machinery.frombeartypeimportbeartype,BeartypeConf,BeartypeStrategy# Avoid type-checking *ANY* methods or attributes of this class.@beartype(conf=BeartypeConf(strategy=BeartypeStrategy.O0))classUncheckedDangerClassIsDangerous(object):# This method raises *NO* type-checking violation despite returning a# non-"None" value.defunchecked_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 PEP 484-compliant typing.no_type_check() decorator: e.g.,
# Import more requisite machinery. It is requisite.frombeartypeimportbeartypefromtypingimportno_type_check# Avoid type-checking *ANY* methods or attributes of this class.@no_type_checkclassUncheckedRiskyClassRisksOurEntireHistoricalTimeline(object):# This method raises *NO* type-checking violation despite returning a# non-"None" value.defunchecked_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).
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.
Superficially, this is just syntactic sugar – but sometimes you gotta dip your
paws into the honey pot.
# Import the requisite machinery.frombeartypeimportbeartype# Decorate a class with @beartype.@beartypeclassIAmABearOfNoBrainAtAll(object):defi_have_been_foolish(self)->str:return'A fly can'tbird,butabirdcanfly.'defand_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.classIAmABearOfNoBrainAtAll(object):@beartypedefi_have_been_foolish(self)->str:return'A fly can'tbird,butabirdcanfly.'@beartypedefand_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.frombeartypeimportbeartypefromdataclassesimportdataclass# 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@dataclassclassSoTheyWentOffTogether(object):a_little_boy_and_his_bear:str|byteswill_always_be_playing:str|None=None
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.frombeartypeimportbeartype,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.@monotowertypedefmuh_colorless_permissive_func(int_or_float:float)->float:returnint_or_float**int_or_float^round(int_or_float)
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.
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:
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.
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.>>> frombeartypeimportbeartype,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... defmuh_bugged_func()->str:... returnb'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
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.frombeartypeimportbeartype,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.@beartowertypedefcrunch_numbers(numbers:list[float])->float:returnsum(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])
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 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):
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.frombeartypeimportbeartype,BeartypeConf# Dynamically create a new @monobeartype decorator disabling colour.monobeartype=beartype(conf=BeartypeConf(is_color=False))# Decorate with this decorator rather than @beartype everywhere.@monobeartypedefmuh_colorless_func()->str:returnb'In the kingdom of the blind, you are now king.'
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).
# Import the requisite machinery.frombeartypeimportbeartype,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.@bslowmobeartypedeftype_check_like_maple_syrup(liquid_gold:list[int])->str:return'The slowest noop yet envisioned? You'renotwrong.'
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.
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.
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.
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.
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:
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.frombeartypeimportbeartype,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...@beartypeclassTypeCheckedClass(object):# Including this method, which raises a type-checking violation# due to returning a non-"None" value.deftype_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.@nobeartypedefnon_type_checked_method(self)->None:return'This string is not "None". Thankfully, no one cares.'
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.frombeartypeimportbeartype,BeartypeConf,BeartypeStrategy# Let us pretend you know what you are doing for a hot moment.fromyour_appimportI_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.O0ifI_AM_RELEASE_BUILDelseBeartypeStrategy.O1))# Decorate with this decorator rather than @beartype everywhere.@maybebeartypedefmuh_performance_critical_func(big_list:list[int])->int:returnsum(big_list)
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.
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!
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.frombeartypeimportbeartypefrombeartype.valeimportIsfromtypingimportAnnotated# <--------------- 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!importnumpyasnpNumpy2DFloatArray=Annotated[np.ndarray,Is[lambdaarray:array.ndim==2andnp.issubdtype(array.dtype,np.floating)]]# Annotate @beartype-decorated callables with beartype validators.@beartypedefpolygon_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)returnnp.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.
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:
The functional validator Annotated[np.ndarray,Is[lambdaarray:array.dtype.type==np.float64]] also detects the same arrays by generating
a slightly slower inline expression calling the lambda function you define:
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.
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.frombeartype.valeimportIsfromtypingimportAnnotated# <--------------- 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[lambdatext: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.
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.frombeartype.valeimportIsAttr,IsEqualfromtypingimportAnnotated# <--------------- 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 == 2importnumpyasnpNumpy2DArray=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.float64NumpyFloat64Array=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.
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.frombeartype.valeimportIsEqualfromtypingimportAnnotated# <--------------- 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:
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.
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.frombeartype.valeimportIsInstancefromtypingimportAnnotated# <--------------- 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.
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.frombeartype.valeimportIsSubclassfromtypingimportAnnotated# <--------------- 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.
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[lambdatext:'.'intext]]
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[lambdatext:bool(text)]&~Is[lambdatext:'.'intext])]
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[lambdatext:bool(text)]|Is[lambdatext:'.'intext])]
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[lambdatext:bool(text)],~Is[lambdatext:'.'intext],]
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[lambdatext:b'.'intext]],Annotated[str,Is[lambdatext:u'.'intext]],]
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.9ortyping_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.
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!
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.frombeartypeimportbeartypefrombeartype.valeimportIsfromtypingimportAnnotated# <--------------- 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[lambdalst:all(isinstance(item,int)foriteminlst)]]# Type-check all integers in a list of integers in O(n) time. How could you?@beartypedefsum_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. '''returnsum(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.
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.frombeartypeimportbeartypefrombeartype.valeimportIsfromtypingimportAnnotated# <--------------- 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[lambdatext:len(text)>=80]# Validator matching only strings suffixed by a period.IsSentence=Is[lambdatext:textandtext[-1]=='.']# Validator matching only single- or double-quoted strings.def_is_quoted(text):returntext.count('"')>=2ortext.count("'")>=2IsQuoted=Is[_is_quoted]# Combine multiple validators by just listing them sequentially.@beartypedefdesentence_lengthy_quoted_sentence(text:Annotated[str,IsLengthy,IsSentence,IsQuoted]])->str:''' Strip the suffixing period from a lengthy quoted sentence... 'cause. '''returntext[:-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.@beartypedefdesentence_lengthy_quoted_sentence_part_deux(text:Annotated[str,IsLengthy&IsSentence&IsQuoted]])->str:''' Strip the suffixing period from a lengthy quoted sentence... again. '''returntext[:-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.)@beartypedefstrip_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. '''returntext[:-1]# this is frankly outrageous
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
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.14154.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>>> fromcollections.abcimportSequence>>> isinstance("Ph'nglui mglw'nafh Cthu—"[0][0][0][0][0],Sequence)True # <-- ...curse you, curse you to heck and back
When we annotate a callable as accepting an int, we never want that
callable to also silently accept a bool. Likewise, when we annotate
another callable as accepting a Sequence[Any] or Sequence[str], we
never want that callable to also silently accept a str. These are
sensible expectations – just not in Python, where madness prevails.
To resolve these counter-intuitive concerns, we need the equivalent of the
relative set complement (or difference). We now
call this thing... type elision! Sounds pretty hot, right? We know.
Let's first validate non-boolean integers with a beartype validator
effectively declaring a new int-bool class (i.e., the subclass of all
integers that are not booleans):
# Import the requisite machinery.frombeartypeimportbeartypefrombeartype.valeimportIsInstancefromtypingimportAnnotated# <--------------- 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.@beartypedefsum_ints(*args:IntNonbool)->IntNonbool:''' I cast thee out, mangy booleans! You plague these shores no more. '''returnsum(args)
Let's next validate non-string sequences with beartype validators
effectively declaring a new Sequence-str class (i.e., the subclass of all
sequences that are not strings):
# Import the requisite machinery.frombeartypeimportbeartypefrombeartype.valeimportIsInstancefromcollections.abcimportSequencefromtypingimportAnnotated# <--------------- 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.)@beartypedefjoin_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.@beartypedefjoin_strs(my_sequence:SequenceNonstrOfStr)->str:''' I expectorate thee up, sequence of strings. '''return'\n'.join(my_sequence)# <-- do *NOT* do this to a string
# Import the requisite machinery.frombeartypeimportbeartypefrombeartype.valeimportIsAttr,IsEqual,IsSubclassfromtypingimportAnnotated# <--------------- 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.)importnumpyasnpNumpy2DFloatArray=Annotated[np.ndarray,IsAttr['ndim',IsEqual[2]]&IsAttr['dtype',IsAttr['type',IsSubclass[np.floating]]]]# Annotate @beartype-decorated callables with beartype validators.@beartypedefpolygon_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)returnnp.abs(0.5*np.sum(polygon[:,0]*polygon_rolled[:,1]-polygon_rolled[:,0]*polygon[:,1]))
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:
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.frombeartypeimportbeartypefrombeartype.valeimportIsAttr,IsEqualfromnumpyimportfloatingfromnumpy.typingimportNDArrayfromtypingimportAnnotated# <--------------- 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]]]
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.
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.frombeartypeimportbeartypefromnumpyimportdtypefromnumpy.typingimportNDArray# 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')]
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.frombeartypeimportbeartypefromnumpyimportfloat64fromnumpy.typingimportNDArray# 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]
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.frombeartypeimportbeartypefromnumpyimportfloatingfromnumpy.typingimportNDArray# 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.
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.
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.
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.>>> frombeartype.doorimportdie_if_unbearable>>> frombeartype.typingimportList,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 typehint 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.
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.>>> frombeartype.doorimportis_bearable>>> frombeartype.typingimportList,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.>>> frombeartype.doorimportis_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.>>> frombeartype.doorimportis_bearable>>> frombeartype.typingimportType>>> fromcollections.abcimportAwaitable,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
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.>>> frombeartype.doorimportis_subhint>>> frombeartype.typingimportAny>>> fromcollections.abcimportCallable,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.
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.
frombeartypeimportbeartypefrombeartype.doorimportis_subhintfrombeartype.pepsimportresolve_pep563fromcollections.abcimportCallable@beartypedefis_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...forfunc_arg_name,func_new_hintinfunc_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.iffunc_old_hintisNone: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.ifnotis_subhint(func_old_hint,func_new_hint):returnFalse# 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.returnTrue
The proof is in the real-world pudding.
>>> fromnumbersimportReal# New and successively older APIs of the same example function.>>> defnew_func(text:str|None,ints:list[Real])->int:...>>> defold_func(text:str,ints:list[int])->bool:...>>> defolder_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().
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.
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.>>> frombeartype.doorimportTypeHint# 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.>>> frombeartype.doorimportUnionTypeHint>>> 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 cannotbe named.' violates type hint int | str | None, as bytes b'The @beartypethat cannot be named.' not str, <class "builtins.NoneType">, or int.# DOOR hints can be iterated Pythonically -- unlike normal type hints.>>> forchild_hintinunion_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)inunion_hint# <-- it's all true.True>>> TypeHint(bool)inunion_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.>>> ifunion_hint:print('This type hint has children.')This type hint has children.>>> ifnotTypeHint(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.>>> fromtypingimportUnion>>> 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.hintint | 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)isTypeHint(None|bool|str|int)True # <-- blowing minds over here.
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
subhintrelation) 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*”
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.
>>> frombeartype.doorimportTypeHint>>> frombeartype.typingimportOptional,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.
>>> frombeartype.doorimportTypeHint>>> TypeHint(list[int])isTypeHint(list[int])True # <-- you caching monster. how could you? we trusted you!
TypeHint wrappers expose these public read-only properties:
Tuple of the zero or more original child type hints subscripting the
original type hint wrapped by this wrapper.
>>> frombeartype.doorimportTypeHint>>> 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.
>>> frombeartype.doorimportTypeHint>>> 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.
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.
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.,
>>> fromtypingasProtocol>>> 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.
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...>>> frombeartype.doorimportTypeHint>>> TypeHint(bytes|None).die_if_unbearable(... "You can't lose hope when it's hopeless.")BeartypeDoorHintViolation: Object "You can't lose hope when it'shopeless." violates type hint bytes | None, as str "You can't losehope when it's hopeless." not bytes or <class "builtins.NoneType">.# ...is equivalent to this procedural approach.>>> frombeartype.doorimportdie_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'shopeless." violates type hint bytes | None, as str "You can't losehope when it's hopeless." not bytes or <class "builtins.NoneType">.
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...>>> frombeartype.doorimportTypeHint>>> 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.>>> frombeartype.doorimportis_bearable>>> is_bearable(... obj="It's like a party in my mouth and everyone's throwing up.",... hint=int|float,... )False
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...>>> frombeartype.doorimportTypeHint>>> TypeHint(tuple[bool]).is_subhint(tuple[int])True# ...is equivalent to this procedural approach.>>> frombeartype.doorimportis_subhint>>> is_subhint(subhint=tuple[bool],superhint=tuple[int])True
...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.
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.
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.
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).
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.
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:
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.
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.
>>> frombeartypeimportbeartype>>> frombeartype.roarimportBeartypeCallHintForwardRefException>>> @beartype... defi_am_spirit_bear(favourite_foodstuff:'salmon.of.course')->None:pass>>> try:... i_am_spirit_bear('Why do you eat all my salmon, Spirit Bear?')... exceptBeartypeCallHintForwardRefExceptionasexception:... print(exception)Forward reference "salmon.of.course" unimportable.
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:
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:
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.frombeartypeimportbeartypefrombeartype.roarimportBeartypeCallHintViolation# Arbitrary user-defined classes.classSpiritBearIGiveYouSalmonToGoAway(object):passclassSpiritBearIGiftYouHoneyNotToStay(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.@beartypedefwhen_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"!exceptBeartypeCallHintViolationasviolation:# Assert that one culprit was responsible for this violation.assertlen(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.assertculpritisSPIRIT_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.frombeartypeimportbeartypefrombeartype.roarimportBeartypeCallHintViolationfrombeartype.typingimportList# Callable annotated to accept a standard container.@beartypedefwe_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!exceptBeartypeCallHintViolationasviolation:# Assert that two culprits were responsible for this violation.assertlen(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.assertroot_culprit==repr(SPIRIT_BEAR_DO_AS_HE_PLEASE)assertleaf_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.
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.
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.
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 frombeartype.typinginstead. 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...frombeartype.typingimportDict,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...frombeartypeimporttyping# ...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:
beartype.typing.Setisset under Python ≥ 3.9 for PEP 585
compliance.
beartype.typing.Setistyping.Set under Python < 3.9 for PEP 484
compliance.
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.frombeartype.roarimportBeartypeDecorHintPep585DeprecationWarningfromwarningsimportfilterwarningsfilterwarnings("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.frombeartype.roarimportBeartypeDecorHintPep585DeprecationWarningfromwarningsimportcatch_warnings,filterwarningswithcatch_warnings():filterwarnings("ignore",category=BeartypeDecorHintPep585DeprecationWarning)...
Type aliases. The hardest (but best) solution is to use type aliases
to conditionally annotate callables with either PEP 484orPEP 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":fromsysimportversion_infoifversion_info>=(3,9):List=listTuple=tuple...else:fromtypingimportList,Tuple,...
Then globally refactor all deprecated PEP 484 imports from typing
to {your_package}._typing instead:
# Instead of this...fromtypingimportList,Tuple# ...just do this.from{your_package}._typingimportList,Tuple
What could be simpler? ...gagging noises faintly heard
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.
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)!
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.
You know how in low-level statically-typedmemory-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?
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-<<EOLdef 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.
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 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.
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?
Yes, 6.42e+03secperloop==6420seconds==107minutes==1hour,47minutes 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.
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."
Yes, 13.8usecperloop==13.8microseconds=0.0000138seconds 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.
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.
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 averageall 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.
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:
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.
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.>>> frombeartypeimportbeartype>>> defsum_list_unbeartyped(some_list:list)->int:... returnsum(some_list)>>> sum_list_beartyped=beartype(sum_list_unbeartyped)>>> %timesum_list_unbeartyped([42]*0xACEBABE)CPU times: user 3.15 s, sys: 418 ms, total: 3.57 sWall time: 3.58 s # <-- okay.Out[20]: 7615456044>>> %timesum_list_beartyped([42]*0xACEBABE)CPU times: user 3.11 s, sys: 440 ms, total: 3.55 sWall 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 notreal-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.
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.
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 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.
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-compliantbeartypevalidators, 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.
# Import the requisite machinery.frombeartypeimportbeartypefromboto3importresourcefromboto3.resources.baseimportServiceResourcefromtypingimportTYPE_CHECKING# If performing static type-checking (e.g., mypy, pyright), import boto3# stub types safely usable *ONLY* by static type-checkers.ifTYPE_CHECKING:frommypy_boto3_s3.service_resourceimportBucket# 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!frombeartype.valeimportIsAttr,IsEqualfromtypingimportAnnotated# <--------------- 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.@beartypedefget_s3_bucket_example()->Bucket:s3=resource('s3')returns3.Bucket('example')
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.
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.
You mind adding an additional mandatory runtime dependency to your app. In
this case, prefer beartypevalidators. 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.frombeartypeimportbeartypefrombeartype.valeimportIsfromtypingimportAnnotated# <--------------- if Python ≥ 3.9.0# from typing_extensions import Annotated # <-- if Python < 3.9.0# Import PyTorch (d)types of interest.fromtorchimport(floatastorch_float,intastorch_int,tensor,)# PEP-compliant type hint matching only a floating-point PyTorch tensor.TorchTensorFloat=Annotated[tensor,Is[lambdatens:tens.type()istorch_float]]# PEP-compliant type hint matching only an integral PyTorch tensor.TorchTensorInt=Annotated[tensor,Is[lambdatens:tens.type()istorch_int]]# Type-check everything like an NLP babelfish.@beartypedefdeep_dream(dreamy_tensor:TorchTensorFloat)->TorchTensorInt:returndreamy_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 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.,
Type-check anypandas 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 allpandera 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.importpandasaspdimportpanderaaspafrombeartypeimportbeartypefrompandera.dtypesimportInt64,String,Timestampfrompandera.typingimportSeries# 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.classMuhDataFrameModel(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_typesdefconvert_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]ifisinstance(column_name_or_index,str)elsedataframe.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: objectprint('[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@beartypedefconvert_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.
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.defbearpanderatype(*args,**kwargs):returnbeartype(pa.check_types(*args,**kwargs))# Knowledge is power. Clench it with your iron fist until it pops.@bearpanderatype# <-- less boilerplate means more powerdefconvert_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
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:
The ClassFactory.make_class() method both accepts a parameter other
whose type is ClassFactoryand 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.frombeartypeimportbeartypefromtypingimportSelf# <---------------- 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.classClassFactory(object):def__init__(self,*args:Sequence)->None:self._args=args# @beartype # <-- No... Oh, Gods. *NO*! The dark side grows stronger.defmake_class(self,other:Self)->Self:# <-- We are all one self.returnClassFactory(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: Selfcannot 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?frombeartypeimportbeartype@beartypeclassBadClassFactory(object):def__init__(self,*args:Sequence)->None:self._args=argsdefmake_class(self,other:'BadClassFactory')->(# <-- no, no, Gods, no'BadClassFactory'):# <------------------------------ please, Gods, noreturnBadClassFactory(self._args+other._args)
A PEP 563-compliant postponed type hint (i.e., type hint unparsed by
from__future__importannotations 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__importannotationsfrombeartypeimportbeartype@beartypeclassTerribadClassFactory(object):def__init__(self,*args:Sequence)->None:self._args=argsdefmake_class(self,other:TerribadClassFactory)->(# <-- NO, NO, GODS, NOTerribadClassFactory):# <------------------------------ PLEASE, GODS, NOreturnTerribadClassFactory(self._args+other._args)
In theory, beartype nominally supports all three. In practice,
beartype only perfectly supports typing.Self. beartypestill 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.
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:
Switch the "default rule set for type checking" to off.
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.
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.
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.
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.frombeartype.doorimportis_bearabledefnarrow_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.ifis_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.elifis_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!defmunch_on_list_of_strings(lst:list[str]):...defmunch_on_list_of_integers(lst:list[int]):...
Beartype: because you no longer care what static type-checkers think.
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:
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:
# Import the requisite machinery.frombeartypeimportbeartype,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.@nobeartypeclassUncheckedDangerClassIsDangerous(object):# This method raises *NO* type-checking violation despite returning a# non-"None" value.defunchecked_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.frombeartypeimportbeartypefromtypingimportno_type_check# Avoid type-checking *ANY* methods or attributes of this class.@no_type_checkclassUncheckedRiskyClassRisksOurEntireHistoricalTimeline(object):# This method raises *NO* type-checking violation despite returning a# non-"None" value.defunchecked_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:
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]
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.
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.,
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.
Let's decorate that function by beartype.beartype() and verify that
beartype.beartype() reduced to the identity decorator by returning that
function unmodified:
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.
Again, let's decorate that function by beartype.beartype() and verify that
beartype.beartype() reduced to the identity decorator by returning that
function unmodified:
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.
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:
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:
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.
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.
Let's see the wrapper function beartype.beartype() dynamically generated
from that:
deflaw_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>0elsekwargs.get('a_cub_may_be_bought_at_a_price',__beartypistry))# If this parameter was passed...if__beartype_pith_0isnot__beartypistry:# Type-check this passed parameter or return value against this# PEP-compliant type hint.ifnotisinstance(__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:
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.
Let's see the wrapper function beartype.beartype() dynamically generated
from that:
deflaw_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>0elsekwargs.get('all_the_jungle_is_thine',__beartypistry))# If this parameter was passed...if__beartype_pith_0isnot__beartypistry:# Type-check this passed parameter or return value against this# PEP-compliant type hint.ifnotisinstance(__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:
Let's see the wrapper function beartype.beartype() dynamically generated
from that:
deflaw_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>0elsekwargs.get('kill_everything_that_thou_canst',__beartypistry))# If this parameter was passed...if__beartype_pith_0isnot__beartypistry:# Type-check this passed parameter or return value against this# PEP-compliant type hint.ifnot(# 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_0orisinstance(__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.
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.
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:
Let's see the wrapper function beartype.beartype() dynamically generated
from that:
deflaw_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>0elsekwargs.get('pull_thorns_from_all_wolves_paws',__beartypistry))# If this parameter was passed...if__beartype_pith_0isnot__beartypistry:# Type-check this passed parameter or return value against this# PEP-compliant type hint.ifnot(# 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_0or(# 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_1or(# 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_2orisinstance(__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.
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.
Uninstall all previously installed versions of beartype. For
example, if you previously installed beartype with pip, manually
uninstall beartype with pip.
pipuninstallbeartype
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.
pip3install-e.[dev]
Create a new branch to isolate changes to, replacing {branch_name}
with the desired name.
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:
So, you want to help beartype comply with even morePython 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.
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.
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 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.,
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:
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:
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.,
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.
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 @beartypestill 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.
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.
\[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.
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:
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).
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 (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:
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:
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!