Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The Future Sound of Beartype: Going Deep on Type-checking #7

Closed
leycec opened this issue Dec 10, 2020 · 44 comments
Closed

The Future Sound of Beartype: Going Deep on Type-checking #7

leycec opened this issue Dec 10, 2020 · 44 comments

Comments

@leycec
Copy link
Member

leycec commented Dec 10, 2020

2020 was an overloaded faceplant for humanity, right? Thanks for devoting your scarce attention in trying times to this small slice of the meaty pie that is Python quality assurance. You are awesome.

Greetings from a distant past, fellow Pythonista. By the time you read this, everything below has already become a nostalgic time capsule obsoleted by the sandy hands of Time and Guido. Still, let's do it.

In this pinned issue, we discuss where beartype is going, where beartype has been, and how we can get from here to 1.0.0 without hitting any more code pigeons with whom we had a deal.

But first...

Let's Talk About You!

...because you are awesome. Here are all the ways we hope to build a vibrant O(1) community that will outlive even cyborg head-in-a-jar @leycec.

Forum

Our forum "GitHub Discussions" is now up. Thanks to the explosive growth in both GitHub stars and page views, GitHub automatically unlocked its beta forum implementation for us. Hooray!

Ask me anything (AMA). I promise to answer at least 10% of all questions –
with particular attention to weeb genre tags like Japan, video games, heavy metal, scifi, fantasy, and the intersection of those five hot topics. So Japanese scifi-fantasy video games with metal OSTs. They exist, people.

Wiki

Our wiki is now open to public modifications. Since beartype is trivial to install (pip), configure (no configuration), and use (no API), there's currently no incentive for a wiki. I acknowledge this. My glorious dream is that the wiki will be an extemporaneous idea factory and whiteboard for unsound and probably dangerous methods of constant-time runtime type checking.

If that fails to manifest and the wiki just devolves into a spam-clogged cesspit of black-hat depravity, we'll reassess. Until then, do what thou wilt shall be the whole of the Law.

Pull Requests

We greedily welcome any pull request no matter how small or thanklessly ambitious. However, note that volunteer contributions will be... complicated. On the one hand, beartype is meticulously documented, commented, and tested. On the other hand, beartype is internally implemented as a two-pass type checker:

  1. A stupidly fast O(1) type checker that only tests whether all passed parameters and returned values satisfy all type hints.
  2. A stupidly slow O(n) type checker that raises human-readable exceptions in the event that one or more passed parameters or returned values violate a type hint.

Also, did we mention that the first pass is stupidly overtuned with cray-cray memoization, micro-optimizations, and caching? Despite my best efforts, this means that meaningful pull requests may never happen. I admit that this is non-ideal – but also unavoidable. Speed and PEP-compliance (in that order) are our primary motivations here. Maintainability and discoverability are tertiary concerns.

This is the high cost of adrenaline. So it goes.

The Future: It Looks Better Than the Past

Sloppy Markdown calendars >>>> real project management that middle-management always stuffs into Excel macro-driven Gantt charts, so:

Year Month Thing @leycec Is Doing for beartype
2020 December Success! Play video games until vomiting across a bucket.
2021 January Success! More video games. Less vomiting. It is a reasonable hope.
2022 February Success! beartype 0.6.0, Part Un. (That means "one" in Quebec.)
2022 March beartype 0.6.0, Part Deux. Ship it.
2022 April beartype 0.7.0, Part Un.
2022 May beartype 0.7.0, Part Deux. Ship it.
2022 June beartype 0.8.0, Part Un.
2022 July beartype 0.8.0, Part Deux. Ship it.
2022 August beartype 0.9.0, Part Un.
2022 September beartype 0.9.0, Part Deux. Ship it.
2022 October beartype 1.0.0, Part Un.
2022 November beartype 1.0.0, Part Deux. But we are not ready yet!
2022 December beartype 1.0.0, Part Trois. Ship it.

The official roadmap says one year to 1.0.0. Will we make it? Defly not. We'll fumble the pass in the final yard, stumble into the enemy mascot, and land in the celebratory Gatoraid™ bucket as the ref slashes the air with a red card to mass hysteria from the crowbar-wielding rowdy crowd.

The official roadmap cannot be denied, however.

Beartype 0.6.0: The Mappings Are Not the Territory

Beartype 0.6.0 intends to extend deep type-checking support to core data structures and abstract base classes (ABCs) implemented via the hash() builtin, including:

  • dict.
  • frozenset.
  • collections.ChainMap.
  • collections.OrderedDict.
  • collections.abc.DefaultDict.
  • collections.abc.ItemsView.
  • collections.abc.KeysView.
  • collections.abc.Mapping.
  • collections.abc.MutableMapping.
  • collections.abc.MutableSet.
  • collections.abc.Set.
  • collections.abc.ValuesView.
  • typing.ChainMap.
  • typing.DefaultDict.
  • typing.Dict.
  • typing.FrozenSet.
  • typing.ItemsView.
  • typing.KeysView.
  • typing.Mapping.
  • typing.MutableMapping.
  • typing.MutableSet.
  • typing.OrderedDict.
  • typing.Set.
  • typing.ValuesView.

beartype currently only shallowly type-checks these type hints. We can do better. We must do better! The future itself may very well depend upon it.

These are among the last big-ticket hints we need to deeply type-check, but they're also the least trivial. Although the C-based CPython implementation almost certainly stores both set members and dictionary keys and values as hash bucket sequences, it fails to expose those sequences to the Python layer. This means beartype has no efficient random access to arbitrary set members or dictionary keys and values.

Does that complicate O(1) runtime type-checking of sets and dictionaries? Yes. Yes, it does. I do have a number of risky ideas here, most of which revolve around internal caches of KeysView, ValuesView, and ItemsView iterators (i.e., the memory views returned by the dict.keys(), dict.values(), and dict.items() methods). I don't want to blow anything up, so this requires care, forethought, and a rusty blood-flecked scalpel.

Memory views only provide efficient access to the next dictionary object iterated by those views. This means the only efficient means of deeply type-checking one unique dictionary object per call to a @beartype-decorated callable is for beartype to internally cache and reuse memory views across calls. This must be done with maximum safety. To avoid memory leaks, cached memory views must be cached as weak rather than strong references. To avoid exhausting memory, cached memory views must be cached in a bounded rather than unbounded data structure.

The stdlib @functools.lru_cache decorator is the tortoise of Python's caching world. Everyone thinks it's fast until they inspect its implementation. Then they just define their own caching mechanism. Of course, beartype did exactly that with respect to an unbounded cache: our private beartype._util.cache.utilcachecall submodule defines the fastest-known pure-Python unbounded cache decorator, which we liberally call everywhere to memoize internal beartype callables.

Now, we'll need to define a similar bounded cache that caches no more than the passed maximum number of cache entries. This isn't hard, but it's something that needs to be done that takes resources. This is why my face looks constipated on a daily basis.

But that's not all. We then need to populate that cache with weak references to memory views dynamically created and cached at call time for @beartype-decorated callables in O(1) time with negligible constants. In Pythonic pseudocode, this might resemble for the specific case of item views:

from weakref import ref

#FIXME: Currently, this is unbounded. Define this as some sort of bounded
#dictionary containing only recently accessed key-value pairs.
dict_id_to_items_view = {}
'''
Bounded dictionary cache mapping from the unique identifier of each mapping
recently passed to a :mod:`beartype`-decorated callable to a weak reference to
a memory view iterating over that mapping's key-value pairs.
'''

def get_dict_nonempty_next_item(mapping: MappingType) -> object:
    '''
    Get the next key-value pair from the items view internally cached by
    :mod:`beartype` for the passed non-empty mapping.

    Specifically, this getter (in order):

    #. If either :mod:`beartype` has yet to internally cache a items view for
       this mapping *or* the prior call to this function returned the last
       key-value pair from this items view, internally creates and caches a new
       items view for this mapping.
    #. Returns the next items from this view.

    Caveats
    ----------
    **This mapping is assumed to be non-empty.** If this is *not* the case,
    this getter raises a :class:`StopIteration` exception.

    Parameters
    ----------
    mapping : MappingType
        Non-empty mapping to be iterated.

    Returns
    ----------
    object
        Next key-value pair from the items view internally cached by
        :mod:`beartype` for this non-empty mapping.

    Raises
    ----------
    StopIteration
        If this mapping is empty. Ergo, this getter should *only* be passed
        mappings known to be non-empty.
    '''

    # Integer uniquely identifying this mapping.
    mapping_id = id(mapping)

    # Items view previously cached for this mapping if any *OR* "None".
    items_iter = dict_id_to_items_view.get(mapping_id, None)

    # If this is the first call to a decorated callable passed or returning
    # this mapping...
    if items_iter is None:
        #FIXME: Protect this both here and below with a
        #"try: ... except Exception: ..." block, where the body of the
        #"except Exception:" condition should probably just return
        #"beartype._util.utilobject.SENTINEL", as the only type hints
        #that would ever satisfy that sentinel are type hints *ALL* objects
        #already satisfy (e.g., "Any", "object").
        dict_id_to_items_view[mapping_id] = ref(iter(mapping.items()))
    # Else, this memory view was previously cached.

    # Attempt to return the next key-value pair from this memory view.
    try:
        return next(dict_id_to_items_view[mapping_id])
    # If we get to the end (i.e., the prior call to next() raises a
    # "StopIteration" exception) *OR* anything else happens (i.e., the prior
    # call to next() raises a "RuntimeError" exception due to the underlying 
    # mapping having since been externally mutated), silently start over. :p
    except Exception:
        # Note that we could also recursively call ourselves here: e.g.,
        #     return get_dict_nonempty_next_item(mapping)
        # However, that would be both inefficient and dangerous.
        dict_id_to_items_view[mapping_id] = ref(iter(mapping.items()))
        return next(dict_id_to_items_view[mapping_id])

@beartype would then generate wrappers internally calling the above get_dict_nonempty_next_item() function to obtain a basically arbitrary (...yes, yes, insertion order, I know, but let's just pretend because it's late and I'm tired) mapping key, value, or key-value pair to be deeply type-checked in O(1) time. This is more expensive than randomly deeply type-checking sequence items, but hopefully not prohibitively so. It's all relative here. As long as we can shave this down to milliseconds of overhead per call, we're still golden babies.

Note that we basically can't do this under Python < 3.8, due to the lack of assignment expressions there. Since get_dict_nonempty_next_item() returns a new key-value pair each call, we can't repeatedly call that for each child pith and expect the same key-value pair to be returned. So, assignment expressions under Python >= 3.8 only. </shrug>

Under Python < 3.8, beartype will fallback to just unconditionally deeply type-checking the first key, value, item, or member of each passed or returned mapping and set. That's non-ideal, but Python < 3.8 is the past and the past is bad, so nobody cares. It's best that way.

Beartype 0.7.0: Well-hung Low-hanging Fruit

Beartype 0.7.0 intends to extend deep type-checking support to non-core data structures and abstract base classes (ABCs) – each of which is trivial to support in isolation but all of which together will break me like a shoddy arrow over their knees:

  • type.
  • collections.deque.
  • collections.Counter.
  • collections.abc.Collection.
  • collections.abc.Container.
  • collections.abc.Iterable.
  • collections.abc.Reversible.
  • typing.Collection.
  • typing.Container.
  • typing.Counter.
  • typing.Deque.
  • typing.Iterable.
  • typing.NamedTuple.
  • typing.Reversible.
  • typing.Type.
  • typing.TypedDict.

It is doable. Will it be done? Four out of five respondents merely shrug.

Beartype 0.8.0: Calling All Callables

Beartype 0.8.0 intends to extend deep type-checking support to callables:

  • collections.abc.AsyncIterable.
  • collections.abc.Awaitable.
  • collections.abc.Callable.
  • collections.abc.Coroutine.
  • typing.AsyncIterable.
  • typing.Awaitable.
  • typing.Callable.
  • typing.Coroutine.

Dynamically type-checking callables at runtime in O(1) is highly non-trivial and (maybe) even infeasible.

Some types of callables can't reasonably be deeply type-checked at all at runtime. This includes one-time-only constructs like generators and iterators, which can't be iterated without being destroyed. So, the Heisenberg Uncertainty Principle of Python objects.

Most types of callables can reasonably be deeply type-checked at runtime, but it's unclear how that can happen in O(1) time. The only non-harmful approach is to ignore the callables themselves and mono-focus instead on the annotations on callables. Specifically:

  • Ignore unannotated callables, because we can't reasonably call them to deeply type-check them without horrible side effects destroying the fabric of your fragile web app like tissue paper.
  • Generate code deeply type-checking callable annotations without iteration in @beartype-decorated callables. Since the arguments subscripting a callable type hint are finite and typically quite small in number (e.g., collections.abc.Callable[[int, str], dict[float, bool]]), there is little incentive to randomize here. Instead, we generate code resembling the code we currently generate for fixed-length tuples (e.g., tuple[int, str, float]).

This will necessitate looking up annotations for stdlib callables in the infamous third-party typeshed, which complicates matters. Whose bright idea was it, anyway, to offload annotations for stdlib callables onto some third-party repo? That's what PEP 563 is for, official Python developers. PEP 563 means nobody has to care about the space or time costs associated with annotations anymore. Maybe we should actually use that. </sigh>

Beartype 0.9.0: Type Variables Mean Pain

Beartype 0.9.0 intends to extend deep type-checking support to parametrized type hints (i.e., type hints subscripted by one or more type variables). I won't even begin to speculate how this will happen. I have a spontaneous aneurysm every time I try thinking about going deeper than a toy example like def uhoh(it_hurts: T) -> list[T]. It's when you get into type variables subscripting arbitrarily nested container and union type hints that my already meagre mental faculties begin unravelling under the heavy cognitive load.

It is possible to do this. But doing this could mean the last threads of my already badly frayed sanity. Challenge accepted.

Beartype 1.0.0: All the Remaining Things

Beartype 1.0.0 intends to extend deep type-checking support to everything that's left – some of which may not even reasonably be doable at runtime. The most essential of these include:

  • Class decoration. Generalizing the @beartype decorator to support both callables and classes is both critical and trivial – with one notable exception: parametrized classes (i.e., user-defined generic classes subclassing either typing.Generic or typing.Protocol, subscripted by one or more type variables). Supporting parametrized classes will probably prove to be non-trivial, because it means constraining types not merely within each class method but across all class methods annotated by parametrized type hints, which means that state needs to be internally preserved between method calls, which we currently do not do at all, because that is unsafe and hard. But that's fine. That's what we're here for. We're not here for Easy Mode™ type checking. We're here because this is the Soulsborne of the type-checking world. If it isn't brutal, ugly, and constantly stealing your soul(s), it ain't beartype.
  • typing.Literal, the typing hint formerly known as PEP 586. While typing.Literal itself is mostly inconsequential, supporting that hint in beartype requires a significant refactoring that should yield speedups across the board for all other hints. Well, isn't that special?

Let's trip over the above issue-laden minefields when we actually survive the preceding milestones with intact sanity, keyboard, and fingernails. Phew.

Beyond the Future: What Does That Even Mean?

Beartype 1.0.0 brings us perfect compliance constant-time compliance with all annotation standards. That's good. But is that it?

It is not.

Beartype + Sphinx + Read the Docs = A Match Made in Docstrings

The beartype API should be up on Read the Docs. It isn't, because we are slothful and full of laziness. Righting this wrong is a two-step process:

#. Enable local generation of Sphinx-based HTML documentation. Fortunately, we've judiciously documented every class, callable, and global throughout the codebase with properly-formatted reStructuredText (reST) in preparation for this wondrous day. The only real work here will be adding a top-level docs/ directory containing the requisite Sphinx directory structure. Still, it's probably one to two weeks worth of hard-headed volunteerism.
#. Enable remote hosting of that documentation on Read the Docs. I've never actually done this part before, so this will be the learning exercise. We'll probably need to wire up our GitHub Actions-based release automation to generate and publish new documentation with each stable release of beartype.

tl;dr: critical and trivial, if a little time-consuming. Let's do docs! And this leads us directly to...

Beartype Configuration API: It's Happening

beartype currently has no public API... effectively. Technically, of course:

  • The beartype.cave submodule is a public clearing house for common types and tuples of types, which was essential in the early days before we implemented full-blown PEP 484 compliance. Now? Vestigial, bro.
  • The beartype.roar submodule exposes public exceptions raised by the @beartype decorator at decoration time and by wrapper functions generated by that decorator at call time. Since there's probably no valid use case for actually catching and handling any of these exceptions in external third-party code, this submodule doesn't do terribly much for anyone either.

Neither of those submodules could be considered to be an actual API. Post beartype 1.0.0, we'd like to change that. The most meaningful change is the change everyone (including me, actually!) really wants: the flexibility to configure beartype to deeply type-check more than merely one container item per nesting level per call. While O(1) constant-time type-checking will always be the beartype default, we'd also like to enable callers to both locally and globally enable:

  • O(log n) logarithmic-time type-checking. Type-checking only a logarithmic number of container items per nesting level per call strikes a comfortable balance between type-checking just one and type-checking all containers items. Of course, this will still necessitate randomly selecting container items. Rather than just generating one random index each call, however, callables decorated with O(log n) type-checking will need to generate a logarithmic number of random indices each call. Since the Python standard library provides no means of doing so, we need to look further afield. The most efficient means of doing so that is commonly available is the numpy.random.default_rng().integers() method. On systems lacking numpy, beartype will probably ignore attempts to enable O(log n) type-checking by emitting a non-fatal warning and falling back to O(1) type-checking. Other than that, this seems mostly trivial to implement. Good 'nuff.
  • O(n) linear-time type-checking, affectionately referred to as "full fat" type-checking by an early adopter in a thread I've long since misplaced. Linear-time type-checking is reasonable only under certain ideal conditions that most callables fail to satisfy – like, say, callables guaranteed to receive and return either containers no larger than a certain size or larger containers that are only ever received or returned (and thus type-checked) exactly once throughout the entire codebase. But sometimes you absolutely know those things are the case (...at least for now) and you're willing to play dice with the DevGodsOfCrunch that that will absolutely, probably, hopefully never change. This is even more trivial to implement. But don't blame us when your entire web app crunches to a halt and you get "the call" on a Saturday night. We told you so.
  • Caller-configurable hybrid type-checking. In this use case, callers configure container sizes at which they want various type-checking strategies to kick in. For example, callers might stipulate that for containers of arbitrary size n:
    • If n <= 10, perform O(n) type-checking on those containers.
    • If n <= 100, perform O(log n) type-checking on those containers.
    • For all other sizes, fallback to O(1) type-checking on those containers.

Here's how this API might shape out in practice. First, we define a new beartype.config submodule declaring a new enumeration ContainerStrategy = Enum('ContainerStrategy', 'O1 Ologn On Hybrid') enabling callers to differentiate between these strategies. Then, we augment the @beartype decorator to accept an optional container_strategy parameter whose value is a member of this enumeration that (wait for it) defaults to O1.

Here's how that API would be used in practice:

from beartype import beartype
from beartype.config import ContainerStrategy

@beartype(container_strategy=ContainerStrategy.Ologn)
def i_know_what_im_doing(promise: list[list[list[int]]]) -> int:
    '''
    A logarithmic number of items at all three nesting levels of
    the passed "promise" parameter will be deeply type-checked
    on each call to this function.
    '''
    return promise[0[0[0]]]

Let's say you decide you like that. Thanks to the magic of the functools.partial() function, you could then mandate that strategy throughout your codebase as follows:

from beartype import beartype
from beartype.config import ContainerStrategy
from functools import partial

beartype_Ologn = partial(beartype, container_strategy=ContainerStrategy.Ologn)

@beartype_Ologn
def i_still_know_what_im_doing(i_swear: list[list[list[int]]]) -> int:
    '''
    A logarithmic number of items at all three nesting levels of
    the passed "promise" parameter will be deeply type-checked
    on each call to this function.
    '''
    return promise[0[0[0]]]

You would then decorate callables with your custom @beartype_Ologn decorator rather than the default @beartype decorator. This conveniently circumvents the need for beartype to define a global configuration API, which I'm reluctant to do, because I'm lazy. Full stop.

Of course, we should probably just publicly define @beartype_Ologn and @beartype_On decorators for everybody so that nobody even has to explicitly bother with functools.partial(). We should. But... we're lazy!

And now for something completely different.

Beyond Typing PEPs, There Lies Third-Party Typing

Third-party types include all of the scientific ones that made Python a household name in machine learning, data science, material science, and biotechnology over the past decade – including:

There will probably never be a standard for type-hinting these types, because these types reside outside the Python standard library.

But that doesn't mean we're done here. We can produce type hints that are both PEP-compliant and generically usable at runtime by any runtime type checker (as well as by static type checkers with explicit support for those hints).

How? By leveraging PEP 3119 - "Introducing Abstract Base Classes", which standardized the __isinstancecheck__() and __issubclasscheck__() metaclass dunder methods. These methods enable third-party developers to dynamically create new classes on-the-fly that:

  • Technically comply with PEP 484, because "user-defined classes (including those defined in the standard library or third-party modules)" are explicitly PEP-compliant.
  • Can be used as PEP-compliant type hints to validate the structure of arbitrary third-party data structures, including those listed above.

The idea here is to extract that machinery into a new PyPI-hosted package named deeptyping (or something something) that declares one type hint factory for each of the above data structures. deeptyping will be designed from the ground-up to be implicitly useful at both static time and runtime by performing deep type-checking on runtime calls to the isinstance() and issubclass() builtins. This stands in stark contrast to the standard typing module, which almost always prohibits calls to those builtins and is thus mostly useless at runtime. Ergo, "deep" typing.

For example, here's what PEP-compliant NumPy array type hints might look like:

from beartype import beartype
from deeptyping import NumpyArray

# This constrains the passed "funbag_of_fun" parameter to be a two-dimensional
# NumPy array with any float dtype (e.g., "np.float32", "np.float64").
@beartype
def catch_the_funbag(funbag_of_fun: NumpyArray(dtype=float, ndim=2)) -> float:
     return funbag_of_fun[0, 0]

The deeptyping.NumpyArray() class factory function would have signature resembling:

from numpy import dtype as _dtype
def NumpyArray(dtype: _dtype, ndim: int): ...

That function would be implemented as a class factory dynamically creating and returning new memoized classes compliant with PEPs 3119 and 484. For example, here's untested pseudocode (that will blow up horribly, which will make me feel bad, so don't try this) for the metaclass of the class that function might create when passed the above parameters:

