Skip to main content

Unbearably fast runtime type checking in pure Python.

Project description

GitHub Actions status

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

                        — The Jungle Book.

Beartype is an open-source pure-Python runtime type checker emphasizing efficiency, portability, and thrilling puns.

Unlike comparable static type checkers operating at the coarse-grained application level (e.g., Pyre, mypy, pyright, pytype), 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 CPython, PyPy, Numba, and Nuitka.

Unlike comparable runtime type checkers (e.g., pytypes, typeguard), beartype wraps each decorated callable with a dynamically generated wrapper efficiently type-checking that specific callable. Since “performance by default” is our first-class concern, all wrappers are guaranteed to:

  • Exhibit O(1) time complexity with negligible constant factors.

  • Be either more efficient (in the common case) or exactly as efficient minus the cost of an additional stack frame (in the worst case) as equivalent type-checking implemented by hand.

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

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


Installation

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

pip3 install beartype

Let’s install beartype with Anaconda, because corporate standards are sometimes good, too:

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

Cheatsheet

Let’s type-check like greased lightning:

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

# Import generic types for use with @beartype.
from beartype.cave import (
    AnyType,
    BoolType,
    FunctionTypes,
    CallableTypes,
    GeneratorType,
    IntOrFloatType,
    IntType,
    IterableType,
    IteratorType,
    NoneType,
    NoneTypeOr,
    NumberType,
    RegexTypes,
    ScalarTypes,
    SequenceType,
    StrType,
    VersionTypes,
)

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

# Decorate functions with @beartype and...
@beartype
def bare_necessities(
    # Annotate builtin types as is, delimited by a colon (":" character).
    param1_must_be_of_builtin_type: str,

    # Annotate user-defined classes as is, too.
    param2_must_be_of_user_type: MyClass,

    # Annotate generic types predefined by the beartype cave.
    param3_must_be_of_generic_type: NumberType,

    # Annotate forward references dynamically resolved (and cached) at first
    # call time as fully-qualified "."-delimited classnames.
    param4_must_be_of_forward_type: 'my_package.my_module.MyClass',

    # Annotate unions of types as tuples. In PEP 484, this is:
    # param5_may_be_any_of_several_types: typing.Union[dict, MyClass, int,],
    param5_may_be_any_of_several_types: (dict, MyClass, int,),

    # Annotate generic unions of types predefined by the beartype cave.
    param6_may_be_any_of_several_generic_types: CallableTypes,

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

    # Annotate unions of types as tuples concatenated together.
    param8_may_be_any_of_several_concatenated_types: (
        IteratorType,) + ScalarTypes,

    # Annotate optional types by indexing "NoneTypeOr" with those types. In
    # PEP 484, this is:
    # param9_must_be_of_type_if_passed: typing.Optional[float] = None,
    param9_must_be_of_type_if_passed: NoneTypeOr[float] = None,

    # Annotate optional unions of types by indexing "NoneTypeOr" with tuples
    # of those types. In PEP 484, this is:
    # param10_may_be_of_several_types_if_passed: typing.Optional[float, int] = None,
    param10_may_be_of_several_types_if_passed: NoneTypeOr[(float, int)] = None,

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

    # Annotate keyword-only arguments as above, too.
    paramN_must_be_passed_by_keyword_only: SequenceType,
# Annotate return types as above, delimited by an arrow ("->" string).
) -> (IntType, 'my_package.my_module.MyOtherClass', BoolType):
    return 0xDEADBEEF


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


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

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

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

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

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

Usage

The @beartype decorator published by the beartype package transparently supports various types of type-checking, each declared with a different type of type hint (i.e., annotation applied to a parameter or return value of a callable).

This is simpler than it sounds. Would we lie? Instead of answering that, let’s begin with the simplest type of type-checking supported by @beartype.

Builtin Types

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

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

from beartype import beartype

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

Let’s call that function with good types:

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

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

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

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

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

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

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

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

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

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

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

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

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

Arbitrary Types

Everything above also extends to:

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

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

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

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

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

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

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

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

Let’s iterate over that generator with good types:

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

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

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

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

Unions of Types

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

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

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

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

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

Let’s call that function with good types:

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

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

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

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

Optional Types

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

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

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

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

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

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

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

Let’s call that function with good types:

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

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

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

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

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

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

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

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

EnumOrEnumMemberType = (EnumType, EnumMemberType)

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

Let’s call that function with good types:

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

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

