github beartype/beartype v0.18.3
Beartype 0.18.3: For Justice, For Victory, For QA

latest releases: v0.18.5, v0.18.4
one month ago

Beartype 0.18.3 is the minor patch release that your careening codebase can no longer live without:

pip install --upgrade beartype

Actually... I lied. I know! I gotta stop doing that. But the sordid truth is that beartype 0.18.3 is mostly just for @iamrecursion and @sylvorg, who single-handedly reported more issues in a single week than the exploding size of @leycec's JRPG backlog. And we know how big that is, don't we? It's big. It's so big it wraps around like a self-sustaining Niven ringworld habitat at Lagrange point L1. Big-big.

Beartype 0.18.3 is for @iamrecursion and @sylvorg. May their usernames live forever in git log infamy. In this release, a few more bugs die.

But first...

GitHub Sponsors: They Scratch the Bear's Back. Now, The Bear Scratches Back.

This release comes courtesy these proud GitHub Sponsors, without whom @leycec's cats would currently be eating grasshoppers:

Thanks so much, masters of fintech and metrology.


The Masters of Fintech and Metrology. That's who.

Type Variables Bound by Forward References: So This Is a Thing, Huh?

So. Funny story. Turns out you can bound PEP 484-compliant type variables (i.e., typing.TypeVar(...) objects) with forward references specified as strings. Who knew? Everybody except @leycec. Nobody tells that guy nuthin'.

Beartype 0.18.3 now explicitly supports type variables bound by forward references. These are type hints of the form:

TypeVar('{TypeVarName}', bounds='{UndefinedType}')

Previously, @beartype only partially supported such variables due to @leycec failing to realize that such variables even existed and constituted a valid use case. This is why your codebase can't have good things. Now, @beartype fully supports heinous abominations valid use cases like:

from beartype import beartype
from typing import TypeVar

# Type variable bound by a forward reference. @beartype supports your
# weird stuff, because normalcy is just a null pointer to garbage.
Fuggit = TypeVar('Fuggit', bound='EnduringMisery')

@beartype
class EnduringMisery(object):
    def searing_pain(self) -> tuple[Fuggit, Fuggit]:
        return ('Fuggit...', '...up!')

# Can anyone guess what this does? That's right. It blows up. Fuggit!
blinding_agony = EnduringMisery()
blinding_agony.searing_pain()


hangin' with the bear homies in apocalyptic wasteland ain't no thang

beartype.vale.Is[...]: Now It Supports Crazy Stuff

The functional beartype validator factory beartype.vale.Is[...] is now subscriptable (indexable) by all manner of shambolic nightmares. Previously, you had to subscript Is[...] with low-level functions and methods. Now, you can subscript Is[...] with high-level callable objects like:

  • Class-based callables (i.e., objects whose classes define the __call__() dunder method, rendering otherwise uncallable objects callable): e.g.,

    from beartype.door import is_bearable
    from beartype.typing import Annotated
    from beartype.vale import Is
    from functools import partial
    
    class TruthSeeker(object):
        def __call__(self, obj: object) -> bool:
            '''
            Tester method returning :data:`True` only if the passed object
            evaluates to :data:`True` when coerced into a boolean and whose
            first parameter is ignorable.
            '''
    
            return bool(obj)
    
    # Beartype validator matching only objects that evaluate to "True".
    Truthy = Annotated[object, Is[TruthSeeker()]]
    
    assert is_bearable('', Truthy) is False
    assert is_bearable('Even lies are true now, huh?', Truthy) is True
  • Partials (i.e., high-level functools.partial(...) callable objects wrapping low-level functions and methods): e.g.,

    from beartype.door import is_bearable
    from beartype.typing import Annotated
    from beartype.vale import Is
    from functools import partial
    
    def is_true(ignorable_arg, obj):
        '''
        Tester function returning :data:`True` only if the passed object
        evaluates to :data:`True` when coerced into a boolean and whose first
        parameter is ignorable.
        '''
    
        return bool(obj)
    
    # Partial of the is_true() tester defined above, effectively ignoring the
    # "ignorable_arg" parameter accepted by that tester.
    is_true_partial = partial(is_true, 'Gods. This code is literally unreadable.')
    
    # Beartype validator matching only objects that evaluate to "True".
    Truthy = Annotated[object, Is[is_true_partial]]
    
    assert is_bearable('', Truthy) is False
    assert is_bearable('Even lies are true now, huh?', Truthy) is True

Is this valuable? No idea. Let's pretend I did something useful tonight so I can sleep without self-recrimination.


...heh. your eyes are now bleeding

Triply-Redeclared Types: Just don't ask.

Beartype 0.18.3 now sports improved support we rhymin' like it's 2099 ova here for Jupyter Notebook cells. Do you like Jupyter? Do you like @beartype? Then you need beartype 0.18.3 now, because beartype 0.18.2 probably already broke everything without your informed consent. Woops.