from numpy import ndarray

class _NumpyArrayDtypeFloatNdim2Metaclass(object):
    '''
    Metaclass dynamically generated by the :func:`deeptyping.NumpyArray`
    class factory function for deep type-checking numpy arrays of
    dtype :class:`float` and dimensionality 2 in a manner compliant with
    PEPs 3119 and 484.
    '''

    def __isinstancecheck__(obj: object, cls: type) -> bool:
        return (
            isinstance(obj, ndarray) and
            obj.dtype.type is float and
            obj.ndim == 2
        )

    def __issubclasscheck__(subcls: type, cls: type) -> bool:
        return issubclass(subcls, ndarray)

There are probably substantially better ways to do that. Hopefully, it would suffice to statically declare merely one generic metaclass that would then be shared between all dynamically created classes with that metaclass returned by the deeptyping.NumpyType() factory function.

Anyways. Everything above is pure speculation. The overarching point is that neither beartype nor any other runtime type checker will need to be refactored to depend on or otherwise explicitly support deeptyping, because all PEP 484-compliant runtime type checkers already implicitly support that sort of thing. It is good.

Technically, there do exist third-party packages violating all annotation standards that support typing hinting of a few of the above data structures. The third-party Traits package, for example, supports NumPy-specific type hints – but only by violating all annotation standards. That rather defeats the point of standards. From the important perspective of PEP-compliance, it is bad.

Thus deeptyping.

Beyond typing_inspect, There Lies pepitup

The dirty little secret behind Python's annotation standards is that they all lack usable APIs. No, the typing.get_type_hints() function doesn't count, because that function doesn't do anything useful that you can't do yourself while doing a whole lot that's non-useful (like being face-stabbingly slow). The only exception is PEP 544 -- "Protocols: Structural subtyping (static duck typing)", which does surprisingly have a usable API. That's nice.

This pains us. In theory, it shouldn't be possible to get any Python Enhancement Proposal (PEP) that lacks a usable API passed the peer review process – let alone the seven PEPs lacking usable APIs that beartype currently supports.

But here we are. It happened. It happened because the official Python developer community wrongly perceived static type checking to be the only useful kind of type checking. Most annotation PEPs never even use the adjective "runtime" except in an incidental or derogatory manner. PEP 484, for example, declares:

The Sequence[int] notation works at runtime by implementing __getitem__() in the metaclass (but its significance is primarily to an offline type checker).

No, its significance is to all type checkers. Who wrote that?

This PEP aims to provide a standard syntax for type annotations, opening up Python code to easier static analysis and refactoring, potential runtime type checking, and (perhaps, in some contexts) code generation utilizing type information. Of these goals, static analysis is the most important. This includes support for off-line type checkers such as mypy, as well as providing a standard notation that can be used by IDEs for code completion and refactoring.

Of course, it's never explained why static type-checking is the most (by which they mean "only") important goal. It's just assumed a priori that runtime type-checking is sufficiently insane, inefficient, and ineffectual as to be universally unimportant – when, in fact, static type-checking of dynamically-typed languages is insane by definition.

Runtime type checkers can decide entire classes of decision problems undecidable by static type checkers, most of which are industry standard throughout the Python community.

Runtime type checkers also never report false negatives (not even for one-shot objects unintrospectable at runtime like generators and iterators). Runtime type checkers never have to guess, infer, or otherwise derive types. But static type checkers always do those things, because guesswork is all they do.

So beartype challenges common assumptions merely by existing. But that's not enough. We may have solved runtime type-checking by internally implementing private stand-in APIs for all these PEPs, but nobody else can safely access that or reuse our efforts.

We don't have our Delorean yet, so we can't retroactively go back and fix this in the past. Even adding public APIs for these PEPs to the Python standard library wouldn't really help anyone, because Python 3.9 will still be alive through most of 2025. But that doesn't mean we just have to "suck it up."

The third-party typing_inspect package partially solves this problem by defining a limited public API for these PEPs. But it only supports:

  • The most recent stable release of Python.
  • A sharply limited subset of these PEPs.

Because of these limitations, you couldn't reimplement beartype based on typing_inspect, for example. But we can publicize beartype internals as our own separate third-party package that supports:

  • All actively maintained releases of Python.
  • The complete feature set of these PEPs.

beartype currently sequesters these internals to the private nested beartype._util.hint.pep subpackage, with associated machinery haphazardly strewn about. The idea here is to extract that machinery into a new PyPI-hosted package named pepitup (or something something) and then refactor beartype to depend on that package.

It's a nice idea. But nice ideas often never happen. Let's see if this one does!

Influencer versus Introvert: Introvert Wins!

And last but certainly not least... influencing. Let's recount the ways @leycec should start banging on that social influencing drum to hype up the nascent O(1) runtime type-checking scene:

  • Academic publication. This is the biggie. beartype is the table flip of the type-checking world. Cue that emoji. (╯°□°)╯︵ ┻━┻ beartype behaves fundamentally differently (both theoretically and practically) from all existing type checkers – runtime or not. This means we have more than a few novel things to talk about. Clearly, I like talking. Let's be honest, right? So that's not the issue. The issue is that passing peer review as an independent scientist with no current University affiliation is balls hard non-trivial. But I'm committed to doing this. Without publication, no one can formally cite beartype in their own publications, which means beartype is invisible to academia, which is bad. Moreover, publication constitutes a soft (maybe hard) prerequisite for securing eventual grant funding from governmental agencies in Canada and the U.S., the two nations I hold dual-citizenship in. For sanity, publication will probably happen in incremental stages:
    1. arXiv preprint. In this stage, we publish a preliminary technical report documenting beartype features, tradeoffs, and complications. I'm fluent in LyX (which I love) and conversant in LaTeX (which I loathe), so this should be "fun" for several qualifying definitions of "fun."
    2. Informal peer review. In this stage, I beg various beartype aficionados, motivators, and early adopters with University affiliation for their totally nice and life-affirming constructive criticism. After my fragile ego recovers from the death blows by e-mail, I'll scrap the whole report and rewrite it from the ground up for...
    3. Journal publication! In this stage, we summit the high mountain of formal peer review with minimal blood loss, festering head wounds, and coronary artery disease. We can safely assume whatever journal(s) I submit to will have blatantly pitiable impact factors and submission standards suggestive of attention-seeking desparation, which I for one welcome.
  • Blog article series. My wife and I don't even have blogs anymore... because we got lazy and they sorta bit-rotted and now we really wish we hadn't let that happen. So that's a blocker. But I promise the uncaring world this: we will resurrect those unseemly blogs from their digital graves in 2021 and it shall be glorious! We're talking "GeoCities circa 1998" tier with dancing unicorns, Under Construction signs, Comic Sans typography, and siezure-inducing hypnotic flashing text littering every ad-strewn header.
  • Twitter feed. Just... "Ugh." I know I should. But I'm a lakeside hermit with an unwieldy penchant for wilderness areas, Japanese culture (早稲田大学生, represent), and Python type-checking. The intersection of these three interests is the empty set. But yeah. Gotta snag those sweet retweet amplifiers if we want O(1) to go the whole nine yards. Let's see if @leycec can extricate himself from the seductive yet limiting INTP shell long enough to actualize this.

Phew. That real-world stuff really isn't as easy as it looks.

And... We're Done

🤯

@leycec leycec pinned this issue Dec 10, 2020
leycec added a commit that referenced this issue Dec 11, 2020
This commit mildly revises our front-facing "README.rst" to note our
newly pinned issue #7, detailing the roadmap to beartype 1.0.0, as well
as listing "typing" objects erroneously omitted from our "Partial
Compliance" subsection. (*Faustian exhausts exhume inhuman inhumations!*)
@leycec
Copy link
Member Author

leycec commented Dec 12, 2020

Paging @albanie, both because you were right about exponential growth ...still didn't expect that and because the Beyond Typing PEPs, There Lies Third-Party Typing subsection above was written with mostly you in mind.

Can't let those early adopters down!

@albanie
Copy link

albanie commented Dec 14, 2020

Predictably epic 😍
It's interesting to see that there was such a strong emphasis on static-only type checks when these PEPs were written (presumably due to limited historical precedents of fast runtime checking).
deeptyping FTW. One day, typed tensors will rule the world.

@leycec
Copy link
Member Author

leycec commented Dec 15, 2020

There's another new section I just excreted you might be interested in, too: Beartype Configuration API: It's Happening. This is the thing that will let you tell beartype to generate O(n) or O(log n) rather than just O(1) type-checking for specific callables.

Let me know how that API works for you. It'll be baked into granite at some point and then we'll all have to live with my permanently dodgy design.

Predictably epic 😍

oh, you ☺️

It's interesting to see that there was such a strong emphasis on static-only type checks when these PEPs were written (presumably due to limited historical precedents of fast runtime checking).

Right. Last week, I was shocked (both pleasantly and unpleasantly) to find that beartype is the world's first sublinear runtime type-checker in any dynamically-typed language.

I couldn't believe it. It's still hard to believe. Like, no one amongst the eight billion-fold tribal humans dispersed across the fragile surface of this beautiful planet thought to try anything but "full fat" runtime type-checking?

Guess not. So, I can't and shouldn't fault Python developers. They're the only ones willing to do this largely thankless standardization work. At least something was standardized – even if it fell painfully short of the mark with respect to runtime introspection. Thanks, former BDFL Guido & Crew!

Seriously. Thanks.

deeptyping FTW.

👊

One day, typed tensors will rule the world.

...and I will worship them as data structure deities when that prophesy inevitably comes to pass.

@leycec
Copy link
Member Author

leycec commented Dec 16, 2020

Update: new Influencer versus Introvert section where we outline the slow road to formal academic publication, informal blog posts, and... Twitter feeds? At least one of those things will happen.

@kevinjacobs-progenity
Copy link

Interesting project. The inside/subtle/clever jokes get in the way of those trying to understand the project. In particular, most of the cleverness is lost on those of us who have no exposure to (or interest in) The Jungle Book, don't naturally associate bears with type checking, and just want to quickly grok the essence of what you've built. Consider writing a quick intro for clever boring people who aren't prepared to slog through the dozens of pages of otherwise-clever content?

@leycec
Copy link
Member Author

leycec commented Dec 16, 2020

Fair and valid criticisms, one and all. I subscribe to the why the lucky stiff school of edumucation, but acknowledge that some (most) people "just want the facts, Mam."

The Cheatsheet is for those people. If that's not quite cursory enough, the high-level one-sentence tl;dr exegesis for beartype would be:

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

Profit ensues. There's not much else to see, because the public beartype API is driven entirely by community standards defined elsewhere. All of the black magic is internal to beartype, which (thankfully) no one sees. All anyone sees externally see is that decorated functions and methods magically begin raising human-readable exceptions when you pass parameters or return values that violate the type hints annotating those parameters or return values. Super, right?

beartype does assume some cursory familiarity with annotation-centric Python Enhancement Proposals (PEPs) and Python's modern type hinting stack, the most mission-critical of which are:

If that's a lot to swallow all at once (which it is), Real Python has an excellent write-up on Python annotations. We can and should improve on that here with a more readable introduction to the topic unique to beartype, probably hosted at Read The Docs. Since you have usability concerns, you're not the only one. Improved documentation is on our post-beartype 1.0.0 roadmap above, which we will hopefully get to shortly. Believe in the future, because the hype is real.

Thanks for the interest, @kevinjacobs-progenity! Let me know if there's some low-hanging documentation fruit I can pluck here to make your stay more comfortable, simplistic, and self-explanatory. Also, Ann Arbor rocks. That's a heck of a University town you got there. 🏙️

@kevinjacobs-progenity
Copy link

Thanks, @leycec. Your response is actually very much along the lines of what a casual/impatient browser would want to see. Add in a very quick summary of what it means to achieve O(1) runtime cost (approximate and amortized value checking), a few technically non-trivial examples with typing errors (and boring code), and my concerns will be address. That said, I'm still trying to wrap my head around the implications of using your approach at runtime with my target application being medical device software (to aid with verification and obviously not in production).

And thanks for being open minded and constructive about my feedback. Hope all is well in the farther snowy North.

leycec added a commit that referenced this issue Dec 17, 2020
This commit significantly revises our front-facing "README.rst" in
accordance with relevant concerns raised by @kevinjacobs-progenity at
issue #7, including:

* A new "Usage" section that accidentally became a new tutorial. Woops.
* Division of the existing "Overview" section into various subsections
  highlighting tradeoffs between beartype and existing type checkers.

I'd like to thanks @kevinjacobs-progenity for his invaluable commentary,
without which beartype would be even less usable, which probably means
unusable. (*Extorted tort lawyers contort conscripted drawers!*)
@leycec
Copy link
Member Author

leycec commented Dec 17, 2020

Thank you, Minion Kevin. Surprisingly funny and well-intended criticism like yours is invaluable, because other prospective users were thinking the same things but probably didn't want me to feel bad.

I welcome feeling bad. That's how bad things get less bad.