Features

Let’s chart current and prospective new features for future generations:

category

feature

versions

note

callables

coroutines

none

functions

0.1.0current

generators

0.1.0current

methods

0.1.0current

parameters

optional

0.1.0current

keyword-only

0.1.0current

positional-only

none

variadic keyword

none

variadic positional

0.1.0current

types

covariant classes

0.1.0current

absolute forward references

0.1.0current

relative forward references

none

tuple unions

0.1.0current

typing

AbstractSet

none

Any

none

AsyncContextManager

none

AsyncGenerator

none

AsyncIterable

none

AsyncIterator

none

Awaitable

none

BinaryIO

none

ByteString

none

ChainMap

none

Collection

none

Container

none

ContextManager

none

Coroutine

none

Counter

none

DefaultDict

none

Deque

none

Dict

none

Callable

none

ForwardRef

none

FrozenSet

none

Generator

none

Generic

none

Hashable

none

IO

none

ItemsView

none

Iterable

none

Iterator

none

KeysView

none

List

none

Mapping

none

MappingView

none

Match

none

MutableMapping

none

MutableSequence

none

MutableSet

none

NamedTuple

none

NewType

none

NoReturn

none

Optional

none

OrderedDict

none

Pattern

none

Reversible

none

Sequence

none

Set

none

Sized

none

SupportsAbs

none

SupportsBytes

none

SupportsComplex

none

SupportsFloat

none

SupportsIndex

none

SupportsInt

none

SupportsRound

none

Text

0.1.0current

TextIO

none

Tuple

none

Type

none

TypeVar

none

ValuesView

none

Union

none

PEP

484

none

Beartype will never full comply with PEP 484.

544

none

563

0.1.1current

585

none

586

none

589

none

packages

PyPI

0.1.0current

Anaconda

0.1.0current

Python

3.5

0.1.0current

3.6

0.1.0current

3.7

0.1.0current

3.8

0.1.0current

PEP 484 & Friends

Beartype does not currently support the following type-checking-centric Python Enhancement Proposals (PEPs):

Efficiency Concerns

Why? Because implementing even the core PEP 484 standard in pure Python while preserving beartype’s O(1) time complexity guarantee is infeasible.

Consider a hypothetical PEP 484-compliant @slothtype decorator decorating a hypothetical callable accepting a list of strings and returning anything:

from slothtype import slothtype
from typing import Any, List

@slothtype
def slothful(sluggard: List[str]) -> Any:
    ...

This is hardly the worst-case usage scenario. By compare to some of the more grotesque outliers enabled by the typing API (e.g., infinitely recursive types), a non-nested iterable of scalars is rather tame. Sadly, slothful still exhibits Ω(n) time complexity for length n of the passed list, where Ω may be read as “at least as asymptotically complex as” under the standard Knuth definition.

That’s bad. Each call to slothful now type-checks each item of a list of arbitrary size before performing any meaningful work. Python prohibits monkey-patching builtin types, so this up-front cost cannot be amortized across all calls to slothful (e.g., by monkey-patching the builtin list type to cache the result of prior type-checks of lists previously passed to slothful and invalidating these caches on external changes to these lists) but must instead be paid on each call to slothful. Ergo, Ω(n).

Safety Concerns

That’s not all, though. PEP 484 itself violates prior PEPs, including:

Optimistic Hand-waving

Beartype does intend to support the proper subset of PEP 484 (and its vituperative band of ne’er-do-wells) that both complies with prior PEPs and is efficiently implementable in pure Python – whatever that may be. Full compliance may be off the map, but at least partial compliance with the portions of these standards that average users care about is well within the realm of “…maybe?”

Preserving beartype’s O(1) time complexity guarantee is the ultimate barometer for what will be and will not be implemented. That and @leycec’s declining sanity. Our bumpy roadmap to a better-typed future now resembles:

beartype version

partial PEP compliance planned

0.2.0

PEP 484

0.3.0

PEP 544

0.4.0

PEP 585

0.5.0

PEP 586

0.6.0

PEP 589

If we wish upon a GitHub star, even the improbable is possible.

License

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

Funding

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

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

See Also

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

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

Project details


Download files

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

Source Distribution

beartype-0.1.1.tar.gz (124.9 kB view hashes)

Uploaded Source

Built Distribution

beartype-0.1.1-py3-none-any.whl (83.5 kB view hashes)

Uploaded Python 3

Supported by

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