Beartype 0.18.3 resolves inscrutable non-determinism (which is technically deterministic if you squint at it, but we don't talk about that) with respect to repeatedly redefined classes defining one or more methods annotated by one or more self-referential relative forward reference (i.e., referring to the class currently being defined). @beartype is now considerably more robust against non-determinism in Jupyter cells containing @beartype-decorated self-referential classes like:

from beartype import beartype

@beartype
class MuhSelfReferentialClass(object):
    def __init__(self, muh_var: int) -> None:
        self.muh_var = muh_var

    @classmethod
    def muh_factory(cls, muh_var: int) -> "MuhSelfReferentialClass":
        '''
        This is fine now. No matter how much you reload the cell
        defining this class, @beartype will still stan for you.

        I have no idea what "stan" even means. I think it's good.
        '''

        return MuhSelfReferentialClass(muh_var + 42)

muh_object = MuhSelfReferentialClass.muh_factory(42)

Flex those burly QA biceps, @beartype. Flex 'em.


things explode when you put @beartype back in the sheath

__class_getitem__ = classmethod(GenericAlias): We Do That Too, Whatever That Is

So. You want to refactor your heroic class that will truly shape the course of human history into a subscriptable type hint factory. You even know about the convenient but unreadable one-line idiom for casting this dark magic. Previously, @beartype refused to support your bad habits arcane knowledge. Now, @beartype understands and appreciates everything you're trying to do for humanity.

Beartype 0.18.3 generalizes the @beartype decorator to support decoration of user-defined types that declare class methods by directly calling the builtin @classmethod decorator as a function passed a C-based callable type (e.g., classmethod(types.GenericAlias)). Doing so enables @beartype to support the standard idiom for user-defined subscriptable type hint factories under Python >= 3.9:

from abc import ABCMeta
from beartype import beartype
from types import GenericAlias

@beartype
class MuhTypeHintFactory(metaclass=ABCMeta):
    '''
    Congrats. Subscripting this class now trivially makes new type hints
    that @beartype fails to understand or appreciate.
    '''

    # This exact one liner appears verbatim throughout the standard
    # library as well as popular third-party packages like NumPy.
    __class_getitem__ = classmethod(GenericAlias)

# Not sure what this means, but you insist you know what you're doing.
# *Do* you, though? *Do* you? @beartype is out to lunch on this one.
MuhTypeHint = MuhTypeHintFactory[str]


the pancakes get me every time. srsly. what is with those pancakes?

Forward Reference Deprioritization: What Does This Even Mean!?

Beartype 0.18.3 deprioritizes @beartype-specific forward reference proxies (i.e., internal objects proxying external user-defined types that have yet to be defined) in type tuples passed as the second arguments to the isinstance() builtin, reducing the likelihood that type-checks involving forward references will raise unexpected exceptions. For example, consider this simple example:

from beartype import beartype
from beartype.typing import Union

@beartype
def explosive_funk(muh_arg: Union['UndefinedType', None] = None):
    print("You thought this was gonna blow up, huh? You're not alone.")
    print("Unless you're in space. In which case you're really alone.")

explosive_funk()

class UndefinedType(object): ...

...which unexpectedly prints without blowing up:

You thought this was gonna blow up, huh? You're not alone.
Unless you're in space. In which case you're really alone.

@beartype type-checks that the default value of the optional muh_arg parameter of the muh_func() function satisfies the type hint 'UndefinedType' | None – despite the fact that the UndefinedType class is undefined! To do so, @beartype now internally reorders the types comprising this union:

# ...from this default type-check, which would raise a decoration-time
# exception due to "UndefinedType" being undefined...
isinstance(muh_arg, (UndefinedTypeProxy, NoneType))

# ...to this default type-check, which should raise *NO* decoration-time
# exception. Why? Because the default value "None" for the "muh_arg"
# parameter satisfies the first "NoneType" type, which then
# short-circuits the isinstance() call and thus ignores the problematic
# "UndefinedTypeProxy" type altogether.
isinstance(muh_arg, (NoneType, UndefinedTypeProxy))

Nobody should ever depend upon this. Therefore, this is a delicious nothingburger – but a delicious nothingburger that could yield future delights in the event that we actually elect to try type-checking defaults at decoration time again. We're not, of course. That would be foolish and dangerous. We're absolutely going to do that again.


rub those cat cheeks! rub 'em!

PEP 563 + PEP 673 + dunder methods.

Beartype 0.18.3 resolves a subtle interaction between PEP 563 (i.e., from __future__ import annotations), PEP 673 (i.e., typing{_extension}.Self), and common dunder methods like... uh, __add__(), I guess. Let's pretend that's common.

Beartype 0.18.3 ensures that the type stack encapsulating the current @beartype-decorated class is now preserved throughout the type-checking process for standard dunder methods annotated by one or more PEP 673-compliant typing{_extension}.Self type hints that are stringified under PEP 563. For example, @beartype now transparently supports pernicious edge cases resembling:

from beartype import beartype
from typing_extensions import Self

@beartype
class MyClass:
    attribute: int

    def __init__(self, attr: int) -> None:
        self.attribute = attr

    def __add__(self, other: int) -> Self:
        self.__class__(self.attribute + other)

If you wanted this, you are literally @iamrecursion. Congrats.


you too will believe that @beartype 0.18.3 actually works

Ping 'Em All

Pinging @posita, @iamrecursion, @sylvorg, @tactile-metrology, @kalaspuff, @danielward27, @kloczek, @uriyasama, @danielgafni, @JWCS, @rbroderi, @AlanCoding, @tvdboom, @crypdick, @WeepingClown13, @RobPasMue, @rbnhd, @radomirgr, @rbroderi.

You are wanted on floor 13. Japanese buildings don't even have a floor 13. Surely nothing could go wrong by violating that fundamental.


those dance moves can mean only one thing... This was @beartype 0.18.3.

Don't miss a new beartype release

NewReleases is sending notifications on new releases.