I pushed a few commits in your honour reworking our front-facing documentation for clarity. The Usage section is entirely new and intended to address the bulk of your concerns. Let me know if you have any remaining thoughts! We'll squash those, too.

That said, I'm still trying to wrap my head around the implications of using your approach at runtime with my target application being medical device software...

Oh. Oh. A fellow biotech bro. Well, isn't that synchronous. My lovely wife is in corporate research for bleeding-edge (please not literally) spinal implants. Our prior work was multiphysics biology simulation for therapeutic tissue regeneration, so we're all old hats at this Python medical game.

Let me know if you have any specific typing needs or concerns. Structural type-checking for NumPy arrays, Pandas dataframes, and loosely related scientific objects like tensors is planned post-beartype 1.0.0 through a new deeptyping package still in the planning stages. See references to deeptyping above if that sounds like mutual interest.

Thanks again for the deep critique. We'll get there... someday. Until then, we play in the piles of fresh snow that hopefully will be coming soon.

@felix-hilden
Copy link
Contributor

Hi, interesting project indeed! Although I'm also one of the boring "gimme the tl;dr" kinda people. But I do admire the dedication you positively radiate through the text!

I'm here to give my two cents, although I don't understand the library quite yet:

  • I would absolutely adore to be able to type hint numpy arrays as you describe, and I am certain so would endless numbers of other developers as well. That's a brilliant idea!
  • Hosting documentation on Read The Docs is quite straightforward, I did it for the first time a while back too. There's no need for GH Actions triggering builds. If you set your documentation to follow release tags, the builds can happen automagically!

All the best to you!

@leycec
Copy link
Member Author

leycec commented Jan 13, 2021

Thanks for the kind commentary, @felix-hilden. Yup. I'm not so good at bringin' that sweet tl;dr yet. Let's hope for the sake of prolix nebulosity and my ailing sticky keyboard I level up on this in 2021.

I would absolutely adore to be able to type hint numpy arrays as you describe, and I am certain so would endless numbers of other developers as well. That's a brilliant idea!

Another 👍 for deeptyping it is!

Behind the scenes, I'm obsessed throughout the remainder of January with roadmapping funding (...was that a collective "Ugh!" I just heard?) and core support for deep O(1) type-checking of dictionaries and sets (yay!).

But... this will happen. Both because we all want this to happen and because that's probably where most of the actual funding opportunities lie. To paraphrase infamous JRPG developer Yoko Taro, I'll do just about anything if I get the money for it.

Of course, I'll still do this and everything else without money. But money helps – especially with eating and other low-level primate activities.

Hosting documentation on Read The Docs is quite straightforward, I did it for the first time a while back too.

That reassures my omnipresent Read The Docs + Sphinx + reStructuredText anxiety disorder. Fortunately, we currently have no API. wat Unfortunately, we will have an API at some point. That is the point at which this will happen. Unless someone does this for me first.

Please do this for me first, someone.

All the best to you!

And to you! 2020 was Spidey's Wild Ride and 2021 frankly didn't start any saner. I pray to a digital effigy of Richard Stallman every evening that sanity will be restored. Positive results should follow shortly, everyone.

leycec added a commit that referenced this issue Mar 4, 2021
This release brings explicit support for ``None``, subscripted generics,
and PEP 561 compliance. This release resolves **10 issues** and merges
**8 pull requests.** Specific changes include:

## Compatibility Improved

* **[PEP 484-compliant `None`
  singleton](https://www.python.org/dev/peps/pep-0484/#id16).** As a
  return type hint, `None` is typically used to annotate callables
  containing *no* explicit `return` statement and thus implicitly
  returning `None`. `@beartype` now implicitly reduces `None` at all
  nesting levels of type hints to that singleton's type per [PEP
  484](https://www.python.org/dev/peps/pep-0484/#id16).
* **[PEP 561 compliance](https://www.python.org/dev/peps/pep-0561).**
  `beartype` now fully conforms to [PEP
  561](https://www.python.org/dev/peps/pep-0561), resolving issue #25
  kindly submitted by best macOS package manager ever @harens. In
  useful terms, this means that:
  * **`beartype` now complies with [mypy](http://mypy-lang.org),**
    Python's popular third-party static type checker. If your
    package had no [mypy](http://mypy-lang.org) errors or warnings
    *before* adding `beartype` as a mandatory dependency, your package
    will still have no [mypy](http://mypy-lang.org) errors or warnings
    *after* adding `beartype` as a mandatory dependency.
  * **`beartype` preserves [PEP
    561](https://www.python.org/dev/peps/pep-0561) compliance.** If your
    package was [PEP
    561-compliant](https://www.python.org/dev/peps/pep-0561) *before*
    adding `beartype` as a mandatory dependency, your package will still
    be [PEP
    561-compliant](https://www.python.org/dev/peps/pep-0561) *after*
    adding `beartype` as a mandatory dependency. Of course, if your
    package currently is *not* [PEP
    561-compliant](https://www.python.org/dev/peps/pep-0561), `beartype`
    can't help you there. We'd love to, really. [It's us. Not
    you.](https://www.youtube.com/watch?v=2uAj4wBIU-8)
  * **The `beartype` codebase is now mostly statically rather than
    dynamically typed,** much to our public shame. Thus begins the
    eternal struggle to preserve duck typing in a world that hates bugs.
  * **The `beartype` package now contains a top-level `py.typed` file,**
    publicly declaring this package to be [PEP
    561-compliant](https://www.python.org/dev/peps/pep-0561).
* **Subscripted generics** (i.e., [user-defined
  generics](https://www.python.org/dev/peps/pep-0484) subscripted by
  one or more type hints), resolving issue #29 kindly submitted by
  indefatigable test engineer and anthropomorphic Siberian Husky
  @eehusky. Since it's best not to ask too many questions about
  subscripted generics, we instead refer you to [the issue report that
  nearly broke a Canadian
  man](#29).

## Compatibility Broken

* **None.** This release preserves backward compatibility with the prior
  stable release.

## Packaging Improved

* **New optional installation-time extras,** enabling both `beartype`
  developers and automation tooling to trivially install recommended
  (but technically optional) dependencies. These include:
  * `pip install -e .[dev]`, installing `beartype` in editable mode as
    well as all dependencies required to both locally test `beartype`
    *and* build documentation for `beartype` from the command line.
  * `pip install beartype[doc-rtd]`, installing `beartype` as well as
    all dependencies required to build documentation from the external
    third-party Read The Docs (RTD) host.
* **Homebrew- and MacPorts-based macOS installation.** Our front-facing
  `README.rst` file now documents `beartype` installation with both
  Homebrew and MacPorts on macOS, entirely courtesy the third-party
  Homebrew tap and Portfile maintained by build automation specialist
  and mild-mannered student @harens. Thanks a London pound, Haren!

## Features Added

* Public `beartype.cave` types and type tuples, including:

  * `beartype.cave.CallableCTypes`, a tuple of all **C-based callable
    types** (i.e., types whose instances are callable objects
    implemented in low-level C rather than high-level Python).
  * `beartype.cave.HintGenericSubscriptedType`, the C-based type of all
    subscripted generics if the active Python interpreter targets Python
    >= 3.9 *or* `beartype.cave.UnavailableType` otherwise. This type was
    previously named `beartype.cave.HintPep585Type` before we belatedly
    realized this type broadly applies to numerous categories of
    PEP-compliant type hints, including PEP 484-compliant subscripted
    generics.

## Features Optimized

* **`O(n)` → `O(1)` exception handling.** `@beartype` now internally
  raises human-readable exceptions in the event of type-checking
  violations with an `O(1)` rather than `O(n)` algorithm, significantly
  reducing time complexity for the edge case of invalid large sequences
  either passed to or returned from `@beartype`-decorated callables. For
  forward compatibility with a future version of `beartype` enabling
  users to explicitly switch between constant- and linear-time checking,
  the prior `O(n)` exception-handling algorithm has been preserved in a
  presently disabled form.
* **`O(n)` → `O(1)` callable introspection during internal
  memoization.** `@beartype` now avoids calling the inefficient stdlib
  `inspect` module from our private
  `@beartype._util.cache.utilcachecall.callable_cached` decorator
  memoizing functions throughout the `beartype` codebase. The prior
  `O(n)` logic performed by that call has been replaced by equivalent
  `O(1) logic performed by a call to our newly defined
  `beartype._util.func.utilfuncarg` submodule, optimizing function
  argument introspection without the unnecessary overhead of `inspect`.
* **Code object caching.** `@beartype` now temporarily caches the code
  object for the currently decorated callable to support efficient
  introspection of that callable throughout the decoration process.
  Relatedly, this also has the beneficial side effect of explicitly
  raising human-readable exceptions from the `@beartype` decorator on
  attempting to decorate C-based callables, which `@beartype` now
  explicitly does *not* support, because C-based callables have *no*
  code objects and thus *no* efficient means of introspection.
  Fortunately, sane code only ever applies `@beartype` to pure-Python
  callables anyway. ...right, sane code? *Right!?!?*

## Features Deprecated

* The ambiguously named `beartype.cave.HintPep585Type` type, to be
  officially removed in `beartype` 0.1.0.

## Issues Resolved

* **Unsafe `str.replace()` calls.** `@beartype` now wraps all unsafe
  internal calls to the low-level `str.replace()` method with calls to
  the considerably safer high-level
  `beartype._util.text.utiltextmunge.replace_str_substrs()` function,
  guaranteeing that memoized placeholder strings are properly unmemoized
  during decoration-time code generation. Thanks to temperate perennial
  flowering plant @Heliotrop3 for this astute observation and resolution
  to long-standing background issue #11.
* **`KeyPool` release validation.** `@beartype` now validates that
  objects passed to the `release()` method of the private
  `beartype._util.cache.pool.utilcachepool.KeyPool` class have been
  previously returned from the `acquire()` method of that class. Thanks
  to @Heliotrop3, the formidable bug assassin, for their unswerving
  dedication to the cause of justice with this resolution to issue #13.
* **Least Recently Used (LRU) cache.** ``@beartype`` now internally
  provides a highly microoptimized Least Recently Used (LRU) cache for
  subsequent use throughout the codebase, particularly with respect to
  caching iterators over dictionaries, sets, and other non-sequence
  containers. This resolves issue #17, again graciously submitted by
  open-source bug mercenary @Heliotrop3.
* **Callable labelling.** `@beartype` now internally provides a private
  `beartype._util.func.utilfuncorigin.get_callable_origin_label` getter
  synthesizing human-readable labels for the files declaring arbitrary
  callables, a contribution by master code-mangler @Heliotrop3 resolving
  issue #18. Thanks again for all the insidious improvements, Tyler! You
  are the master of everyone's code domain.
* **Release automation.** Our release workflow has now been migrated
  from the unmaintained `create-release` GitHub Action to @ncipollo's
  actively maintained `release-action`, resolving issue #22 kindly
  submitted by human-AI-hybrid @Heliotrop3.

## Tests Improved

* **Microsoft Windows and macOS exercised under CI**, resolving issue
  #21. Since doing so increases our consumption of Microsoft resources
  that we care deeply about, care has been taken to reduce the cost of
  our CI workflow. This includes:
  * Replacing our prior use of the external third-party `tox-gh-actions`
    GitHub Action streamlining `tox` usage with our own ad-hoc build
    matrix that appears to be simpler and faster despite offering
    basically identical functionality.
  * Removing our prior installation of optional dependencies, especially
    including NumPy. *Yeah.* Let's not do that anymore.
  Thanks to dedicated issue reporter @Heliotrop3 for his unsustainable
  deep-code trawling of the `beartype` codebase for unresolved `FIXME:`
  comments.
* **PyPy 3.7 exercised under CI.** Our `tox` and GitHub Actions-based
  continuous integration (CI) configurations now both correctly exercise
  themselves against both PyPy 3.6 and 3.7, resolving the upstream
  actions/setup-python#171 issue for `beartype`.
* **CI thresholded.** Our CI configuration now caps tests to a sane
  maximum duration of time to avoid a repeat of the pull request we do
  *not* talk about here. Okay, it was #23. I blame only myself.
* **New functional tests,** including:
  * A **CPython-specific [mypy](http://mypy-lang.org) functional test,**
    optionally exercising our conformance to static type-checking
    standards when the third-party `mypy` package is installed under
    CPython. This test is sufficiently critical that we perform it under
    our CI workflow, guaranteeing test failures on any push or PR
    violating mypy expectations.
  * A **`README.rst` functional test,** optionally exercising the
    syntactic validity of our front-facing `README.rst` documentation
    when the third-party `docutils` package (i.e., the reference reST
    parser) is installed. This test is sufficiently expensive that we
    currently avoid performing it under our CI workflow.
* **New unit tests,** including:
  * **Text munging unit tests,** exercising the private
    `beartype._util.text.utiltextmunge` submodule with lavish attention
    to regex-based fuzzy testing of the critical `number_lines()`
    function. Humble `git log` shout outs go out to @Heliotrop3 for this
    mythic changeset that warps the fragile fabric of the GitHub cloud
    to its own pellucid yet paradoxically impenetrable intentions,
    resolving issue #24.

## Documentation Revised

* **Sphinx skeleton.** The `beartype` repository now defines a largely
  unpopulated skeleton for Sphinx-generated documentation formatted as
  reST and typically converted to HTML to be hosted at [Read The Docs
  (RTD)](https://beartype.readthedocs.io/en/latest/?badge=latest),
  generously contributed by @felix-hilden, Finnish computer vision
  expert and our first contributor! This skeleton enables:
  * An HTTP 404 redirect page on missing page hits.
  * The standard retinue of builtin Sphinx extensions (e.g., `autodoc`,
    `viewcode`).
  * MathJax configured for remote implicit downloads.
  * Napolean configured for NumPy-formatted docstrings.
  * An optional dependency on `sphinx_rtd_theme`, a third-party Sphinx
    extension providing RTD's official Sphinx HTML.
  * A **badge** (i.e., shield, status icon) on our front-facing
    `README.rst` documentation signifying the success of the most recent
    attempt to build and host this skeleton at RTD.
  * A **top-level `sphinx` script**, building Sphinx-based package
    documentation when manually run from the command line by interactive
    developers.
* A **[beautiful banner graphic that makes grown adults openly
  weep](https://github.com/beartype/beartype-assets/tree/main/banner),**
  featuring the official `beartype` mascot "Mr. Nectar Palm" – again
  courtesy @felix-hilden, because sleep is for the weak and Felix has
  never known the word.
* A **new prefacing "tl;dr" section** that's redundant with numerous
  other sections, but we're surprisingly okay with that.
* A **new "Usage" section** that accidentally became a new tutorial and
  division of the existing "Overview" section into various subsections
  highlighting tradeoffs between `beartype` and existing type checkers,
  resolving clarity concerns raised by @kevinjacobs-progenity at issue
  #7. Thanks for the invaluable commentary, Kevin!
* A **new "Frequently Asked Questions (FAQ)" section,** inspired by the
  admission from several prospective users that they have utterly no
  idea what @leycec is talking about. Fair play, users. You win this
  round.
* A **new "Workflow" subsection of the "Developer" section,** listing
  developer-specific instructions for forking, cloning, installing,
  modifying, and submitting PRs for `beartype` in a live manner.
* **Properly rendered code blocks,** kindly reported by humane human
  extraordinaire @harens in discussion topic #28. Thanks and may the
  little-seen English sun eternally shine upon ye, Haren!

## API Changed

* Added:
  * `beartype.cave.CallableCTypes`.
  * `beartype.cave.HintGenericSubscriptedType`.
* Deprecated:
  * `beartype.cave.HintPep585Type`.

(*Exogenous exhaustion!*)
@leycec
Copy link
Member Author

leycec commented Mar 4, 2021

Beartype 0.6.0 has been released to a fanfare of cat mewls, crackling icicles, and the quietude of a snow-blanketed Canadian winter. ☃️

We hope you are warm and safe, coding as only you can code. This release brings explicit support for None, subscripted generics, and PEP 561 compliance after resolving 10 issues and merging 8 pull requests. Changes include:

Compatibility Improved

  • PEP 484-compliant None singleton. As a return type hint, None is typically used to annotate callables containing no explicit return statement and thus implicitly returning None. @beartype now implicitly reduces None at all nesting levels of type hints to that singleton's type per PEP 484.
  • PEP 561 compliance. beartype now fully conforms to PEP 561, resolving issue PEP 561 compliance #25 kindly submitted by best macOS package manager ever @harens. In useful terms, this means that:
    • beartype now complies with mypy, Python's popular third-party static type checker. If your package had no mypy errors or warnings before adding beartype as a mandatory dependency, your package will still have no mypy errors or warnings after adding beartype as a mandatory dependency.
    • beartype preserves PEP 561 compliance. If your package was PEP 561-compliant before adding beartype as a mandatory dependency, your package will still be PEP 561-compliant after adding beartype as a mandatory dependency. Of course, if your package currently is not PEP 561-compliant, beartype can't help you there. We'd love to, really. It's us. Not you.
    • The beartype codebase is now mostly statically rather than dynamically typed, much to our public shame. Thus begins the eternal struggle to preserve duck typing in a world that hates bugs.
    • The beartype package now contains a top-level py.typed file, publicly declaring this package to be PEP 561-compliant.
  • Subscripted generics (i.e., user-defined generics subscripted by one or more type hints), resolving issue BeartypeDecorHintPepSignException and sub classing typing.Generic #29 kindly submitted by indefatigable test engineer and anthropomorphic Siberian Husky @eehusky. Since it's best not to ask too many questions about subscripted generics, we instead refer you to the issue report that nearly broke a Canadian man.

Compatibility Broken

  • None. This release preserves backward compatibility with the prior stable release.

Packaging Improved

  • New optional installation-time extras, enabling both beartype developers and automation tooling to trivially install recommended (but technically optional) dependencies. These include:
    • pip install -e .[dev], installing beartype in editable mode as well as all dependencies required to both locally test beartype and build documentation for beartype from the command line.
    • pip install beartype[doc-rtd], installing beartype as well as all dependencies required to build documentation from the external third-party Read The Docs (RTD) host.
  • Homebrew- and MacPorts-based macOS installation. Our front-facing README.rst file now documents beartype installation with both Homebrew and MacPorts on macOS, entirely courtesy the third-party Homebrew tap and Portfile maintained by build automation specialist and mild-mannered student @harens. Thanks a London pound, Haren!

Features Added

  • Public beartype.cave types and type tuples, including:

    • beartype.cave.CallableCTypes, a tuple of all C-based callable types (i.e., types whose instances are callable objects implemented in low-level C rather than high-level Python).
    • beartype.cave.HintGenericSubscriptedType, the C-based type of all subscripted generics if the active Python interpreter targets Python >= 3.9 or beartype.cave.UnavailableType otherwise. This type was previously named beartype.cave.HintPep585Type before we belatedly realized this type broadly applies to numerous categories of PEP-compliant type hints, including PEP 484-compliant subscripted generics.

Features Optimized

  • O(n)O(1) exception handling. @beartype now internally raises human-readable exceptions in the event of type-checking violations with an O(1) rather than O(n) algorithm, significantly reducing time complexity for the edge case of invalid large sequences either passed to or returned from @beartype-decorated callables. For forward compatibility with a future version of beartype enabling users to explicitly switch between constant- and linear-time checking, the prior O(n) exception-handling algorithm has been preserved in a presently disabled form.
  • O(n)O(1) callable introspection during internal memoization. @beartype now avoids calling the inefficient stdlib inspect module from our private @beartype._util.cache.utilcachecall.callable_cached decorator memoizing functions throughout the beartype codebase. The prior O(n) logic performed by that call has been replaced by equivalent O(1) logic performed by a call to our newly defined beartype._util.func.utilfuncargsubmodule, optimizing function argument introspection without the unnecessary overhead ofinspect`.
  • Code object caching. @beartype now temporarily caches the code object for the currently decorated callable to support efficient introspection of that callable throughout the decoration process. Relatedly, this also has the beneficial side effect of explicitly raising human-readable exceptions from the @beartype decorator on attempting to decorate C-based callables, which @beartype now explicitly does not support, because C-based callables have no code objects and thus no efficient means of introspection. Fortunately, sane code only ever applies @beartype to pure-Python callables anyway. ...right, sane code? Right!?!?

Features Deprecated

  • The ambiguously named beartype.cave.HintPep585Type type, to be officially removed in beartype 0.1.0.

Issues Resolved

  • Unsafe str.replace() calls. @beartype now wraps all unsafe internal calls to the low-level str.replace() method with calls to the considerably safer high-level beartype._util.text.utiltextmunge.replace_str_substrs() function, guaranteeing that memoized placeholder strings are properly unmemoized during decoration-time code generation. Thanks to temperate perennial flowering plant @Heliotrop3 for this astute observation and resolution to long-standing background issue replace_str_substrs #11.
  • KeyPool release validation. @beartype now validates that objects passed to the release() method of the private beartype._util.cache.pool.utilcachepool.KeyPool class have been previously returned from the acquire() method of that class. Thanks to @Heliotrop3, the formidable bug assassin, for their unswerving dedication to the cause of justice with this resolution to issue Improving KeyPool's Saftey #13.
  • Least Recently Used (LRU) cache. @beartype now internally provides a highly microoptimized Least Recently Used (LRU) cache for subsequent use throughout the codebase, particularly with respect to caching iterators over dictionaries, sets, and other non-sequence containers. This resolves issue Implement LRU Caching #17, again graciously submitted by open-source bug mercenary @Heliotrop3.
  • Callable labelling. @beartype now internally provides a private beartype._util.func.utilfuncorigin.get_callable_origin_label getter synthesizing human-readable labels for the files declaring arbitrary callables, a contribution by master code-mangler @Heliotrop3 resolving issue Implementing get_callable_filename_or_placeholder #18. Thanks again for all the insidious improvements, Tyler! You are the master of everyone's code domain.
  • Release automation. Our release workflow has now been migrated from the unmaintained create-release GitHub Action to @ncipollo's actively maintained release-action, resolving issue Updating Continuous Deployment #22 kindly submitted by human-AI-hybrid @Heliotrop3.

Tests Improved

  • Microsoft Windows and macOS exercised under CI, resolving issue Generalizing Github actions workflow to MacOS and Windows #21. Since doing so increases our consumption of Microsoft resources that we care deeply about, care has been taken to reduce the cost of our CI workflow. This includes:
    • Replacing our prior use of the external third-party tox-gh-actions GitHub Action streamlining tox usage with our own ad-hoc build matrix that appears to be simpler and faster despite offering basically identical functionality.
    • Removing our prior installation of optional dependencies, especially including NumPy. Yeah. Let's not do that anymore.
      Thanks to dedicated issue reporter @Heliotrop3 for his unsustainable deep-code trawling of the beartype codebase for unresolved FIXME: comments.
  • PyPy 3.7 exercised under CI. Our tox and GitHub Actions-based continuous integration (CI) configurations now both correctly exercise themselves against both PyPy 3.6 and 3.7, resolving the upstream Installing pypy3 failed with "Error: IS_WINDOWS is not defined" actions/setup-python#171 issue for beartype.
  • CI thresholded. Our CI configuration now caps tests to a sane maximum duration of time to avoid a repeat of the pull request we do not talk about here. Okay, it was refactor: Implement threading.Lock #23. I blame only myself.
  • New functional tests, including:
    • A CPython-specific mypy functional test, optionally exercising our conformance to static type-checking standards when the third-party mypy package is installed under CPython. This test is sufficiently critical that we perform it under our CI workflow, guaranteeing test failures on any push or PR violating mypy expectations.
    • A README.rst functional test, optionally exercising the syntactic validity of our front-facing README.rst documentation when the third-party docutils package (i.e., the reference reST parser) is installed. This test is sufficiently expensive that we currently avoid performing it under our CI workflow.
  • New unit tests, including:
    • Text munging unit tests, exercising the private beartype._util.text.utiltextmunge submodule with lavish attention to regex-based fuzzy testing of the critical number_lines() function. Humble git log shout outs go out to @Heliotrop3 for this mythic changeset that warps the fragile fabric of the GitHub cloud to its own pellucid yet paradoxically impenetrable intentions, resolving issue test: Add test cases for utiltextmunge #24.

Documentation Revised

  • Sphinx skeleton. The beartype repository now defines a largely unpopulated skeleton for Sphinx-generated documentation formatted as reST and typically converted to HTML to be hosted at Read The Docs (RTD), generously contributed by @felix-hilden, Finnish computer vision expert and our first contributor! This skeleton enables:
    • An HTTP 404 redirect page on missing page hits.
    • The standard retinue of builtin Sphinx extensions (e.g., autodoc, viewcode).
    • MathJax configured for remote implicit downloads.
    • Napolean configured for NumPy-formatted docstrings.
    • An optional dependency on sphinx_rtd_theme, a third-party Sphinx extension providing RTD's official Sphinx HTML.
    • A badge (i.e., shield, status icon) on our front-facing README.rst documentation signifying the success of the most recent attempt to build and host this skeleton at RTD.
    • A top-level sphinx script, building Sphinx-based package documentation when manually run from the command line by interactive developers.
  • A beautiful banner graphic that makes grown adults openly weep, featuring the official beartype mascot "Mr. Nectar Palm" – again courtesy @felix-hilden, because sleep is for the weak and Felix has never known the word.
  • A new prefacing "tl;dr" section that's redundant with numerous other sections, but we're surprisingly okay with that.
  • A new "Usage" section that accidentally became a new tutorial and division of the existing "Overview" section into various subsections highlighting tradeoffs between beartype and existing type checkers, resolving clarity concerns raised by @kevinjacobs-progenity at issue The Future Sound of Beartype: Going Deep on Type-checking #7. Thanks for the invaluable commentary, Kevin!
  • A new "Frequently Asked Questions (FAQ)" section, inspired by the admission from several prospective users that they have utterly no idea what @leycec is talking about. Fair play, users. You win this round.
  • A new "Workflow" subsection of the "Developer" section, listing developer-specific instructions for forking, cloning, installing, modifying, and submitting PRs for beartype in a live manner.
  • Properly rendered code blocks, kindly reported by humane human extraordinaire @harens in discussion topic README notes #28. Thanks and may the little-seen English sun eternally shine upon ye, Haren!

API Changed

  • Added:
    • beartype.cave.CallableCTypes.
    • beartype.cave.HintGenericSubscriptedType.
  • Deprecated:
    • beartype.cave.HintPep585Type.

@rsokl
Copy link

rsokl commented Apr 12, 2021

Hello! I love this project!

Given that you plan to target tensor/array-likes post 1.0.0, I wanted to put on your radar a tensor typing meeting, where we try to wrap our arms around variadic types, type arithmetic, annotating broadcastable shapes, and more – all things that need a real bear hug 🐻 🐻 .

The most concrete thing to come out of this so far is PEP 646: Variadic Generics (still under review, but seems like it will go the distance).

I hope that this thread was the appropriate place to post this. I will happily post elsewhere (or nowhere) if it is not.

@leycec
Copy link
Member Author

leycec commented Apr 13, 2021

Oh! It's my favourite Ryan! This may sound doubtful, but I fondly remember when you starred @beartype. We were just starting to explode (in the best possible way) at that point, and then somebody from MIT starred us. Insert {oh-face-here}.

Thanks so much for the heads up on PEP 646, too. I couldn't help myself and just gave my hot take on the mailing list. I'm afraid the knives may have come out. Let's see if my grim yet constructive commentary makes it through moderation. It's popcorn time.

Sadly, PEP 646 got the Guido support, so it's more-or-less fait accompli. That's fine... I guess. Whatever PEP eventually lands, we'll wholeheartedly endorse and support it. It just would've been nice to get something a bit more practical, forward-compatible, and immediately useful. </sigh>

Thanks again, Ryan! We have thrilling changes in store for everyone just around the corner. Yup: it's our own homebrew tensor validation API. 😁

@rsokl
Copy link

rsokl commented Apr 15, 2021

Well shucks, I am glad we were able to meet then! It has been exciting to see this project grow in popularity, and I am glad that I was able help create a fond memory 😄 And I'll be sure to stay tuned for your tensor-validation API!

@leycec
Copy link
Member Author

leycec commented May 25, 2021

Uncork that expensive and only mildly carcinogenic champagne bottle you've been saving up for special data science occasions, folks. @leycec has finally done something that users actually want. 🍾 🍾 🍾

Introducing... blast beat drumroll, please! beartype 0.7.0, featuring the world's first PEP-compliant validation API. It's also:

  • The world's first Turing-complete type hinting API. Design your own PEP-compliant type hints with compact two-liners validating literally anything. From simple builtin scalars like integers and strings to complex data structures like multidimensional NumPy arrays and on-disk Pandas DataFrames, validate custom project constraints now without waiting for the open-source community to officially standardize and implement those constraints for you. 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.
  • The world's first tensor type hinting API. Beartype validators were designed from the ground up for Python's scientific stack – because the only constraints here are those we impose upon ourselves. That's why most of the examples both below and in our validator showcase validate the core data structure of Science Itself. We know it; we usually love it, even when we're using scipy sparse matrices instead. Yup! It's numpy.ndarray, now validated to actually be what you want it to be. 😜
  • Optimally efficient with no hidden costs, fees, or tradeoffs. Because beartype users, being the best, get the best.

Tensor Validation: Now Trending on Beartype

But rather than mansplain at you all summer evening, I'll just leave this example bombshell here:

# Import the requisite machinery.
from beartype import beartype
from beartype.vale import Is
from typing import Annotated
import numpy as np

# Type hint matching any two-dimensional NumPy array of floats of arbitrary
# precision. Yup. That's a beartype validator, folks!
Numpy2DFloatArray = Annotated[ndarray, Is[lambda array:
    array.ndim == 2 and np.issubdtype(array.dtype, np.floating)]]

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

    *Don't ask.*
    '''

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

</micdrop>

Here's the soft landing. Create a beartype validator by:

  1. Importing:
    • from beartype.vale import Is
    • from typing import Annotated
  2. Declaring MyValidator = Annotated[{cls}, Is[lambda obj: {test_expr}], where:
    • {cls} is any arbitrary class (e.g., str, numpy.ndarray).
    • {test_expr} is any arbitrary expression evaluating to a boolean (e.g., len(obj) <= 80, obj.dtype == np.dtype(np.float64)).
  • Use MyValidator wherever you use standard type hints.

As a fully PEP-compliant type hint, MyValidator is safely interoperable with other PEP-compliant type hints and usable wherever other PEP-compliant type hints are usable – including deeply nested in those type hints.

Tensor Validation: Make This Faster, Beartype!

We're not done yet. The above example is mildly inefficient, because it calls your user-defined lambda function once for each call to a @beartype-decorated callable for each annotation of that callable referencing Numpy2DFloatArray. Can we do better?

We can. This is beartype, so we can always do better. Behold, alternate syntax for semantically type-hinting the same data structure but avoiding any pure-Python callable calls and thus the fastest tensor validator that has ever existed:

# Import the requisite machinery.
from beartype import beartype
from beartype.vale import IsAttr, IsEqual
from typing import Annotated
import numpy as np

# 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.)
#
# Internally, @beartype generates type-checking code resembling:
#     isinstance(obj, ndarray) and
#     obj.ndim == 2 and (
#         obj.dtype.type == np.float32 or
#         obj.dtype.type == np.float64
#     )
Numpy2DFloatArray = Annotated[ndarray,
    IsAttr['ndim', IsEqual[2]] &
    IsAttr['dtype',
        IsAttr['type', IsEqual[np.float32] | IsEqual[np.float64]]]
]

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

    *Don't ask.*
    '''

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

</micdrop_harder>

Full-fat O(n) Type-checking: Game Over, Man!

We're still not done yet, even though the sweat is now pouring down your forehead, your five-year old is squalling something about vomit unicorns, and your wife is giving you the frown look that portends a troubled bedtime.

Because beartype validators are Turing-complete, you can do literally anything with them – including things @leycec doesn't want you to like full-fat O(n) type checking:

# Import the requisite machinery.
from beartype import beartype
from beartype.vale import Is
from typing import Annotated

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

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

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

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

In beartype validators, @leycec created his own arch-nemesis: @NegaLeycec. When @leycec made a public API so powerful it had the latent energy to destroy his own online legacy, only you can step away from the brink and do the right thing.

Because data scientists don't let data scientists type-check in O(n) time. Don't be that one guy always lazily splayed out on the musty couch in Office #12. Please. 🥺

Obligatory Shoutouts

@Heliotrop3, @HETHAT, @MartinPdeS, @Saphyel, @aiporre, @albanie, @djhenderson, @eehusky, @felix-hilden, @harens, @rsokl, @terrdavis, @thanakijwanavit:

Beartype now does everything everyone wanted. Take all my GitHub karma and may all your Git commits foreevermore be blessed by passing unit tests.

@felix-hilden
Copy link
Contributor

DAYUM! Congrats 😄 Excited to try this out!

@harens
Copy link
Contributor

harens commented May 25, 2021

Packaging status

For all those eagerly awaiting to try the new version...

MacPorts: macports/macports-ports@7c42a28 (Project page)

sudo port install py-beartype

Pre-built binaries are available for macOS Big Sur all the way down to Snow Leopard which was released back in 2009. Yes, you did read that correctly. Yes, MacPorts is indeed better than Homebrew :)

Homebrew: beartype/homebrew-beartype@eb0d97f

brew install beartype/beartype/beartype

Binaries are available for macOS Big Sur, Catalina, and Linux (all x86_64).


As always, thank you @leycec (and the other contributors) for your hard work on this amazing project!

@leycec
Copy link
Member Author

leycec commented May 26, 2021

Pre-built binaries are available for macOS Big Sur all the way down to Snow Leopard which was released back in 2009.

So. The prophesied time has finally come. My wife must now resurrect the Core 2 Duo 2010 MacBook from its plush velvet cryogenic storage chamber in our Hall of Ancient and Venerable Laptops. ...yup, the bedroom closet

@harens has made you useful again, 2010 MacBook. 🤗

Yes, you did read that correctly. Yes, MacPorts is indeed better than Homebrew :)

dem is fightin wurds

Eternal gratitude, endless summer vibrations, and post-life positive karma to Haren for his tireless dedication! Wut a gr8 m8.

@leycec
Copy link
Member Author

leycec commented Aug 18, 2021

Beartype 0.8.0 just dropped – and you were there to see it:

$ pip install --upgrade beartype

When your children ask about this moment, tell them they wouldn't believe the things you've seen online. Yes, things like:

  • Typed NumPy arrays (i.e., numpy.typed.NDArray type hints). The @beartype decorator now transparently supports the third-party numpy.typing.NDArray type hint newly introduced by NumPy ≥ 1.21.0, resolving issue Cannot use numpy.typing.NDArray type hints #42 kindly submitted by NumPy extraordinaire @Antyos. Note that usage of typed NumPy arrays under Python < 3.9.0 requires installation of the third-party typing-extensions package, which @beartype will then automagically detect and leverage internally.
  • Typing backports (i.e., typing_extensions type hints). The @beartype decorator now transparently supports all public type hints of the third-party typing_extensions package, resolving issue Regarding Python ≥ 3.9 requirement for validators #34 kindly submitted by Ryan Soklaski (@rsokl) of considerable MIT Beaver Works Summer Institute and Python Like You Mean It fame.
  • Python < 3.9.0 beartype validators. The @beartype decorator now portably supports beartype validators (i.e., beartype.vale objects annotating typing{_extensions}.Annotated type hints) across all Python versions, also resolving issue Regarding Python ≥ 3.9 requirement for validators #34 kindly submitted by Ryan Soklaski (@rsokl). hallowed be his username Note that usage of beartype validators under Python < 3.9.0 requires:
    • Installation of the third-party typing-extensions package.
    • Annotation of beartype.vale objects by typing_extensions.Annotated (rather than typing.Annotated).

What we're saying is: pip install typing-extensions. Your data pipeline will thank us later.

Badge

Beartype also now sports an official badge (i.e., shield intended for display by downstream projects):

Beartype badge says, "You're okay, bro."

You may now proudly confirm your commitment to littering your front page with an incomprehensible smear of project shields using this reST- and Markdown-friendly text. It looks resplendent on your Git[Hub|Lab] repo, does it not? It does.

Unsurprisingly, @posita made that... because what doesn't @posita do? Nuthin'! I'm pretty sure that means @posita does everything.

tl;dr:

numpy.typing.NDArray type hints are a go. Backported typing_extensions type hints are a go. Beartype validators are a go under all Python versions via the typing_extensions.Annotated backport. Beartype badge is a go.

Everything is a go. Go, go, go! Do great science and engineering, great people.

@posita
Copy link
Collaborator

posita commented Aug 19, 2021

Badge spotted in the wild! https://pypi.org/project/dycelib/ 😉

@leycec
Copy link
Member Author

leycec commented Aug 21, 2021

Yes! It's happening. The badge is spreading like a slime mold before our very eyes.

Relatedly: everyone who routinely rolls dice (...squints eyes at tabletop role-players, board gamers, and compulsive gamblers), check out @posita's spicy dyce DSL for simulating dice rolls:

>>> from dyce import H
>>> d6 = H(6)     # histogram for a standard six-sided die
>>> 2@d6 * 3 - 4  # histogram of average 2d6 × 3 - 4 outcomes
H({2: 1, 5: 2, 8: 3, 11: 4, 14: 5, 17: 6, 20: 5, 23: 4, 26: 3, 29: 2, 32: 1})

🌶️ 🌶️ 🌶️

@leycec
Copy link
Member Author

leycec commented Oct 22, 2021

Beartype 0.9.0 has landed with a soft, mildly disconcerting gloop sound.

$ pip install --upgrade beartype

Coroutines! Asynchrous generators! Precision-agnostic numpy.typing.NDArray[numpy.floating] typed NumPy arrays! Portable beartype validators! The new beartype.vale.IsSubclass validator! Deep typing.Type[...] and type[...] type-checking! Deep typing.TypeVar('T', bound={type}) and typing.TypeVar('T', {type1}, ..., {typeN}) type-checking! Shallow PEP 589 typing.TypedDict type-checking! PEP 585 deprecation warnings explained! PEP 585 type hints deduplicated! PEP 586 typing_extensions.Literal and PEP 593 typing_extensions.Annotated backports!

All this (and more) is within your iron-fisted grasp. Will you shake the outstretched bear paw that only wants to dramatically improve the quality of your world-shattering codebase? Only your intrepid team of crack code commandos will decide the ultimate fate of... Python. 2021. Q4.

@leycec
Copy link
Member Author

leycec commented Oct 22, 2021

Shout-outs to the based bear team: @KyleKing, @antonagestam, @Branii, @pbourke, and @posita. Your legend grows by the day.

@OutfoxedAdmin
Copy link

I love you beartype <3

@leycec
Copy link
Member Author

leycec commented Feb 10, 2022

beartype 0.10.0 has landed with a wet squishy splash suggestive of either imminent disaster or radical progress towards a bold QA future. You decide.

$ pip install --upgrade beartype

But first, this release is...

In Homage to Dale Pietak

My stunning Canadian mother-in-law fled the Soviet invasion of Lithuania...

red army enters vilnius (june 15th, 1940)

...on horseback across the Lithuanian alps as an emaciated 5-year-old girl.

it probably looked like this
probably not her, so squint while pretending

This release is for Dale.

And now, a pivotal moment that could make you question every cherished assumption you once held dear but are now throwing away:

@leycec did something.

A Configuration API for the Ages (Go Figure)

beartype 0.10 introduces a new configuration API enabling you to (...wait for it) configure type-checking on a per-callable decoration-time basis. First, the tl;dr or it didn't happen:

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

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

So, it happened. Secondly, it happened efficiently. Beartype configurations are self-memoizing:

>>> BeartypeConf() is BeartypeConf()
True
>>> BeartypeConf(is_debug=True) is BeartypeConf(is_debug=True)
True

Moreover, beartype subdecorators returned by configuring @beartype are also self-memoizing:

>>> beartype(conf=BeartypeConf()) is beartype(conf=BeartypeConf())
True
>>> beartype(conf=BeartypeConf(is_debug=True)) is beartype(conf=BeartypeConf(is_debug=True))
True

This means you don't need to alias, cache, or store anything. We did that for you, because @beartype never stopped believing in you. Of course, you can alias beartype subdecorators if you really must for readability: e.g.,

# I grow weary of repeating myself, @beartype.
beartype_On = beartype(conf=BeartypeConf(
    strategy=BeartypeStrategy.On))

# This is the sound of sweet justice.
@beartype_On
def typecheck_slowly(this_gonna_hurt: list[int]) -> int:
    return itertools.accumulate(this_gonna_hurt)

Nothing Actually Works, Does it?

As the above tl;dr implies, the O(n) linear-time and O(log n) logarithmic-time type-checking strategies (i.e., BeartypeStrategy.On and BeartypeStrategy.log n) have yet to be implemented. They exist to:

  • (A) demonstrate that we've heard your plaintive cry in the heat of the night for fuller, fatter, sluggish type-checking alternatives that chug CPU cycles like @leycec chugs obscure JRPGs, because you promise you know what you're doing... and you mean it this time.
  • (B) enable you to begin configuring @beartype accordingly throughout your codebase.

We'll gradually catch up to your codebase. When we do, you'll know because everything will grind to a halt and Twitter will erupt with outrage.

What API Horrors Have You Unleashed Now!?

To recap, the __init__() method of the beartype.BeartypeConf dataclass currently accepts these parameters:

  • An optional strategy parameter whose value must be a BeartypeStrategy enumeration member (defaulting to BeartypeStrategy.O1). This is how you notify @beartype of how you want us to type-check your callable. Strategies include:
    • BeartypeStrategy.On, a new O(n) linear strategy deterministically type-checking all items of a container. This strategy is currently unimplemented (but will be implemented by a future @beartype release).
    • BeartypeStrategy.Ologn, a new O(lgn) logarithmic strategy probabilistically type-checking a randomly selected number of items j of a container obj such that j = log(len(obj)). This strategy is currently unimplemented (but will be implemented by a future beartype release).
    • BeartypeStrategy.O1, our default O(1) constant-time strategy probabilistically type-checking a single randomly selected item of a container. You currently enjoy this strategy. Since this is the default, this strategy need not be explicitly configured. Of course, you're going to do that anyway, aren't you? </sigh>
    • BeartypeStrategy.O0, disabling type-checking for a callable by reducing @beartype to the identity decorator for that callable. Although currently useless, this strategy will usefully let you prevent select callables from being type-checked by our as-yet-unimplemented import hook. When implemented, that hook will type-check all callables in a given package by default. Some means is needed to prevent that from happening for specific callables. This is that means.
  • An optional keyword-only is_debug boolean parameter (defaulting to False). Ever been itching to know what exactly @beartype is or is not doing for you? This scratches that itch. When enabled, @beartype:
    • Pretty-prints to stdout (standard output) the code for the wrapper function dynamically generated by @beartype that type-checks the decorated callable, including line number prefixes for readability.
    • Exposes those wrapper functions to debugging via a Python debugger (e.g., pdb). Thanks to a phenomenal pull request by the dynamic dual threat that is @posita + @TeamSpen210, @beartype now conditionally caches the bodies of type-checking wrapper functions with the standard (albeit poorly documented) linecache module. Thanks so much! Bear Clan 2022!!!

Let's admit we're all exhausted at this point. Ideally, I'd be done here. Sadly, we're only just beginning. The 0.10.0 release cycle was a titanic struggle between the forces of API bloat and bloated apathy.

API bloat won. Introducing...

A Functional Type-checking API that Functions

beartype 0.10 introduces a new functional API enabling you to type-check anything anytime against any PEP-compliant type hints. First, the tl;dr or it didn't happen:

   # Import the functional API.
   >>> from beartype import is_bearable, die_if_unbearable

   # Test whether any object satisfies any type hint at any time.
   >>> is_bearable(['Things', 'fall', 'apart;'], list[str])
   True
   >>> is_bearable(['the', 'centre', 'cannot', 'hold;'], list[int])
   False

   # Raise an exception if any object violates any type hint at any time.
   >>> die_if_unbearable(['And', 'what', 'rough', 'beast,'], list[str])
   >>> die_if_unbearable(['its', 'hour', 'come', 'round'], list[int])
   beartype.roar.BeartypeAbbyHintViolation: Object ['its', 'hour', 'come',
   'round'] violates type hint list[int], as list index 0 item 'its' not
   instance of int.

😮

Caveat emptor: the current implementation is painfully unoptimized, but will be optimized shortly for certain optimistic definitions of "shortly." Since a slow API that exists >>>> a fast API that doesn't exist, we hope everyone will bear ...heh with the temporary slowdown. The functional API should more than fast enough for general-purpose use, but cracks will show if you throw it at a tight inner loop.

To recap:

  • beartype.abby.is_bearable() strictly returns a boolean signifying whether the passed arbitrary object satisfies the passed type hint or not. Exceptions are raised only when the passed type hint is bad. But you wouldn't pass bad type hints! You're a good person.
  • beartype.abby.die_if_unbearable() raises the new beartype.roar.BeartypeAbbyHintViolation exception when the passed arbitrary object violates the passed type hint.

To sweeten the sour deal, all of the above also accept an optional conf parameter configuring the type-checking performed by that call: e.g.,

# In theory, this type-checks the entire list in linear time.
# In practice, we haven't actually implemented that yet.
# That's @beartype for you: it promises much,
# implements some, and documents even less.
is_bearable(
    obj=['Things', 'fall', 'apart;'],
    hint=list[str],
    conf=BeartypeConf(BeartypeStrategy.On),
)

Even the 12 year-old child prodigy from Switzerland who's reading this against the express wishes of her overprotective nursemaid is exhausted by now. Nonetheless, the trainwreck of API inevitability continues.

Introducing...

A Compatibility API that Avoids Breaking the Future

If you @beartype under Python < 3.9, you've taken an arrow to the knee from our unfairly maligned PEP 585 deprecations:

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

Previously, you had to do something about that. Now, let @beartype do everything for you by globally replacing all imports from the standard typing API with the exact same imports from our new beartype.typing API: e.g.,

# Just do this...
from beartype import typing

# ...instead of this.
#import typing

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

# ...instead of this.
#from typing import Dict, FrozenSet, List, Set, Tuple, Type

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

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

Currently, that's all beartype.typing does. Going forward, beartype.typing will do significantly more for your third-quarter crypto profit margins. what am i even saying This includes:

The endless wheel of suffering continues as...

bearboto3 Joins the Chat

All hail West Texas and the newest official subproject under the soggy @beartype umbrella: @paulhutchings' unbeatable bearboto3, @beartype-driven runtime type-checking for Amazon's Boto3 AWS API.

Paul Hutchings is fearless so you don't need to be. But wait! The cosmic tesseract of causality churns on as...

@posita Joins the Chat

Let's see a 21-gun salute (surely those are blanks!?) for @posita, the first and only official member of The Organization Presently Known As @beartype. @posita's gritty post-urban pull requests both here and elsewhere are deserving of all the handclaps these sweaty palms can still muster. 👏 👏

@leycec Passes Out on the Keyboard

its kinda like that

@leycec
Copy link
Member Author

leycec commented Feb 10, 2022

Shoutouts to the Burgeoning Bear Clan: @posita, @TeamSpen210, @paulhutchings, @qiujiangkun, @MrHemlock, @matanster, @mikaelho, @Masoudas, @mvaled, @mxlei01, @harshita-gupta, @shawwn, @NiklasRosenstein, @dycw, and @Jasha10.

@leycec
Copy link
Member Author

leycec commented Sep 20, 2022

beartype 0.11.0 has just impacted the fragile surface of PyPI and conda-forge, turning 😑 into 🤯. Since pip is our friend in all things:

$ pip install --upgrade beartype

So much typing goodness is in store for your codebase. Play the highlight real, boys! Alternately, read the infodump and weep for your free time.

Colour: Beyond Monochrome

Is that... No, but it couldn't be. Yes, but it is! It's thematically appropriate and aesthetically pleasing ANSII pigmentation in type-checking violations raised by @beartype:

ftw

Colour me impressed, @beartype. For safety, colourization only happens conditionally when standard output is attached to an interactive terminal. Let us know if you detest this scheme. B-b-but... how could you? It's beautiful.

Praise be to machine learning guru @justinchuby (Justin Chu) for his outstanding volunteerism in single-handedly making all this juiciness happen at PR #162. 😮

pyright: @beartype No Longer Disagrees with You

@beartype now officially supports two static type-checkers:

  • Mypy, Python's standard type-checker championed by the Guido himself.
  • pyright, Microsoft's highly tuned type-checker bundled with PyLance – Microsoft's bleeding-edge Python extension for VSCode.

VSCode's rampant popularity makes PyLance and thus pyright the new ad-hoc standard for Python typing. Microsoft's will be done. I'm pretty certain – but not certain – that @beartype is the first "large" Python project to support multiple competing static type-checkers. We lack common sense.

Unofficially, we don't advise doing this at home. The @beartype codebase is now a festering litter dump of # type: ignore[curse-your-cheating-eyes-mypy] and # pyright: ignore[TheseAreNotTheFalsePositivesYouAreLookingFor]. Avert thy eyes, all who code there.

Class Decoration: Save Your Wrists with @beartype

Rejoice, RSI-wracked buddies! @beartype now decorates classes, too – including non-trivial nested classes with self-referential annotations postponed under PEP 563, just 'cause:

from __future__ import annotations  # <-- never actually do this
from beartype import beartype

# One @beartype decoration to rule them all and in /dev/null bind them.
@beartype
class ImAClassWouldILie(object):
    def this_is_fine(self) -> ImAClassWouldILie:
        return self

    @staticmethod
    def static_methods_are_fine_too() -> ImAClassWouldILie:
        return ImAClassWouldILie()

    @classmethod
    def yup_class_methods_also_fine(cls) -> ImAClassWouldILie:
        return cls()

    @property
    def omg_property_methods_are_fine(self) -> ImAClassWouldILie:
        return self

    class ImANestedClassYouCanTellBecauseImNested(object):
        def wtf_beartype_how_is_this_fine(self) -> ImAClassWouldILie:
            return ImAClassWouldILie()

        def still_cant_believe_this_is_fine_either(self) -> (
            ImANestedClassYouCanTellBecauseImNested):
            return self

Say goodbye to decorating methods manually. Your wrists that are throbbing with pain will thank you.

But this isn't simply a nicety. If your codebase is object-oriented (...please Guido, let it be so), let us now suggest that you incrementally refactor your existing usage of @beartype from methods to classes. Why? Because:

  • PEP 673 (i.e., typing.Self). typing.Self explodes onto the typing scene with Python 3.11, enabling methods to trivially annotate that they return instances of their classes. typing.Self is probably the first in a new category of class-centric type hints. Exciting! But @beartype will only support typing.Self when decorating classes – not methods. Less exciting.
  • PEP 563. I mean, nobody should actually use PEP 563. That goes without say. But if you violate sanity, performance, and the prior maxim by actually using PEP 563, @beartype only supports the sort of heinous self-referential type hints seen above when decorating classes – not methods. Not that it matters, because you weren't using PEP 563 anyway. Right, guys? Right!?!?

beartype.door: The Decidedly Object-oriented Runtime-checker

Oh, boy. Now we hittin' the Hard Stuff™.

Has anyone ever tried to actually use type hints at runtime? Like, not merely annotate classes, callables, and attributes with type hints but actually use those type hints for a productive purpose? Anybody? Anybody? ...helllllllllllllo?

so cute

Fret not, bear bros. @beartype now enables anyone to introspect, query, sort, compare, type-check, or otherwise manhandle type hints at any time in constant time. Dare your codebase open... the DOOR (Decidedly Object-oriented Runtime-checker)? spooky Halloween sounds

# This is DOOR. It's a Pythonic API providing an object-oriented interface
# to low-level type hints that basically have no interface whatsoever.
>>> from beartype.door import TypeHint
>>> union_hint = TypeHint(int | str | None)
>>> print(union_hint)
TypeHint(int | str | None)

# DOOR hints have Pythonic classes -- unlike normal type hints.
>>> type(union_hint)
beartype.door.UnionTypeHint  # <-- what madness is this?

# DOOR hints can be classified Pythonically -- unlike normal type hints.
>>> from beartype.door import UnionTypeHint
>>> isinstance(union_hint, UnionTypeHint)  # <-- *shocked face*
True

# DOOR hints can be type-checked Pythonically -- unlike normal type hints.
>>> union_hint.is_bearable('The unbearable lightness of type-checking.')
True
>>> union_hint.die_if_unbearable(b'The @beartype that cannot be named.')
beartype.roar.BeartypeDoorHintViolation: Object b'The @beartype that cannot
be named.' violates type hint int | str | None, as bytes b'The @beartype
that cannot be named.' not str, <class "builtins.NoneType">, or int.

# DOOR hints can be iterated Pythonically -- unlike normal type hints.
>>> for child_hint in union_hint: print(child_hint)
TypeHint(<class 'int'>)
TypeHint(<class 'str'>)
TypeHint(<class 'NoneType'>)

# DOOR hints can be indexed Pythonically -- unlike normal type hints.
>>> union_hint[0]
TypeHint(<class 'int'>)
>>> union_hint[-1]
TypeHint(<class 'str'>)

# DOOR hints can be sliced Pythonically -- unlike normal type hints.
>>> union_hint[0:2]
(TypeHint(<class 'int'>), TypeHint(<class 'str'>))

# DOOR hints supports "in" Pythonically -- unlike normal type hints.
>>> TypeHint(int) in union_hint  # <-- it's all true.
True
>>> TypeHint(bool) in union_hint  # <-- believe it.
False

# DOOR hints are sized Pythonically -- unlike normal type hints.
>>> len(union_hint)  # <-- woah.
3

# DOOR hints reduce to booleans Pythonically -- unlike normal type hints.
>>> if union_hint: print('This type hint has children.')
This type hint has children.
>>> if not TypeHint(tuple[()]): print('But this other type hint is empty.')
But this other type hint is empty.

# DOOR hints support equality Pythonically -- unlike normal type hints.
>>> from typing import Union
>>> union_hint == TypeHint(Union[int, str, None])
True  # <-- this is madness.

# DOOR hints support comparisons Pythonically -- unlike normal type hints.
>>> union_hint <= TypeHint(int | str | bool | None)
True  # <-- madness continues.

# DOOR hints are semantically self-caching.
>>> TypeHint(int | str | bool | None) is TypeHint(None | bool | str | int)
True  # <-- blowing minds over here.

Praise be to Harvard microscopist and lead @napari dev @tlambert03 (Talley Lambert) for his phenomenal volunteerism in single-handedly building a new typing world. He ran the labyrinthian gauntlet of many, many painful PRs so that you didn't have to.

Napari: for all your multi-dimensional image viewing needs.

beartype.door: never leave typing without it.

beartype.peps: This Day All PEP 563 Dies

So. beartype.door. That's great and all. Usable type hints. Blah, blah, blah. But what if you actually want to use typing-centric Python Enhancement Proposals (PEPs) that break runtime, were never intended to be used at runtime, and have no official runtime API? Of course, I speak of PEP 563.

Fret not even more, bear bros. @beartype now enables anyone to resolve PEP 563-postponed type hints without actually needing to sleep on a bed of nails with this GitHub issue as your only comfort:

# In "horrorshow.py":
from __future__ import annotations  # <-- never actually do this

def sad_function_is_sad() -> str | bytes | None:
    if 'sad':
        return "You can't spell 'sadness' without madness (excluding the 'm')."
    else:
        return b'This cannot be! But it is! An unreachable condition erupts.'

print('Uh oh. We got postponed type hints here:')
print(repr(sad_function_is_sad.__annotations__['return']))
print()

# Your time is over, PEP 563.
from beartype.peps import resolve_pep563

# Make the bad postponed type hints go away, Bear Daddy.
resolve_pep563(sad_function_is_sad)

print('Suck it, PEP 563. Only real type hints need apply:')
print(repr(sad_function_is_sad.__annotations__['return']))

Now run that, because you trust @leycec implicitly. You do trust @leycec implicitly, don't you? 🥲

$ python3.10 horrorshow.py
Uh oh. We got postponed type hints here:
'str | bytes | None'

Suck it, PEP 563. Only real type hints need apply:
str | bytes | None

beartype.peps: the final word on PEP 563.

Lastly but not leastly...

we doin' this

...to financially feed @leycec and his friendly @beartype through our new GitHub Sponsors profile. Come for the candid insider photos of a sordid and disreputable life in the Canadian interior; stay for the GitHub badge and warm feelings of general goodwill.

Cue hypnagogic rave music that encourages fiscal irresponsibility.

Shoutouts to the Beasts in Back

Greets to the bear homies: @tlambert03, @justinchuby, @rskol, @langfield, @posita, @braniii, @dosisod, @bitranox, @jdogburck, @da-geek-incite, @MaxSchoenau, @gelatinouscube42, @stevemarin, @rbroderi, @kloczek, @twoertwein, @wesselb, and @patrick-kidger. 🏩

@Jasha10
Copy link

Jasha10 commented Sep 20, 2022

Thanks @leycec!
FYI there's a typo in the class decoration example above:

@@ -21,7 +21,7 @@

     class ImANestedClassYouCanTellBecauseImNested(object):
         def wtf_beartype_how_is_this_fine(self) -> ImAClassWouldILie:
-            return ImAClassWouldILie
+            return ImAClassWouldILie()

         def still_cant_believe_this_is_fine_either(self) -> (
             ImANestedClassYouCanTellBecauseImNested):

@leycec
Copy link
Member Author

leycec commented Sep 21, 2022

@Jasha10: That's a teaching moment. Never trust a class named ImAClassWouldILie. 😁

I jest, of course. Thanks so much for the fast fix! I've patched up the original example, which should now lie a bit less than it did before.

@leycec
Copy link
Member Author

leycec commented Sep 21, 2022

Big bear hugs to machine learning paragons @KyleKing and @albanie (Samuel Albanie) for their incredibly generous support through GitHub Sponsors. By your charity, hard things become easier.

it be like this

@leycec
Copy link
Member Author

leycec commented Jan 18, 2023

beartype 0.12.0 gently descends like an AI-upscaled snowflake to PyPI, where goodness lives:

$ pip install --upgrade beartype

We've got configuration and exception APIs so well-documented you won't believe it's @beartype! All these seductive lies and more are yours for only the low price of your vanishing time, invaluable attention, and lifetime of regret. Reading this will only take five minutes. Okay, maybe ten. Fifty at the most. Surely, no more than a full day.

Alternately, read the lore dump and cry as an unedited wave of raw plaintext assaults you.

But First, Soothing AI Art that Obsoletes All Human Effort

Cancerous mandelbulb bears? You gazed upon it in befuddlement here first, I'm pretty sure:

image

Brought to you by some AI somewhere. Alright, alright! It's Midjourney v4. Yup. We shillin'.

Clearly, Midjourney is internally using @beartype. How else can one explain the pristine beauty one witnesses above? One can't. Clearly. This paragraph is one big sprawling lie.

After you successfully "read" each of the following sections, GitHub will drip-feed you another cancerous mandelbub bear from beyond the wall of sleep. That's right. We're gamifying this with a dopamine feedback cycle encouraging addictive and self-destructive behaviour. Let's begin.

Configuration: Control @beartype, Because You Know Better

If I had a toonie Canada: incoming for every time someone loudly hated the glaring kaleidoscope of colour beartype 0.11.0 decorates type-checking violations with (without your permission), I'd be guzzling apple cider in Cancún. I can almost taste that sweet nectar, full-bodied yet surprisingly non-alcoholic. I can almost bask in the warm offshore breeze. The Sun herself beams with joy. Ahhhhhhh...

Wait. Where am I? Who am I? Oh, right. I am @leycec and this is @beartype. </sigh>

Thankfully, beartype 0.12.0 has got your hairy back. Return to the monochrome days of yore, where things were better and your eyeballs didn't bleed. Bask in the applause of your coworkers as you loudly take all this credit (and more):

# Import the requisite machinery.
from beartype import beartype, BeartypeConf

# Dynamically create a new @monobeartype decorator disabling colour.
monobeartype = beartype(conf=BeartypeConf(is_color=False))

# Decorate with this decorator rather than @beartype everywhere.
@monobeartype
def muh_colorless_func() -> str:
    return b'In the kingdom of the blind, you are now king.'

Cancerous mandelbulb bear.

image

The good times won't stop there, however much you might want them to. beartype 0.12.0 also gives configuration options for...

PEP 484's Implicit Numeric Tower: It's Baaaack

All two of you that have read the Black Bible of Typing (AKA PEP 484), please raise your paws. Okay. It's just me, isn't it? I'll pretend there are at least two of you to make myself feel better.

PEP 484 contains this suspiciously opinionated section I dub the implicit numeric tower. And I quote Guido:

PEP 3141 defines Python’s numeric tower, and the stdlib module numbers implements the corresponding ABCs (Number, Complex, Real, Rational and Integral). There are some issues with these ABCs, but the built-in concrete numeric classes complex, float and int are ubiquitous (especially the latter two :-).

Rather than requiring that users write import numbers and then use numbers.Float etc., this PEP proposes a straightforward shortcut that is almost as effective: when an argument is annotated as having type float, an argument of type int is acceptable; similar, for an argument annotated as having type complex, arguments of type float or int are acceptable. This does not handle classes implementing the corresponding ABCs or the fractions.Fraction class, but we believe those use cases are exceedingly rare.

When you find yourself embedding ASCII emoji like ":-)" in public standards, you know that you have done wrong and should now abase yourself with a televised ablution involving skintight burlap sacks and burly leather switches. Instead, that unsubstantiated drivel actually got published! I sigh.

Thankfully, beartype 0.12.0 has got your hairy back... again. You could just write int | float rather than do anything Guido says. Or you could ignore sound advice and return to the bad old days of PEP 484, where writing float when you really mean int | float saves you from a vicious downward spiral of carpal tunnel syndrome and subsequent occupational bankruptcy. You ask and you receive:

# Import the requisite machinery.
from beartype import beartype, BeartypeConf

# Dynamically create a new @beartowertype decorator enabling the tower.
beartowertype = beartype(conf=BeartypeConf(is_pep484_tower=True))

# Decorate with this decorator rather than @beartype everywhere.
@beartowertype
def crunch_numbers(numbers: list[float]) -> float:
    return sum(numbers)

# This is now fine.
crunch_numbers([3, 1, 4, 1, 5, 9])

# This is still fine, too.
crunch_numbers([3.1, 4.1, 5.9])

Cancerous mandelbulb bear.

image

@beartype: Is It Actually Doing Anything At All, Really?

Have you ever wondered whether @beartype might just be pretending to do something... while in actuality doing nothing whatsoever? You are not alone. I too wonder this.

Thankfully, beartype 0.12.0 has got your hairy back... again and again. Force @beartype to show you what it's hiding behind your hairy back by printing out the type-checking code it dynamically generates on your behalf:

# Import the requisite machinery.
>>> from beartype import beartype, BeartypeConf

# Dynamically create a new @bugbeartype decorator enabling debugging.
# Insider D&D jokes in my @beartype? You'd better believe. It's happening.
>>> bugbeartype = beartype(conf=BeartypeConf(is_debug=True))

# Decorate with this decorator rather than @beartype everywhere.
>>> @bugbeartype
... def muh_bugged_func() -> str:
...     return b'Consistency is the bugbear that frightens little minds.'
(line 0001) def muh_bugged_func(
(line 0002)     *args,
(line 0003)     __beartype_func=__beartype_func, # is <function muh_bugged_func at 0x7f52733bad40>
(line 0004)     __beartype_conf=__beartype_conf, # is "BeartypeConf(is_color=None, is_debug=True, is_pep484_tower=False, strategy=<BeartypeStrategy...
(line 0005)     __beartype_get_violation=__beartype_get_violation, # is <function get_beartype_violation at 0x7f5273081d80>
(line 0006)     **kwargs
(line 0007) ):
(line 0008)     # Call this function with all passed parameters and localize the value
(line 0009)     # returned from this call.
(line 0010)     __beartype_pith_0 = __beartype_func(*args, **kwargs)
(line 0011)
(line 0012)     # Noop required to artificially increase indentation level. Note that
(line 0013)     # CPython implicitly optimizes this conditional away. Isn't that nice?
(line 0014)     if True:
(line 0015)         # Type-check this passed parameter or return value against this
(line 0016)         # PEP-compliant type hint.
(line 0017)         if not isinstance(__beartype_pith_0, str):
(line 0018)             raise __beartype_get_violation(
(line 0019)                 func=__beartype_func,
(line 0020)                 conf=__beartype_conf,
(line 0021)                 pith_name='return',
(line 0022)                 pith_value=__beartype_pith_0,
(line 0023)             )
(line 0024)
(line 0025)     return __beartype_pith_0

Hah! @beartype is doing something. You still don't believe me, but that debug output doesn't lie. ...or does it

Cancerous mandelbulb bear.

image

@beartype: Wait, What Violated a Type-check?

Have you ever read a riotously coloured @beartype exception message and thought to yourself: "Tell me what you know, @beartype! Which object is this that's violating my codebase? Which object, curse your clumsy paws!?"

Exception messages like this are less helpful than desired:

beartype.roar.BeartypeCallHintParamViolation: @beartyped my_module.MyClass.__init__() parameter my_array="array([[False, False],
       [False, False],
       [ True, False],
       [ True, False],
... violates type hint NArray[Shape['11 foo'], bool], as <protocol "numpy.ndarray"> "array([[False, False],
       [False, False],
       [ True, False],
       [ True, False],
... not instance of <class "nptyping.common.NArray[Shape['11 foo'], bool]">.

Right? Like, that's not getting anyone very far. @beartype, please do better.

Thankfully, beartype 0.12.0 heard your plaintive cries in the darkness. By the power of the new BeartypeCallHintViolation.culprits property defined by all type-checking violation exceptions, you too can dynamically inspect the exact object responsible for violating type-checking at runtime:

# Import the requisite machinery.
from beartype import beartype
from beartype.roar import BeartypeCallHintViolation

# Arbitrary user-defined classes.
class SpiritBearIGiveYouSalmonToGoAway(object): pass
class SpiritBearIGiftYouHoneyNotToStay(object): pass

# Arbitrary instance of one of these classes.
SPIRIT_BEAR_REFUSE_TO_GO_AWAY = SpiritBearIGiftYouHoneyNotToStay()

# Callable annotated to accept instances of the *OTHER* class.
@beartype
def when_spirit_bear_hibernates_in_your_bed(
    best_bear_den: SpiritBearIGiveYouSalmonToGoAway) -> None: pass

# Call this callable with this invalid instance.
try:
    when_spirit_bear_hibernates_in_your_bed(
        SPIRIT_BEAR_REFUSE_TO_GO_AWAY)
# *MAGIC HAPPENS HERE*. Catch violations and inspect their "culprits"!
except BeartypeCallHintViolation as violation:
    # Assert that one culprit was responsible for this violation.
    assert len(violation.culprits) == 1

    # The one culprit: don't think we don't see you hiding there!
    culprit = violation.culprits[0]

    # Assert that this culprit is the same instance passed above.
    assert culprit is SPIRIT_BEAR_REFUSE_TO_GO_AWAY

The perspicacious reader ...yes, we know that word, too will note that BeartypeCallHintViolation.culprits property is actually a tuple of one or more items. Why? Python containers (e.g., dict, list). If an item of a Python container is the responsible culprit, then @beartype doesn't just provide you that item; it provides you both that item and the container containing that item.

# Import the requisite machinery.
from beartype import beartype
from beartype.roar import BeartypeCallHintViolation
from beartype.typing import List

# Callable annotated to accept a standard container.
@beartype
def we_are_all_spirit_bear(
    best_bear_dens: List[List[str]]) -> None: pass

# Standard container deeply violating the above type hint.
SPIRIT_BEAR_DO_AS_HE_LIKE = [
    [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_LIKE)
# Shoddy magic happens here. Catch violations and try (but fail) to
# inspect the original culprits, because they were containers!
except BeartypeCallHintViolation as violation:
    # Assert that two culprits were responsible for this violation.
    assert len(violation.culprits) == 2

    # Root and leaf culprits. We just made these words up, people.
    root_culprit = violation.culprits[0]
    leaf_culprit = violation.culprits[1]

    # Assert that these culprits are, in fact, just repr() strings.
    assert root_culprit == repr(SPIRIT_BEAR_DO_AS_HE_LIKE)
    assert leaf_culprit == repr(SPIRIT_BEAR_DO_AS_HE_LIKE[0][0])

Don't ask why @beartype is only giving you repr() strings here. Just... don't. Debug like your life depends on it – because it very well might, someday. cue sweaty action scene

Cancerous mandelbulb bear.

image

typing.NamedTuple: Yeah, We Do That Too.

@beartype now fully supports named tuples. @langfield demanded it. @langfield gets it!

# Import the requisite machinery.
from typing import NamedTuple

# Decorate named tuples with @beartype.
@beartype
class LangfieldStyleTuple(NamedTuple):
    # Annotate fields with PEP-compliant type hints.
    tremble_and_weep: str | bytes

Cancerous mandelbulb bear. Just kidding! It's actually gorgeous stained glass of a loon on a lake.

image

wait, wut

Nuitka: Yeah, We Do That Too.

@beartype now fully supports Nuitka, the Python-to-C[++] transpiler we desperately hope ports itself to the Android toolchain. Make this happen, Nuitka! Ain't nobody got time for Cython in 2023. We know that @beartype now fully supports Nuitka, because integration tests in @beartype's test suite says we do. Would tests lie? 😮‍💨

Gorgeous stained glass of blue jays in a tree.

image

Lastly but not leastly...

we doin' this

...to financially feed @leycec and his friendly @beartype through our new GitHub Sponsors profile. Come for the candid insider photos of a sordid and disreputable life in the Canadian interior; stay for the GitHub badge and warm feelings of general goodwill.

Cue hypnagogic rave music that encourages fiscal irresponsibility.

Shoutouts to the Beasts in Back

Greets to the bear homies: @Jasha10, @foxx, @tlambert03, @rskol, @langfield, @posita, @kloczek, @wesselb, @yioda, @shenwpo, @ShaneHeldsinger, @murphyk, @ArneBachmannDLR, @jpetrucciani, and @pinkwah. 🏩

Cancerous mandelbulb bear.

image

@foxx
Copy link

foxx commented Jan 18, 2023

Damn that's a chonky feature list - thank you for all your efforts on this project!

Happy to say our company @zeroguard is now officially a beartype sponsor. And let's be real, It would be almost criminal for me not to sponsor, given that 98% of our Python file coverage on our 400-ish internal repos shows import beartype. Not to mention the many hours of development time saved thanks to having trustable runtime type enforcement.

@bitranox
Copy link

bitranox commented Jan 18, 2023 via email

@leycec
Copy link
Member Author

leycec commented Apr 8, 2023

...munch, munch, munch. That is the sound of beartype 0.13.0 noisily eating the nutritious bugs rummaging around in your world-shattering codebase. You didn't know they were there – but they were. Then @beartype ate them and belched contentedly. 😋

Let's start this ML party.

$ pip install --upgrade beartype

As you upgrade, an autonomic thrill of inexpressible joy jolts your limbic system. You rock back into the plush leather chair that is the literal seat of your digital empire with a contented sigh. Feels good to be good.

But this episode of "What won't @leycec say for emoji upvotes?" has only begun. Allow Alpha – the artificial general intelligence (AGI) from Hitoshi Ashinano's seminal slice-of-cli-fi-life manga Yokohama Kaidashi Kikou (ヨコハマ買い出し紀行) – to serenade you through the future of typing a modest grab-bag of typing goodies. The codebase you heroically save just might be your own.

it do be like that
Yokohama. Not quite what it used to be, but I'll take it.

New Challengers Join the Beartype Family

I'm so maternally proud. Introducing... the newest teenage bear cubs of the @beartype family. They're feisty, because teenage. They're unruly, because bears. They're listening to synthwave-cybermetal mashups that reverberate with pulsating bass lines through your mechanical keyboard keys, because @beartype. They are:

  • @posita's luminous numery. Type-check numeric scalars stupidly fast. Integers! Floats! Oh, my. You thought they were easy to type-check – and then you remembered NumPy, SymPy, and everybody else. Built on the back of @beartype but hopefully less smelly and itchy:
# ML-friendly numeric scalers made simple. That's how we roll.
from beartype import beartype
from numerary import IntegralLike, RealLike

# "int" and "float"? So passé. It's only the best for my Jupyter cell.
@beartype
def deeper_thot(arg: RealLike) -> IntegralLike:
    assert arg != 0 and arg ** 0 == 1
    return arg // arg + 42
  • @wesselb's epochal Plum. It's bursting with Pythonic multiple dispatch for the masses. It's @typing.overload, except it actually does something rather than absolutely nothing. JuliaLang: eat your fetid heart out:
# Plum + numerary: when their powers combine, even /r/Python trembles.
from plum import dispatch
from numerary import RealLike

# Which overload of this function will you call? Why not all three!?!?
@dispatch
def muh_overloaded_func(x: str) -> str:
    return "Pretty sure that a string is a string."

@dispatch
def muh_overloaded_func(x: int) -> str:
    return "Not sure if positive integer or just happy to see me."

@dispatch
def muh_overloaded_func(x: RealLike) -> str:
    return "This is a floating-point... thing. Plum and numerary says so."

you too
If I had to live in a shanty lean-to down by the river, let this be the one.

pandera: type-check pandas data frames like it's 2027

@beartype now transparently supports all pandera type hints. Deeply type-check the contents of almost all pandas objects (including any data frame, index, or series) with type hints published by the third-party pandera package – the leading industry-standard for blah, blah, blah... hey, wait. What is this HR speak in @beartype's automated news dispenser!? Yes. It's true. We are shilling.

Seeing is believing. Soon, you too will believe that ML pipelines can be domesticated. Arise, huge example! Stun the disbelievers throwing peanuts at our issue tracker.

# Import important machinery. It's important.
import pandas as pd
import pandera as pa
from beartype import beartype
from pandera.dtypes import Int64, String, Timestamp
from pandera.typing import Series

# Arbitrary pandas data frame. If pandas, then data science.
muh_dataframe = pd.DataFrame({
    'Hexspeak': (
        0xCAFED00D,
        0xCAFEBABE,
        0x1337BABE,
    ),
    'OdeToTheWestWind': (
        'Angels of rain and lightning: there are spread',
        'On the blue surface of thine aery surge,',
        'Like the bright hair uplifted from the head',
    ),
    'PercyByssheShelley': pd.to_datetime((
        '1792-08-04',
        '1822-07-08',
        '1851-02-01',
    )),
})

# Pandera dataclass validating the data frame above. As above, so below.
class MuhDataFrameModel(pa.DataFrameModel):
    Hexspeak: Series[Int64]
    OdeToTheWestWind: Series[String]
    PercyByssheShelley: Series[Timestamp]

# Custom callable you define. Here, we type-check the passed data frame, the
# passed non-pandas object, and the returned column of this data frame.
@beartype
@pa.check_types
def convert_dataframe_column_to_series(
    # Annotate pandas data frames with pandera type hints.
    dataframe: pa.typing.DataFrame[MuhDataFrameModel],
    # Annotate everything else with standard PEP-compliant type hints. \o/
    column_name_or_index: str | int,
# Annotate pandas series with pandera type hints, too.
) -> Series[Int64 | String | Timestamp]:
    '''
    Convert the column of the passed pandas data frame (identified by the
    passed column name or index) into a pandas series.
    '''

    # This is guaranteed to be safe. Since type-checks passed, this does too.
    return (
        dataframe.loc[:,column_name_or_index]
        if isinstance(column_name_or_index, str) else
        dataframe.iloc[:,column_name_or_index]
    )

# Prints joyful success as a single tear falls down your beard stubble:
#     [Series from data frame column by *NUMBER*]
#     0    3405697037
#     1    3405691582
#     2     322419390
#     Name: Hexspeak, dtype: int64
#
#     [Series from data frame column by *NAME*]
#     0    Angels of rain and lightning: there are spread
#     1          On the blue surface of thine aery surge,
#     2       Like the bright hair uplifted from the head
#     Name: OdeToTheWestWind, dtype: object
print('[Series from data frame column by *NUMBER*]')
print(convert_dataframe_column_to_series(
    dataframe=muh_dataframe, column_name_or_index=0))
print()
print('[Series from data frame column by *NAME*]')
print(convert_dataframe_column_to_series(
    dataframe=muh_dataframe, column_name_or_index='OdeToTheWestWind'))

# All of the following raise type-checking violations. Feels bad, man.
convert_dataframe_column_to_series(
    dataframe=muh_dataframe, column_name_or_index=['y u done me dirty']))
convert_dataframe_column_to_series(
    dataframe=DataFrame(), column_name_or_index=0))

See our extensive FAQ entry on the topic for further discussion. There be dragons belching flames over the hapless village. You want to know about those dragons. The hapless village is actually your codebase in this awkward metaphor.

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—
transmission terminated

yeah
I would listen to this song until I dreamily pass out onto a sunken electrical line.

PEP 591: typing.Final[...]. Final-ly.

@beartype now shallowly type-checks dataclass attributes annotated with PEP 591-compliant typing.Final[...] type hints. @beartype reduces all type hints of the form Final[{hint}] to merely {hint} (e.g., Final[int] to int). In other words, @beartype no longer raises exceptions when confronted with final type hints and instead at least tries to do the right thing:

# Import important machinery. It's still important.
from beartype import beartype
from beartype.typing import Final
from dataclasses import dataclass

@beartype
@dataclass
class MuhDataclass(object):
    # @beartype now accepts this by just reducing this to:
    #     readonly_attr: str = 'Destroyer and preserver; hear, oh hear!'
    readonly_attr: Final[str] = 'Destroyer and preserver; hear, oh hear!'

# @beartype now rejects attempts to initialize final fields to
# invalid initial values by raising a type violation. (Good.)
good_data_is_good = MuhDataclass(readonly_attr='Make it so, ensign.'))

# Sadly, @beartype also accepts attempts to redefine final fields to
# valid new values. (Bad! Y u so bad, @beartype?)
good_data_is_good.readonly_attr = 'Damn you, @beartype! Damn you to heck.'

This still isn't quite what everyone wants @beartype to do here; ideally, @beartype should also raise exceptions on detecting attempts to redefine instance and class variables annotated as Final[...]. Doing so is definitely feasible and exactly what @beartype should eventually do – but also non-trivial, because whatever @beartype eventually does needs to preserve compatibility with each implementation of @dataclass across each version of Python now and forever.

Amen, @beartype. Preach it.

still young, still easy
Oh, as I was young and easy in the mercy of his means, time held me green and dying. — some dude from Wales

PEP 647: typing.TypeGuard. En-garde, bad types!

@beartype now shallowly type-checks return values annotated by PEP 647-compliant typing.TypeGuard[...] type hints. @beartype unconditionally reduces all such hints to the builtin bool type (e.g., TypeGuard[list[str]] to bool). In other words, @beartype no longer raises exceptions when confronted with type guards and instead at least tries to do the right thing:

# Import important machinery. Beautiful androids playing mandolins ask this of you.
from beartype import beartype
from beartype.typing import TypeGuard

# @beartype now accepts this by just reducing this to:
#     def is_typeguard_slow(lst: List[object]) -> bool:
@beartype
def is_typeguard_slow(lst: list[object]) -> TypeGuard[list[str]]:
    '''
    :pep:`647`-compliant type guard returning :data:`True` only if the
    passed list contains only string items via the standard linear runtime
    type-check implemented manually.
    '''

    # *leisurely barfs into adjacent bucket*
    return all(isinstance(item, str) for item in lst)

Be still, my beating heart and arthritic hands.

look mo, no safety rails
We all once had days like this. Thankfully, we use @beartype instead now.

Documentation: I can't believe it's not README.rst, either.

@beartype productions is proud to exhaustedly present... our Read the Docs (RtD)-hosted website providing quasi-readable documentation:

Mash that link for a chance to learn how to actually use @beartype. The chance is still low, but probably non-zero. You turn your back on your codebase for just 567 git commits – and suddenly your formerly self-documenting API that once "just worked" now languidly sprawls across fifteen public subpackages like an uninvited wild ferret on well-raked shag carpeting.

Docos. They now exist. You read it here, which means it must be true. We should have done this eight years ago. Instead, we played video games. This must be what it feels like when git repositories cry.

things that make you go, "hmm"
That's either a flying lemon slice, alien mothership from Trappist-1, or artistic license. You decide.

Gentoo and Arch Linux: @beartype is there, too.

@beartype is now officially packaged by increasingly many Linux distributions nobody has ever heard of or should use, including:

  • Gentoo Linux, the distro that might just break your CPU over its knee.
  • Arch Linux, the distro that thought it would be a great idea to name its package manager after a yellow blob that eats consciousness-altering "food pellets" that is itself eaten by ghosts named Inky, Blinky, Pinky, and... Clyde. One of these names is not like the others. True story: Clyde's official name in Japan is Stupid. 😮‍💨

the end
JAX coffee. Need more JAX? Just drink it, bro.

Lastly but not leastly...

we doin' this

...to financially feed @leycec and his friendly @beartype through our new GitHub Sponsors profile. Come for the candid insider photos of a sordid and disreputable life in the Canadian interior; stay for the GitHub badge and warm feelings of general goodwill.

Cue hypnagogic rave music that encourages fiscal irresponsibility. 🎵 🎹 🎶

Shoutouts to the Beasts in Back

Greets to the bear bromancers: @ulfaslakprecis, @NLPShenanigans, @justinchuby, @patrick-kidger, @kloczek, @danigm, @hhoeflin, @yurivict, @LittleBigGene, @faangbait, and @JWCS.

salud
For those who are about to code, @beartype and Alpha salute you.

@leycec
Copy link
Member Author

leycec commented May 2, 2023

*Glorp*, *glorp*, *glorp*. This is the sound that disgusting alien pupae in the 80's scifi horror film you just made your wife watch makes when it's giving birth to... beartype 0.14.0! *glorp*

python3 -m pip install --upgrade beartype

We gestated this rapid release under our new "Fast is better than slow" motto. Will @beartype be able to keep up the frantic pace? Does @leycec need sleep? Is this a healthy lifestyle for anyone? The answer to these questions and more is: "Almost certainly not." We're gonna have to read Japanese light novels for the next five months to recover. Thankfully, we did something first.

Get your PEP 673 (i.e., typing.Self) and 675 (i.e., typing.LiteralString) support while it's red hot, folks. This deal only lasts for a lifetime.

beartype 0.14.0 is brought to you by the James Webb Space Telescope. So cool! It's space.

JWST God-Hand

The Pillars of Creation? Or Creepy Cosmic God-Hand of Retribution? You decide.

PEP 673: Taking QA Selfies with typing.Self

So. You come to @beartype for a favour. You ask us to type-check that a method accepts and returns an instance of the same class. But... how? How, @beartype?! Curse your shoddy documentation!

This is the age-old typing question. It's a story as familiar as the Mario brothers on yet another mushroom power-up bender. Thanks to fungi, Mario saves the princess toadstool. Likewise, thanks to typing.Self, @beartype saves your rickety codebase factory functions:

# Import important stuff. It's tough.
from typing import Self  # <---------------- if Python ≥ 3.11.0
# from typing_extensions import Self   # <-- if Python < 3.11.0

# Decorate classes – not methods. It's rough.
@beartype  # <-- this is the way.
class SpaceIsThePlace(object):
   def __init__(self, *args: str) -> None:
       '''
       This makes sense because the docstring says this makes sense.
       '''
       self._dark_side_of_the_bear = args

   def make_space_even_bigger(self, other: Self) -> Self:
       '''
       Factory function returning a new instance of this class,
       concatenating this and the passed instance.
       '''
       return SpaceIsThePlace(
           self._dark_side_of_the_bear + other._dark_side_of_the_bear)

Note: judicious use of Self everywhere. Technically, this requires Python 3.11. Pragmatically, typing_extensions means that you can bring Python 3.11 with you back into the past – where code was simpler, Python was even slower, and nothing really worked as intended.

Before Python 3.11, the only way to type-check that a method accepts or returns an instance of the same class was to (ab)use either forward references (e.g., "SpaceIsThePlace") or PEP 563 (e.g., from __future__ import annotations). That is what everyone should no longer do. We refer to this as...

# The bad ole days when @beartype had to bathe in the gutter.
# *PLEASE DON'T DO THIS ANYMORE.* Do you want @beartype to cry?
@beartype
class SpaceIsARottenPlace(object):
   def __init__(self, *args: str) -> None:
       self._dark_side_of_the_bear = args

   def make_space_go_boom(
       self, other: 'SpaceIsARottenPlace') -> (  # <-- no, no, Gods, no
       'SpaceIsARottenPlace'):  # <------------------- please, Gods, no
       return SpaceIsThePlace(
           self._dark_side_of_the_bear + other._dark_side_of_the_bear)

Or even...

# Breaking the Python interpreter: feels bad, man.
# *PLEASE NEVER DO THIS ANYWHERE.* @beartype is a shambling wreck thanks
# to code like this.
from __future__ import annotations

@beartype
class SpaceIsAnEvenMoreRottenPlaceThanICouldHaveImagined(object):
   def __init__(self, *args: str) -> None:
       self._dark_side_of_the_bear = args

   def make_space_go_boom(
       self, other: SpaceIsARottenPlace) -> (  # <-- NO, NO, GODS, NO
       SpaceIsARottenPlace):  # <------------------- PLEASE, GODS, NO
       return SpaceIsThePlace(
           self._dark_side_of_the_bear + other._dark_side_of_the_bear)

Technically, @beartype does support both of those approaches – nominally, anyway. Pragmatically, we're still grappling with unresolved slippery edge cases that are guaranteed to blow up your test suite in your next changeset that you are about to commit. Even when @beartype does perfectly support forward references and PEP 563 in a future release, you should still strongly prefer typing.Self. Why?

Speed. It's why we're here. We can admit this to ourselves. If @beartype were slow, nobody would be reading this. We'd be too busy fighting with mypy and pyright. Thankfully, we're here.

@beartype generates optimally efficient type-checking code for typing.Self. It's literally just a trivial call to the isinstance() builtin. Sadly, the same cannot be said for forward references or PEP 563. @beartype generates suboptimal type-checking code for both, because @beartype defers the lookup of the class referred to by each forward reference to call time; although @beartype caches each class after doing so for efficiency, all of this incurs space and time costs you'd rather not pay at runtime.

typing.Self: it saved our issue tracker from certain doom. Now, it will save your codebase from our issues.

gravitation

Gravitational lensing? Or the inside of our sink after a rough winter? Hope for the best.

PEP 675: typing.LiteralString = You Now Laugh at SQL Injection Attacks

So. I meant to talk a great deal about PEP 675 and typing.LiteralString. But I'm tired and hungry and almost all out of James Webb Space Telescope pics. For those who SQL, I can say that I strongly advise use of typing.LiteralString to avoid SQL injection attacks.

Seriously. Unfortunately, @beartype can only help you so much here; @beartype currently just equates typing.LiteralString to the builtin str type. That's better than nothing, but sorta defeats the whole purpose really.

For full-blown deep type-checking of literal strings, you'll reluctantly need to pair @beartype with a static type-checker like mypy or pyright. On the one hand, this means fighting false positives, which is about as much fun as reading what you are currently reading. On the other hand, this means avoiding SQL injection attacks.

That's not really a tradeoff, right? One thing makes you annoyed; the other thing destroys your business model, your reputation on Twitter, and a lifetime's worth of carefully curated data. You know what to do here. Do what @beartype cannot do... yet. Do @beartype + static analysis. Take one for the dev team.

literally everything

Deep field. Big brains on Reddit say these are the earliest galaxies to have ever been observed – "just" 400 million years after the Big Bang. We're only 2% away from the Big Bang itself at this point. Go deeper, James Webb. Uncover the truth of the deep field. But can we even handle the truth at this point!?

Lastly but not Leastly...

we doin' this

...to financially feed @leycec and his friendly @beartype through our new GitHub Sponsors profile. Come for the candid insider photos of a sordid and disreputable life in the Canadian interior; stay for the GitHub badge and warm feelings of general goodwill.

Cue hypnagogic rave music that encourages fiscal irresponsibility. 🎵 🎹 🎶

Shoutouts to the Beasts in Back

Greets to the Bear Bromancer @jvesely (Jan Vesely), whose timely and increasingly desperate pleadings with me to just release beartype 0.14.0 already have finally born fruit. This James Webb Space Telescope pic of Uranus is for you.

uranus
😮

@leycec
Copy link
Member Author

leycec commented Jul 22, 2023

@beartype 0.15.0 arises from the ashes of our issue tracker. Now, so too will your codebase.

python3 -m pip install --upgrade beartype

Like a cyberpunk phoenix whose intertube veins are made of pure honey and blueberry juice, @beartype 0.15.0 introduces the new beartype.claw API. Automate the @beartype decorator away with magical import hooks from beartype.claw. Do it for the new guy that's sobbing quietly in his cubicle.

When you call import hooks published by the beartype.claw API, you enable hybrid runtime-static type-checking. By "hybrid runtime-static," we mean that beartype.claw performs both standard runtime type-checking (ala the @beartype decorator) and standard static type-checking (ala mypy and pyright) but at runtime – and that ain't standard.

That's right. @beartype is now a tentacular cyberpunk horror like that mutant brain baby from Katsuhiro Otomo's dystopian 80's masterpiece "Akira." You can't look away!

brain baby, ya
May Neo-Tokyo have mercy on @beartype's soul.

Does this mean that you can now safely discard mypy, pyright, and every other pure-static type-checker that your enlightened IDE has unjustly subjected you to over the past several years? In general:

  • For many of you: "Yes. That is what this means." Pure-static type-checkers lie to you about everything, require maintaining fragile and unreadable type: ignore[...] and pyright: ignore[...] comment chatter throughout your once-pristine codebase, and fail to enforce anything at test- or runtime. In other words, they (mostly) suck; we should all stop using them, because they (mostly) fail at their core mandate.
  • For some of you: "No. Please do still use your pure-static type-checkers." They still excel at use cases that @beartype cannot possibly hope to cover in @leycec's twenty-three next lifetimes as a serial open-source volunteer – including security-oriented use cases like the PEP 675-compliant typing.LiteralString type hint. That's critical.

In either case, beartype.claw lies less,...in most cases, much much less requires no comment chatter, and enforces everything at test- and runtime. You do still want a real-time IDE linter to catch mundane mistakes like trivial syntactic errors and semantics concerns like obviously undeclared attributes, of course; allow me to now shill for @astral-sh's magnum opus ruff here. If it barks, it's either ruff or that neighbourhood mongrel harassing our Maine Coons again. Those are domesticated cats, fat doggo – not raccoons. Why won't you listen to a human's plea in the night? 😮‍💨

tl;dr: Just Tell My Coworker What to Do, Daddy @leycec

For those who are about to code, we salute you with all you need to know:

# In your "{your_package}.__init__" submodule:
from beartype.claw import beartype_this_package  # <-- boilerplate for victory
beartype_this_package()  # <-- congrats. your team just won.

That's it. That's beartype.claw in ten seconds (dyslexia notwithstanding). As the simplest of several import hooks published by the beartype.claw API, the beartype_this_package() function:

  • Implicitly decorates all callables and classes in {your_package} by the @beartype decorator. Rejoice, fellow mammals! You no longer need to explicitly decorate anything by @beartype ever again. Of course, you still 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 subjects all PEP 526-compliant annotated variable assignments (e.g., muh_int: int = 'Pretty sure this isn't an integer.') to runtime type-checking via the beartype.door.die_if_unbearable() function. More below.

Okay, that's not it. The beartype.claw rabbit hole goes so deep that we couldn't even document anything for this release. Because exhaustion defeated common sense, these release notes are all the documentation.

doesn't look good
This is what happens when we don't beartype_this_package().

Will The Real Import Hooks Stand Up?

beartype.claw actually provides five vaguely related import hooks. In ascending order of scope, these are...

beartyping: The Context Manager That Does Stuff

The beartype.claw.beartyping context manager is our most self-limiting import hook. When you want to temporarily type-check only a few packages and modules isolated to a single block of code, put that beartyping on speed dial:

# Anywhere you like, but typically in your "{your_package}.__init__" submodule:
from beartype.claw import beartyping  # <-- boilerplate intensifies

with beartyping():  # <-- context managers: they manage context
    import {your_package}.{your_module}    # <-- @beartype your own stuff with pride
    import {their_package}.{their_module}  # <-- @beartype somebody else's stuff with or without pride

As the above example suggests, all beartype.claw import hooks apply equally well to code you and your fearless team have authored, other people's third-party code, and Python's standard library. Whether they intended you to @beartype their stuff or not, do it anyway. Shove @beartype's snuffling snout into every hidden nook and cranny of the Python ecosystem. If it feels good, improves quality assurance, and impresses that weird management guy, could it be wrong?

it do be like that
The journey of a thousand bugs begins with a single telekinetic leap. beartype.claw.beartyping: this is that leap.

beartype_this_package: It Does What It Says, Unlike @leycec

The beartype.claw.beartype_this_package() import hook isolates its bug-hunting action to the current package. This is what everyone wants to try first. If beartype_this_package() fails, there is little hope for your package. Even though it's probably @beartype's fault, @beartype will still blame you for its mistakes.

Typically called as the first statement in your top-level {your_package}.__init__ submodule, beartype_this_package() extends the surprisingly sharp claws of @beartype to all callables, classes, and PEP 526-compliant annotated variable assignments defined across all submodules and subpackages of the current package – regardless of lexical or filesystem nesting depth. As the term "import hook" implies, beartype_this_package() only applies to subsequent imports performed after that function is called; previously imported submodules and subpackages remain unaffected.

it do be like that
beartype_this_package(): it do be like that.

beartype_package: No One Expects GitHub Bear

The beartype.claw.beartype_package() import hook isolates its bug-hunting action to the single package or module with the passed absolute name. Examples or it didn't happen:

# In your "{your_package}.__init__" submodule, this logic is exactly equivalent
# to the beartype_this_package() example above:
from beartype.claw import beartype_package  # <-- boilerplate continues boilerplating
beartype_package('your_package')  # <-- 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 wanted to confine beartype.claw to a single subpackage or submodule of your package (rather than your entire package)? In that case, beartype_this_package() is over-bearing.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('your_package.your_subpackage.your_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:

# Anywhere you like. Whenever you want to break something over your knee,
# never leave Vim without beartype_package():
beartype_package('somebody_elses_package')  # <-- blow it up like you just don't care

truer words
Truer words were never spoken, wizened psychic baby lady.

beartype_packages: When A Single Bear Just Won't Do

The beartype.claw.beartype_packages() import hook isolates its bug-hunting action to the one or more packages or modules with the passed absolute names. Whereas beartype_package() accepts only a single string, beartype_packages() accepts an iterable of zero or more strings. One function to QA them all, and in the darkness of our implementation bind them:

# In your "{your_package}.__init__" submodule, @beartype multiple packages
# including your own package. You live life unencumbered by rules, because you're
# making this up as you go along. Freedom means breaking things with your elbows.
from beartype.claw import beartype_packages  # <-- boilerplate comes to a boil
beartype_packages((
    'your_package',
    'some_package.published_by.the_rogue_ai.Johnny_Twobits',  # <-- seems trustworthy
    'numpy',  # <-- ...heh. no one knows what will happen here!
    'scipy',  # <-- ...but we can guess, can't we? *sigh*
))

this is the end
The end of the road is where beartype_packages() is just getting started.

beartype_all: You Will Be Assimilate

The beartype.claw.beartype_all() import hook doesn't isolate anything! It's the ultimate extroverted import hook, spasmodically unleashing a wave of bug-hunting action across the entire Python ecosystem. After calling beartype_all(), any package or module authored by anybody (including standard packages and modules in Python's standard library) will be subject to @beartype.

This is the extreme QA nuclear option. Because this is the extreme QA nuclear option, most packages should not do this. beartype_all() forces possibly unwanted @beartype-ing on all downstream consumers importing your package. The only packages that should do this are high-level applications run as hegemonic executables (rather than imported by other higher-level applications and packages).

@beartype cannot be held responsible for the sudden rupture of normalcy, the space-time continuum, or your previously stable job. For those who are about to explode their codebases, we duck inside a bomb shelter:

# In your "{your_package}.__init__" submodule, this logic nukes Python from orbit:
from beartype.claw import beartype_all  # <-- @beartype seemed so innocent, once
beartype_all()  # <-- where did it all go wrong?

let's go
The beartype_all() lifestyle is short but sweet, just like @leycec.

PEP 526: What Do These Ineffable Numbers Even Mean?

This is PEP 526:

happy_fun_times: str = 0xDEADDEAD  # <-- not so happy-fun after all, is it

Previously, PEP 526-compliant annotated variable assignments were beyond the feeble reach of the Bear. Now, the Bear lovingly fondles those assignments with runtime type-checking fuelled by our beartype.door.die_if_unbearable() function. Specifically, for each assignment of the form var_name: type_hint = new_value at any lexical scope of any module imported under an import hook described above, beartype.claw appends that assignment by a statement of the form die_if_unbearable(var_name, type_hint) type-checking that assignment against that type hint at runtime.

beartype.claw thus silently expands the above single-line assignment to: e.g.,

from beartype.door import die_if_unbearable  # <-- boilerplate still boilerplating

happy_fun_times: str = 0xDEADDEAD  # <-- you thought you could hide, bug. but you were wrong
die_if_unbearable(happy_fun_times, str)  # <-- raise an exception like a dancing necromancer

Since the above assignment violates that type hint, beartype.claw will now raise a beartype.roar.BeartypeDoorHintViolation exception at the point of that assignment. beartype.claw ignores all variable assignments that are not annotated by type hints: e.g.,

happy_fun_times = 0xBEEFBEEF  # <-- probably not happy-fun, but beartype no longer cares

If you hate this, you'll just love our new BeartypeConf.claw_is_pep526 configuration option. Which leads us directly to...

let's go
Unhappy people forgot to annotate their variable assignments, I see.

BeartypeConf: Now With More Umph in its Conf

We didn't tell you this, because we save the best for last. But all of the import hooks described above accept an optional keyword-only conf: BeartypeConf = BeartypeConf() parameter (i.e., user-defined beartype configuration, defaulting to the default beartype configuration). Unsurprisingly, that configuration configures all actions performed by the beartype.claw API under that import hook:

# In your "{your_package}.__init__" submodule, enable @beartype's support for the
# PEP 484-compliant implicit numeric tower (i.e., expand "int" to "int | float" and
# "complex" to "int | float | complex"):
from beartype import BeartypeConf           # <-- it all seems so familiar
from beartype.claw import beartype_package  # <-- boil it up, boilerplate
beartype_package('your_package', conf=BeartypeConf(is_pep484_tower=True))  # <-- *UGH.*

Equally unsurprisingly, our existing beartype.BeartypeConf dataclass has been augmented with new beartype.claw-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 PEP 526-compliant annotated variable assignments like muh_int: int = 'Pretty sure this isn't an integer.'. Although this is usually what everyone wants, this may not be what someone suspicious dressed in black leather, a red "velvet" cape, and aviator goggles wants in an edge case too unfathomable to even contemplate. If you are such a person, consider disabling this option to reduce runtime safety and destroy your code like Neo-Tokyo vs. Mecha-Baby-Godzilla: ...who will win!?!?
# In your "{your_package}.__init__" submodule, disable PEP 526 support out of spite:
from beartype import BeartypeConf            # <-- boiling boilerplate...
from beartype.claw import beartype_packages  # <-- ...boils plates, what?

beartype_packages(
    ('your.subpackage', 'your.submodule'),   # <-- pretend this makes sense
    conf=BeartypeConf(claw_is_pep526=False)  # <-- *GAH!*
)
  • 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 decorator at decoration time. This is usually what everyone wants, because the @beartype decorator 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 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 either want to see the full exception traceback or to 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 with maximal roaring and blood-flecked claws:
# In your "{your_package}.__init__" submodule, raise exceptions because you hate worky:
from beartype import BeartypeConf                # <-- boiling boilerplate...
from beartype.claw import beartype_this_package  # <-- ...ain't even lukewarm
beartype_this_package(conf=BeartypeConf(warning_cls_on_decorator_exception=None))  # <-- *ohboy*

doesn't look so good
Crack commando Bear-Team: Assemble!

Lastly but not Leastly...

we doin' this

...to financially feed @leycec and his friendly @beartype through our new GitHub Sponsors profile. Come for the candid insider photos of a sordid and disreputable life in the Canadian interior; stay for the GitHub badge and warm feelings of general goodwill.

Cue hypnagogic rave music that encourages fiscal irresponsibility. 🎵 🎹 🎶

Shoutouts to the Beasts in Back

Greets to:

And... I'm spent. Clearly, this mega-issue is also spent. Fifteen million meme images and dissertation-length monologuing have clogged the Intertubes beyond repair. With an appreciable sigh of relief as we move into the new age of beartype.claw, let's close this venerable thread. All newer release announcements will be posted to our peanut gallery. In fact, it already has. This must be what the future feels like.

Goodbye, Future Sound of Beartype. Thanks for all the salmon.

high-fives, yo
beartype.claw rises with the paw of quality. Will you high-five that paw?

@leycec leycec closed this as completed Jul 22, 2023
@skeggse
Copy link

skeggse commented Jul 22, 2023

Enter beartype_package(), the outer limits of QA where you control the horizontal and the vertical:

image

👏👏👏👏👏👏 well met, @leycec! I'm excited to give this a try soon!

@patrick-kidger
Copy link
Contributor

This is awesome! Marvelous work @leycec.

And because I love bringing you anguish ensuring that beartype is robust, one question: does this interact safely with import hooks from other packages?

@wesselb
Copy link
Member

wesselb commented Jul 22, 2023

Whoa. Absolutely fantastic work, @leycec! This is insanely cool.

@JWCS
Copy link

JWCS commented Jul 23, 2023

By the way, this issue is still pinned; idk what you're going to do next, but it might be time to just let this 1GB webpage sleep, and pin the blame on something else.

@leycec
Copy link
Member Author

leycec commented Sep 6, 2023

Wahoo! Thanks so much for the phenomenal outpouring of support. Because I am crippled with social stupidity have Asperger's, I just realized five minutes ago that I kinda failed to double-check whether anyone ever commented on the 0.15.0 release. And... uh. You did. Three weeks ago.

awkward

Let us abruptly change the subject.

...does this interact safely with import hooks from other packages?

@patrick-kidger: Yes! So much yes! Miraculously, beartype.claw conflicts with no existing import hooks anywhere in the Python ecosystem. Allow me to now pontificate tiresomely.

Before implementing beartype.claw, I clawed through everyone else's issue trackers to discover any pending issues with the standard approach of implementing import hooks. Exactly as you suggest, the standard approach invites integration woes. If two third-party packages both attempt to install import hooks with that approach, then the hooks installed by the package that installed last silently overwrite the hooks installed by the package that installed first. 😬

Is this a real-world problem? This is a real-world problem. pytest, the 500lb QA gorilla in the room, silently installs import hooks to implicitly rewrite all assert statements across your entire test suite into something that actually prints useful output on assertion failure. This is well-known. What is less well-known is what this means for everybody else. Import hooks implemented with the standard approach are guaranteed to conflict with pytest. Since pytest justifiably assumes precedence over everybody else (including @beartype), this basically means that nobody (except pytest) can implement import hooks with the standard approach. pytest got there first and already colonized that design space.

I'm shocked ...simply shocked, I say that Python never standardized an import hook architecture. It didn't. The result is chaos and anarchy on the mean streets of PyPI.

I quietly abandoned the standard approach. Instead, I reverse-engineered Python's importlib machinery. What I saw both frightened and disquieted me. Nothing shakes the soul like the standard importlib package. But... it worked! I realized in a fit of late-night euphoria one dismal Winter's day that there is a completely different way of implementing import hooks. Nobody knows about it. I've never seen it done anywhere before or since. I'm confident I invented something awful there. It's the main reason it took me two years to do this.

This means there are guaranteed to be no conflicts between beartype.claw and competing import hooks; beartype.claw is implemented in such a profoundly alien way that it's not even an "import hook" in the standard understanding of that term. I can't even talk about it, because then everyone else will want to do it too to be mutually compatible with competing import hooks and especially pytest – which beartype.claw would then immediately begin conflicting with, defeating the whole point of my doing it in the first place.

And... maybe I should have redacted the last two self-congratulatory paragraphs.

@patrick-kidger
Copy link
Contributor

Haha, no worries!

So, jaxtyping and pytest's import hooks do both work together. That's something that "just worked" without intervention from me. I don't think there's an fundamental incompatibility there.

I've not tested but I assume they're both just rewriting the AST one after the other, e.g. much like a @macro1 @macro2 now_some_code(foo) in Julia, if that's a familiar point of reference. The results can sometimes be a little surprising if the inner macro does something wicked and rewrites the code into something unrecognisable to the outer macro, but is mostly fine.

So I'm curious what went wrong when you tried using the standard approach with beartype?

(Side note, I'm curious if you have a link to the new version you've cooked up?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests