pypi beartype 0.17.0
Beartype 0.17.0: Ultrabear vs. Mecha-Bugbear

latest releases: 0.21.0, 0.21.0rc0, 0.20.2...
20 months ago

Team Tokyo Bear presents... Ultrabear vs. Mecha-Bugbear, the titanic struggle of ultimate opposites. On the left, @beartype 0.17.0 in the hybrid static-runtime type-checking corner. On the right, the voracious bugs proliferating throughout your codebase in adorable collectable card format.

vs.
a @leycec in the paw is worth two in the mouth of mecha bear

There can be only one victor in your git log.

Wait. What's Happening Here?

@beartype 0.17.0 is a-go-go:

pip install --upgrade beartype

@beartype 0.17.0 descends like Ultraman King german-suplexing Absolute Tartarus onto Tokyo Tower for only like the fifth time. How many times can society rebuild Tokyo Tower before learning to accept that that thing's just a Kaiju magnet for dark monster forces from a mirror pocket universe? Some buildings are better left un-built.

Wait. What were we debating again? Incoherent monologues about Ultraman power levels can only mean one thing:


when you're straddling a giant fish head in the canadian rockies and conehead sumo baby just wanna play

But first...

A Round of Applause for the Homies

We give thanks. I'm humbly and hugely grateful to everyone who's ever financially supported @beartype via GitHub Sponsors. I'm especially grateful to our generous lifetime donors who almost gave a kidney for @beartype. These are @beartype's Three Biggest Fat Bear-cats:

  • @langfield, may their beatific username go down in online fame. May the poets (so, hip hop artists) sing their virtues before a rapt audience of Bay Area techno-futurologists and ChatGPT-embodied automatons. @langfield generously donated more to @beartype than I thought human(e)ly possible. The worst part is that I never gave @langfield what they wanted: deep type-checking. 2024 is the year that @beartype makes good on its promises to @langfield.
  • @patrick-kidger, the extreme scuba-diving Google X globetrotter and likely Thunderball remake stunt double who quietly nominated @beartype for a Google Open Source Peer Bonus award. Shock twist: @beartype won. This is why I now rep @patrick-kidger merch. jaxtyping support? We do that. Equinox support? That too. If it's a @patrick-kidger byproduct, @beartype probably now hawks it on Etsy.
  • @KyleKing, one of @beartype's longest-running GitHub sponsors. He wore glorious glasses that sparkle with the fury of a thousand suns. He's also helped immensely over the years – both financially and across our issue tracker. May @KyleKing achieve his dreams of delivering AI-fuelled nanobots directly into the bloodstreams of millions of Americans in a well-regulated legislative environment that guarantees safety through the power of federal bureaucracy. Hideo Kojima knows where that road goes... and it goes somewhere awesome.

@beartype supporters fight for you. Their username is legend.


@langfield is... Some Dude in a Skintight Rubber Suite.
also featuring @patrick-kidger (left) and @KyleKing (right)
also featuring all your codebase bugs (kaiju gettin rocked)

pytest-beartype: It's a Steaming Hot Thing, Now

Devtools superstar Tushar Sadhwani (@tusharsadhwani) saves everyone's QA bacon with pytest-beartype, @beartype's newest official package. If you always wanted to test-drive @beartype but were too afraid to risk becoming homeless when the whole thing backfired on your last working production server, let pytest-beartype confine @beartype to just your pytest-based test suite. Who cares if pytest burns down, am I right? Anyone?

Let's begin:

  1. Install this steaming hot thing:

    pip install --upgrade beartype
  2. Configure this still-steaming hot thing before it goes lukewarm. You have two choices here, depending on whether you prefer passing temporary command-line options or writing permanent configuration files:

    • Pass the new --beartype-packages='{package_name1},...{{package_nameN}' command-line option to the pytest command, where --beartype-packages is a comma-delimited list of all package names to be infested polluted sullied throttled type-checked by @beartype:

      pytest --beartype-packages='final_doom,u_wut_mate,strawberry.pancakes'  # <-- feels surprisingly good
    • Modify your existing top-level pyproject.toml configuration file with a new [tool.pytest.ini_options] section resembling:

      # In your "pyproject.toml" file...
      [tool.pytest.ini_options]
      beartype_packages = 'final_doom,u_wut_mate,strawberry.pancakes'  # <-- just. do. it.

For those who love CLI warrioring but hate POSIX-compliant shell syntax, bash, u make me hurt inside also check out @tusharsadhwani's zxpy: a Python + bash mashup that basically throws out the entirety of bash. Okay. So, it's more a beatdown than a mashup, really. Bash that bash up, Python!


pytest-beartype surveys all it has done. conclusion: "this is fine"

Beartype: Phase I: Roman Numerals Means Things Just Got Serious

@beartype 0.17.0 hallmarks the end of Beartype: Phase I. The central theme here was shallow type-checking (i.e., type-checking that objects are of the expected types without recursively type-checking any items contained in those objects).

Let's recap in slow-mo. Like that inevitable filler ep where your favourite TV show reboots itself after a five-year gap with all new child actors and a reprehensible script seemingly authored by lizzid people, @beartype wasn't always this good decent acceptable.

@beartype once raised exceptions when confronted with complex, non-standard, or otherwise disreputable type hints. Now, with @beartype 0.17.0, @beartype either passively accepts literally anything you can throw at it by doing nothing or generates shallow or deep type-checking code validating that thing. @beartype no longer explodes. Instead, @beartype permissively tolerates a QA-breaking world full of strife and typing monstrosities it can never fully comprehend.

@beartype is now a Jack-of-All-QA-Trades. @beartype didn't know jack before. Now, @beartype know jack.


@beartype 0.17.0: "Extruded alien protein chunks in my dinner?"

Beartype: Phase II: Roman Numerals Embiggen

@beartype 0.17.0 also hallmarks the beginning of Beartype: Phase II. The central theme here is deep type-checking (i.e., type-checking both that objects are of the expected types and recursively type-checking some or all items contained in those objects). Now that @beartype shallowly type-checks almost everything, it's time to dive into the deep end. Over the course of 2024, @beartype will gradually roll out:

  • Deep type-checking of all type hints in O(1) constant time.
  • Deep type-checking of some type hints in O(n) constant time. When we do this, we'll couple this to an actual deadline scheduler preventing @beartype from consuming more than some preconfigured ratio of wall-clock time.

You may now be thinking:

"But does @beartype 0.17.0 actually do anything?"

...heh. Let's begin.


@beartype 0.17.0: "Mutated alien alligators ain't no thang."

Beartype 0.17.0: The Interquel We All Deserve

@beartype 0.17.0 massively increases the configurability of @beartype. Because everybody always wanted to:

  • Emit non-fatal warnings on type-checking violations? Yup. We got that. Grep violation_*type, then win.
  • Raise custom exception types on type-checking violations? We got that, too. violation_*type grepping intensifies.
  • Raise custom exception messages on type-checking violations? Got that. __instancecheck_str() enters the chat emboldened and swaggering.
  • Modify the verbosity of type-checking violation messages? Got that. violation_verbosity + BeartypeVerbosity is snickering in the back.
  • Reduce complex type hints to simple type aliases in type-checking violations? That is a thing now. type {name} = {hard_stuff} | {moar_stuff}.
  • Blatantly lie about the types your API expects by instructing @beartype to internally transform source to target type hints matching various patterns with type hint overrides? You know we even got that. hint_overrides + BeartypeHintOverrides. It's best not to question this stuff.

These improvements were made possible only by the code-bending thaumaturgy of Montreal API snow wizard @felixchenier (Félix Chénier), who exhaustedly pushed numerous pull requests (PRs) across the git finish line. @beartype is now something actually usable by living humans that breathe oxygen. As a token of our gratitude, please accept this animated Ultraman GIF.


@felixchenier (right) threatens bugs (offscreen) as @leycec (left) supports

To exhibit the fearsome level-up that is @beartype 0.17.0, we now present...

The Flux Beartyper

Like everyone, I used to hate the universal beartype.claw.beartype_all() import hook up until five minutes ago. By default, beartype.claw.beartype_all() dangerously raises fatal exceptions on type-checking violations that occur anywhere in your full app stack – including in code you do not own, have no control over, and mostly could care less about. But what if you could configure beartype.claw.beartype_all() to instead emit non-fatal warnings rather than destroy your entire app due to somebody else's sins?

Thankfully, it happened. I slipped off a toilet while hanging a clock shaped like a hibernating bear, banged my head on a towel rack shaped like a spawning salmon, and... I saw it there. The Flux Beartyper:

# In your "{your_package}.__init__":
from beartype import BeartypeConf
from beartype.claw import beartype_this_package, beartype_all

beartype_this_package()  # <-------------------------------------- raise exceptions for your package
beartype_all(conf=BeartypeConf(violation_type=UserWarning))  # <-- emit warnings for everyone else's

That's it. That's the Flux Beartyper. This K-k-k-killer Combo compels @beartype to:

  • Enforce type hints across the current package by raising fatal exceptions on type-checking violations in this package.
  • Complain about (but do not enforce) type hints across all other packages and the standard library itself by emitting non-fatal warnings on type-checking violations in those packages.

The Flux Beartyper is thus the superset of mypy and pyright: it does everything those guys do (complain about everything), while also doing something those guys can never do (actually enforce something). Moreover, it selectively enforces those things only on the one thing you have under your total control: your own codebase.


in the endless struggle of bad versus good code, only roundhouse chops to the scaled carapace will decide the fate of your investment portfolio

BeartypeConf Explodes with Greasy New Possibilities

@beartype configurations just got a whole lot embiggened. Since the lesson of my childhood is that bigger is always better, we feel happy about this startling explosion of unmaintainable technical debt and bewildering code complexity.


is that what our lives have come to

violation_*type: When You Know Better, You Better Tell @beartype

By default, @beartype raises thoughtful but eyebrow-raising type-checking violations with exception types like beartype.roar.BeartypeCallHintParamViolation and beartype.roar.BeartypeCallHintReturnViolation. Fine-grained granularity. That's just great... isn't it?

But what if you hate that? What if you love coarse-grained generality instead? What if you really just want @beartype to raise TypeError exceptions on type-checking violations like everything else in the bloody runtime type-checking community already? Previously, those users had to grit their teeth until grinding their molars down into stubs. You know, what does "grit teeth" even mean? Why can you grit teeth but not anything else? I've always wanted to grit my toes. Can't do it. Grit my lips? It's right out.

For those who are about to grit their keyboards, @beartype 0.17.0 introduces a new secret brotherhood of BeartypeConf options governing the types of violations it produces:

  • (Recommended) violation_type, the default type of exception raised by @beartype when a type-checking violation occurs – any type-checking violation, including:
    • When an object passed to die_if_unbearable() violates a type-check.
    • When a parameter of a @beartype-decorated callable violates a type-check.
    • When a return of a @beartype-decorated callable violates a type-check.

Most users who want to configure violations want to pass this option. Defaults to None, in which case @beartype preserves backward compatibility by just doing what it currently does – which is perfectly fine, of course. No shade on @beartype defaults. Obsessive-compulsives may also fine-tune:

  • violation_door_type, the type of exception raised by @beartype when an object passed to die_if_unbearable() violates the passed type hint. Since @beartype type-checks PEP 526-compliant annotated variable assignments (e.g., godzilla: Sequence[KaijuThatHateTokyo] = Gojira('RAAAR!')) by internally calling die_if_unbearable(), this is also the type of exception raised when an annotated variable violates its type hint. Defaults to beartype.roar.BeartypeDoorHintViolation.
  • violation_param_type, the type of exception raised by @beartype when a parameter violates its type hint. Defaults to beartype.roar.BeartypeCallHintParamViolation.
  • violation_return_type, the type of exception raised by @beartype when a return violates its type hint. Defaults to beartype.roar.BeartypeCallHintReturnViolation.

violation_type is merely a convenience enabling users to trivially control the violation_door_type, violation_param_type, and violation_return_type parameters without having to explicitly pass all three of those parameters.

Pretend that @beartype is normal. It feels good and @beartype can no longer complain:

from beartype import BeartypeConf
from beartype.claw import beartype_this_package

# My spirit guide says that normalcy is a state of mind. Yet, you disagree.
beartype_this_package(conf=BeartypeConf(
    violation_type=AttributeError,     # <-- actually, never pass "AttributeError"
    violation_door_type=RuntimeError,  # <-- ..................or "RuntimeError"
    violation_param_type=TypeError,    # <-- Okay. Fine. This is okay.
    violation_return_type=ValueError,  # <-- passing "ValueError": not a great idea
))


does that dude in the back really have drills for arms? really? so cool

violation_*type: When Exceptions Are Too Scary for the Slumber Party

Warnings are exceptions in Python. I know, right? Who knew. All these years. The builtin Warning class subclasses the builtin Exception class. Oddly, this implies that warnings are technically raisable as exceptions. They are, but you shouldn't. Warnings should only be emitted with warnings.warn().

You know that – but does @beartype? Does @beartype acknowledge this distinction? Imagine me now saying: "Nope. @beartype sucks. It just raises warnings like exceptions." That... would be a pretty bad look. Even Ultrabear would frown. That is why I am now instead saying the opposite.

@beartype rules! When you pass a Warning subclass as a violation_*type option, @beartype detects that as your attempt to emit non-fatal warnings from type-checking violations and then does so by dynamically generating type-checking code that calls warnings.warn().

Is there a real-world application? There are so many I cannot count them all on my vestigial drill hands. Pretending that @beartype is mypy is one. This is another: gradual adoption.

Imagine a monolithic codebase named FuglyBugs that hates you. That codebase is a sprawling million-line ghetto of badly typed spaghetti whose most inventive feature is single-letter attribute names in the Unicode Tertiary Ideographic Plane. You're the new guy next to the gurgling water cooler that leaks suspicious fluid all over the floor according to a Poisson distribution with a high λ. It's do or die. The garbage is piling up in the corridor. Your grizzled landlady is hissing about "overdue rent" or something. Who cares, landlady? But the cats are hissing as well. You can't throw @beartype directly at that codebase without destroying your nascent life story. So what a somber devops goin' do?

You gradually adopt a QA bear cub today, the @beartype way:

# In your "your_package.__init__" submodule:
from beartype import BeartypeConf
from beartype.claw import beartype_this_package

# Emit non-fatal warnings on type-checking violations from your own package.
# The monolithic codebase that you preserve might just be named `FuglyBugs`.
beartype_this_package(conf=BeartypeConf(violation_type=UserWarning))

Of course, you can pass an app-specific UserWarning subclass rather than UserWarning. And... you should probably do that.


FuglyBugs! spit it! who did this 2 u!?

violation_verbosity + BeartypeVerbosity: Massage Your Brain with Beartype

Let us breathe out and then back in and then... actually please keep doing that. Let it never be said that @beartype gives bad advice. Wait. What is this, a yoga class in our release notes? What were we talking about again? Which is an appropriate lead-in to...

Verbosity. Prior versions of @beartype were verbose (like this changelog). Type-checking violation messages included the full contents of the current beartype configuration, which now contains an infinite "wealth" of options.

@beartype 0.17.0 curtails that insanity by dialing down on the prolix nebulosity. Beartype configurations are no longer embedded in violations by default, because your sanity is our personal responsibility. Somebody hates this change and is now thinking: "But I like the old way. I like parking tickets, too."

@beartype 0.17.0 is here for that somebody, introducing a new violation_verbosity option that governs violation verbosity. The value of this option is one of the following members of our new beartype.BeartypeVerbosity integer enumeration:

  • BeartypeViolationVerbosity.MINIMUM, intended for end users potentially lacking core expertise in Python. Babies, in other words. This is for babies.
  • BeartypeViolationVerbosity.DEFAULT, intended for a general developer audience assumed to be fluent in Python but vengeful on GitHub. tentatively raises hand
  • BeartypeViolationVerbosity.MAXIMUM, extending the default verbosity with additional metadata intended for inadvisable all-nighter debugging sessions that end in shaking, weeping, and puffy cheeks. This includes:
    • A partial diff of the beartype configuration under which this violation occurred. By "partial diff," I mean only the subset of configuration options explicitly set by you that differ from their default values. Previously, violations unnecessarily described all options -- even those not explicitly set by you that defaulted to their default values. Awful.

violation_verbosity defaults to BeartypeViolationVerbosity.DEFAULT, because your brain is a precious quantity. But you know better.

# In your "{your_package}.__init__":
from beartype import BeartypeConf, BeartypeVerbosity
from beartype.claw import beartype_this_package

beartype_this_package(conf=BeartypeConf(
   violation_verbosity=BeartypeVerbosity.MAXIMAL))  # <-- vomitous output: *ON*


never trust a mechanized triceratops named Yapool is all I'm sayin'

hint_overrides + BeartypeHintOverrides: Who You Gonna Believe?

Have you ever wanted to lie to your userbase, static type-checkers, other runtime type-checkers, and document generators alike? Now you can.

Let's back up. PEP 484 – the standard named "Type Hints," so that's probably what it's about – included this bizarre substandard see wut i did there named the implicit numeric tower. The idea was simple, albeit horrible. Static type-checkers would just globally replace all:

  • float types in type hints with float | int unions.
  • complex types in type hints with complex | float | int unions.

That's fine. When your significant other says that, it's absolutely not fine. This is like that. Because globally replacing types in type hints without user consent which then reduces numerical precision isn't actually that cool. So, @beartype only conditionally supports the implicit numeric tower. If you want us to do that, that's cool, but you have to opt in by enabling is_pep484_tower=True.

So far, so good. But what about third-party scalars published by packages like NumPy and SymPy? Third-party scalars don't subclass builtin scalars (e.g., numpy.int_ does not subclass int). But real-world "tough guy" data science and machine learning mostly uses third-party scalars rather than builtin scalars. The implicit numeric tower is thus obsolete for most of us. Sadness overflows my pewter mug shaped like a grizzly bear.

So... what now, PEP 484? Huh? Let @beartype 0.17.0 tell you what now.

@beartype 0.17.0 introduces yet another outrageous new BeartypeConf option: hint_overrides, whose value is a beartype.BeartypeHintOverrides instance mapping source to target type hints. And... BeartypeHintOverrides is an in-house immutable dictionary type (i.e., pure-Python @beartype-specific implementation of a hypothetical frozendict builtin), enabling the memoized BeartypeConf dataclass to accept dictionaries while preserving caching. And... @beartype globally and recursively substitutes all type hints that are keys of the hint_overrides dictionary with their corresponding values. And... can this get any more complicated? The answer is: "Yes." You feel very tired.

hint_overrides is a generalization of the implicit numeric tower. Like the implicit numeric tower, you're lying to everybody. Unlike the implicit numeric tower, your lies are no longer constrained to what PEP 484 fed you; you can now lie about everything. And you should! Lies are healthy. "@beartype said so."

Crazily, Python has no official frozen dictionary type. @beartype had to make up its own. I grunt meaningfully and then point back to the chalkboard, which now resembles a Cthulhian nightmare of random scribbling from beyond the realm of sleep.

Since hint_overrides generalizes the implicit numeric tower, you can now explicitly express the implicit numeric tower by instead passing:

# Tiresome machinery. Why won't you import yourself, already?
from beartype import BeartypeConf, BeartypeHintOverrides

# This new beartype configuration explicitly does the same thing as...
explicit_numeric_tower = BeartypeConf(hint_overrides=BeartypeHintOverrides({
    float: float | int,
    complex: complex | float | int,
})

# This old beartype configuration.
implicit_numeric_tower = BeartypeConf(is_pep484_tower=True)

B-b-but what if you want to actually do something useful? Specifically, what if you want an API typed as matching only builtin scalars to also transparently match third-party scalars? Behold! You weave a web of tangled lies, but it all works out in the end. You boost your end-of-life karmic score alot higher than all those beleaguered champions who stoically fight for justice:

# Even more machinery. Just import yourself, already!
import numbers
from beartype import BeartypeConf, BeartypeHintOverrides

# Beartowertype: the ultimate @beartype configuration for numerical analysis.
# If you can't count the statistical variance of your lies, neither can I.
beartowertype = BeartypeConf(hint_overrides=BeartypeHintOverrides({
    int: numbers.Integral,
    float: numbers.Real,
    complex: numbers.Complex,
})

Users don't understand what numbers.Integral means. But users do understand what int means. Okay. Not all users. A majority of users. Okay. Not even that. A few users understan... Okay. You understand what int means.

Meanwhile, NumPy, SymPy, and everybody else understands what numbers.Integral means. They register their own third-party scalars with PEP 3141-compliant abstract base classes (ABCs) defined by the standard numbers module, also referred to as the "explicit numeric tower".

By instructing @beartype to internally replace all builtin scalar types like int with corresponding ABCs in the explicit numeric tower like numbers.Integral, you have made your own Ultimate Implicit Numeric Tower: an implicit numeric tower that actually works, because it actually supports stuff you care about. Because you know best. You do you. Now, @beartype does too. wait, what does that one-liner actually mean


caring means punching a monster in the gut for humanity

Beartype Presents... The Félix Chénier Connection

Let's put all of the above together. Courtesy Félix Chénier, the Université du Québec à Montréal du Canada du Planet Earth du Milky Way Spiral Galaxy du Material Universe mad lad that made all this possible, @beartype presents the Big and Tall @beartype configuration:

# In your "{your_package}.__init__":
from beartype import BeartypeConf, BeartypeHintOverrides, BeartypeVerbosity
from beartype.claw import beartype_this_package, beartype_all

beartype_this_package(conf=BeartypeConf(  # <-- This. Is. His. Way.
    violation_type=TypeError,  # <-- what is @beartype to you, a joke?
    violation_verbosity=BeartypeVerbosity.MINIMAL,  # <-- SILENCE, HUMANS!
    hint_overrides=BeartypeHintOverrides({  # <-- NumPy + SymPy = ancient power
        int: numbers.Integral,
        float: numbers.Real,
        complex: numbers.Complex,
    }),
))
beartype_all(conf=BeartypeConf(
    violation_type=UserWarning,  # <-- unexpected Flux Beartyper returns
))

Configure @beartype like Félix Chénier would. The feeling of power is delicious, yet kinda indescribable.


so this is what feels like when monsters cry

Violation Readability: The Tesseract Unfolds

@beartype once raised hair-raising type-checking violations with messages straight outta the Seventh Circle of Typing Hell. Wince and cringe as your third eye bleeds from its calcified perch in the pineal gland!

beartype.roar.BeartypeCallHintParamViolation: @beartyped __main__.f() parameter
x="Array(1, dtype=int32, weak_type=True)" violates type hint <class
'jaxtyping.Float[Array, 'dim1']'>, as <protocol "jaxlib.xla_extension.ArrayImpl">
"Array(1, dtype=int32, weak_type=True)" not instance of <class
"jaxtyping.Float[Array, 'dim1']">.

Indeed, the dawn of typing prehistory was a dark time. Those days are long behind us, though. @beartype 0.17.0 now delivers two new extensible APIs for generating readable messages designed by you, consumed by your userbase, and complained about relentlessly on your issue tracker. @beartype is no longer responsible for anything! I'm weeping with joy here.


@beartype: just some rando in a skin-tight rubber suit after all

__instancecheck_str__(): Just Do It Yourself, Because Our Way Sucked

@beartype 0.17.0's unleashes our first official plugin API: the __instancecheck_str__() protocol, a new double underscore method accepting a single object that violates the current class and returning a human-readable substring describing that violation. __instancecheck_str__() intentionally shares a similar name with the standard __instancecheck__() dunder method; both are defined on the metaclass of a class. Whereas __instancecheck__() returns a bool, however, __instancecheck_str__() returns a str. The full signature resembles:

    def __instancecheck_str__(cls, obj: Any) -> str:

This will soon make sense. Would I lie? This is your final API for raising human-readable violations:

from beartype import beartype

# Define __instancecheck_str__() on the metaclass.
class MetaclassOfMuhClass(type):
    def __instancecheck_str__(cls, obj: object) -> str:
        return (
            f'{repr(obj)} has disappointed {repr(cls)}... '
            f'for the last time.'
        )

# The actual class is the same as it always was. So boring.
class MuhClass(object, metaclass=MetaclassOfMuhClass):
    pass

# Use that class as a type hint everywhere.
@beartype
def muh_func(muh_obj: MuhClass) -> None:
    pass

# Cheers! You now have all the power while @beartype weeps in the corner.
muh_func("Some strings, you just can't reach.")

...which now raises the humane violation:

beartype.roar.BeartypeCallHintParamViolation: Function __main__.muh_func()
parameter muh_obj="Some strings, you just can't reach." violates type hint
<class '__main__.MuhClass'>, as "Some strings, you just can't reach." has
disappointed <class '__main__.MuhClass'>... for the last time.

Simple, right? And it actually is. Caveats may apply. Notably, the string you return from your __instancecheck_str__() implementation:

  • Should be a sentence fragment whose first character is typically not capitalized.
  • May optionally be suffixed by a trailing period. In either case, @beartype intelligently does the right thing and ensures that the entire violation message is always suffixed by only a single trailing period.

__instancecheck_str__() is intended to be supported by competing runtime type-checkers (e.g., typeguard, Pydantic) as a pseudo-standard. Naturally, ain't nobody got the time to write an actual PEP for that. Pseudo-standard.


just another day at the kaiju office ends on a terrifying footnote

PEP 695: Only the Third Time Python Standardized Type Aliases

@beartype 0.17.0 now sorta supports PEP 695-compliant type aliases (i.e., type hints with simple names whose values are other more complex type hints, instantiated by statements of the form type {alias_name} = {alias_value} under Python ≥ 3.12). Type aliases are useful for improving the readability of type-checking violations. Type aliases are also mostly broken by runtime CPython deficiencies. One out of two ain't bad.

The numpy.typing.ArrayLike union is the canonical use case. Shield your child's eyes from this abomination beyond from the Chaos Gate:

# What could be simpler? <-- something nobody should ever say
>>> from numpy.typing import ArrayLike
>>> ArrayLike
typing.Union[
    collections.abc.Buffer,
    numpy._typing._array_like._SupportsArray[numpy.dtype[typing.Any]],
    numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[typing.Any]]],
    bool, int, float, complex, str, bytes,
    numpy._typing._nested_sequence._NestedSequence[
        typing.Union[bool, int, float, complex, str, bytes]]
]  # ...so. literally everything is array-like, huh? even bools, huh? *sigh*

Now imagine – in the vast, cavernous, and crawling darkness beyond your eyelids – what happens when you annotate @beartype-decorated classes and callables with numpy.typing.ArrayLike. Thankfully, you don't even have to imagine:

>>> from beartype import beartype
>>> from numpy.typing import ArrayLike
>>> @beartype
... def run_nurgle_run(like_an_array: ArrayLike) -> None: pass
>>> run_nurgle_run(('Blood for the Blood God!', 'Skulls for the Skull Throne!',))
beartype.roar.BeartypeCallHintParamViolation: Function __main__.run_nurgle_run()
parameter like_an_array=('Blood for the Blood God!', 'Skulls for the Skull
Throne!') violates type hint typing.Union[collections.abc.Buffer,
numpy._typing._array_like._SupportsArray[numpy.dtype[typing.Any]],
numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[typing.Any]]],
bool, int, float, complex, str, bytes,
numpy._typing._nested_sequence._NestedSequence[typing.Union[bool, int, float,
complex, str, bytes]]], as tuple ('Blood for the Blood God!', 'Skulls for the
Skull Throne!'):
* Not bool, float, str, <protocol ABC "collections.abc.Buffer">, complex, bytes, or int.
* Not instance of <protocol "numpy._typing._array_like._SupportsArray">.
* Not instance of <protocol "numpy._typing._nested_sequence._NestedSequence">.
* Not instance of <protocol "numpy._typing._nested_sequence._NestedSequence">.

Tell me that you don't understand what I'm saying without telling me that you don't understand what I'm saying, @beartype.

Now consider this human-readable alternative that truncates the above abnormal outgrowth of code logorrhea into something even Ultrabear's mother could love:

# Don't try this under Python < 3.12. Just... don't.
>>> from beartype import beartype
>>> from numpy.typing import ArrayLike as _ArrayLike
>>> type ArrayLike = _ArrayLike  # <-- tautological nonesense *or* 4d chess move?
>>> @beartype
... def run_nurgle_run(like_an_array: ArrayLike) -> None: pass
>>> run_nurgle_run(('Blood for the Blood God!', 'Skulls for the Skull Throne!',))
beartype.roar.BeartypeCallHintParamViolation: Function __main__.run_nurgle_run()
parameter like_an_array=('Blood for the Blood God!', 'Skulls for the Skull
Throne!') violates type hint ArrayLike, as tuple ('Blood for the Blood God!',
'Skulls for the Skull Throne!') not tuple ('Blood for the Blood God!', 'Skulls
for the Skull Throne!'):
* Not bool, float, str, <protocol ABC "collections.abc.Buffer">, complex, bytes, or int.
* Not instance of <protocol "numpy._typing._array_like._SupportsArray">.
* Not instance of <protocol "numpy._typing._nested_sequence._NestedSequence">.
* Not instance of <protocol "numpy._typing._nested_sequence._NestedSequence">.

Saner. Terser. Arguably, even readable. @beartype still explains the violation without lore-dumping the squalid guts of numpy.typing.ArrayLike, which is now abbreviated to simply ArrayLike.

Caveats apply, because this is @beartype. Due to character flaws beyond my control (...video games is what I'm saying), @beartype currently only partially supports type aliases. Notably, @beartype:

  • Fully supports type aliases containing neither forward references nor recursion. Bears cheer!

    type ChooseYourEpicFate = str | int  # <-- this is fine
  • Conditionally supports global type aliases (i.e., defined as global attributes at module scope) containing forward references but not recursion under the proviso that you only automatically apply @beartype via its beartype.claw import hooks. If you manually apply @beartype via the @beartype.beartype decorator, however, @beartype will raise exceptions on encountering any type aliases containing forward references. Why? Because PEP 695 is fundamentally broken and lies about everything. More bears half-heartedly cheering while crying at the same time.

    type UnseenHorror = ClassOfDoom | ClassOf94  # <-- this is sorta fine...
                                                 # <-- if you "beartype.claw";
                                                 # <-- else, defly *NOT FINE*.
  • Cannot support local type aliases (i.e., defined as local attributes in callables) containing forward references. Sadly, no subsequent @beartype release is expected to support this use case. For unknown reasons that would probably bore and anger all of us in equal measure, CPython's runtime implementation of local annotation scopes is fundamentally, irredeemably, and profoundly broken. Thus, bears cry.

    def dry_your_tears_on_my_git_stash() -> None:
        type AyyLmao = LocalAreaMan | UniversalAreaGrey  # <-- *NOT FINE*
  • Does not support type aliases containing recursion. Unlike the prior bullet point, @beartype can theoretically fully support this use case. It simply chooses not to at the moment, because it is very tired and must now lie down. A subsequent @beartype release is expected to fully support recursive type aliases. Unenthusiastic bears roll around on your front lawn.

    type FurBaby = TerrorCat | DogBreath | list[FurBaby] # <-- *NOT FINE*... yet

You may now be thinking: "Uhh... how can @beartype support type aliases containing forward references declared at global but not local scope?" Actually, who am I even kidding? Nobody cares. The audience for type aliases consists of two Capuchin monkeys that mostly just chortle as they tickle one another and a microdosing banana. For them, allow my disillusioned younger self to copy-paste himself from @beartype's git log:

The most problematic and troubling aspect of PEP 695 with respect to runtime
type-checking is its horrifying, terrifying, and frankly shocking lack of
runtime support for forward references.
Although PEP 695 repeatedly
mic-drops forward references as a central motivation for its existence, the
actual runtime implementation of PEP 695 lacks any support whatsoever for
forward references and even goes to elaborate and outrageous lengths to
prevent runtime type-checkers from resolving forward references in PEP
695-compliant type aliases. Yet again, a new runtime-hostile PEP arises.
Thankfully, this is @beartype. We do what we want here. And what we want here
is to fully dismantle PEP 695, break it, and then bend it to our perfidious
will until this Accursed Abomination Unto Nuggan does what it was advertised
but failed to do at runtime. So says the Bear.

Wait. I can hear the Capuchin monkeys and microdosing banana ruminating already: "If PEP 695 lacks runtime support for forward references, then how does @beartype actually support global type aliases containing forward references?" Thank you, banana-monkey. I'll take it from here.

This is where beartype.claw import hooks come in. When you apply import hooks, what you're really doing is applying abstract syntax tree (AST) transformations that transmute your crippled-by-design CPython code into an entirely new language of our own devising: pybearthon. Pybearthon could mostly care less whether or not CPython itself is broken, because pybearthon walks slithers its own way. In this case, pybearthon silently transforms...

# This otherwise broken global type alias...
type UnseenHorror = ClassOfDoom | ClassOf94

# ...into this suddenly worky global type alias!
from beartype._util.hint.pep.proposal.utilpep695 import (
    iter_hint_pep695_forwardref as __iter_hint_pep695_forwardref_beartype__)
type UnseenHorror = ClassOfDoom | ClassOf94
for _ in __iter_hint_pep695_forwardref_beartype__({alias_name}):
    globals()[_.__name_beartype__] = _  # <-- don't ask. srsly. just... don't.

Because PEP 695 is fundamentally broken, that same AST transformation fails at local scope with insane exceptions like:

NameError: cannot access free variable 'ClassOfDoom' where it is not associated
with a value in enclosing scope

Ergo, runtime type-checkers can only support global type aliases containing forward references. You now regret your frank line of questioning, banana-monkey.

An even worse caveat applies, however. Yet again, it's not @beartype's fault. Python ≤ 3.11 hates type aliases. By "hates," I mean "Python ≤ 3.11 raises non-human-readable SyntaxError exceptions at bytecode generation time on attempting to import any module containing even a single type alias regardless of where in that module that type alias is." This is True hatred: a new plateau of hate-filled overkill hitherto unknown to your handlebar mustache-twirling boss.

Even if you try to hide type aliases from older Python versions behind if conditionals like if sys.version_info >= (3, 12):, your circumlocution fails. Then, at last, you know @leycec to be an insufferable oracle of horrible truth. Hiding doesn't work. You either need to:

  • Isolate all type aliases across your entire codebase to a unique submodule conditionally imported only under Python ≥ 3.12.
  • Conditionally and dynamically declare type aliases with the exec() builtin hidden behind clever if conditionals like if sys.version_info >= (3, 12):. This is the lazy way. Thus, this is what we do below.

You are now thinking: "type aliases seriously suck, dude. Seriously." Allow me to now dispel all your justifiable fears with edgelord code that should make you cringe. I say, "Embrace the cringe." Let's goooooooooooooo:

# Pretend this means something to you.
from numpy.typing import ArrayLike as _ArrayLike
from sys import version_info
from typing import TYPE_CHECKING, NewType, TypeAlias

# If mypy or pyright, pacify mypy or pyright with... deprecated syntax!?!?
if TYPE_CHECKING:
    ArrayLike: TypeAlias = _ArrayLike
# Else, we are Python.
#
# If we are Python ≥ 3.12, dynamically declare a type alias to avoid
# "SyntaxError" complaints from older Python interpreters.
elif version_info >= (3, 12):
    exec('type ArrayLike = _ArrayLike')  # <-- stupidly clever or just stupid? you decide
# Else, we are obsolete Python. In this case, abuse PEP 484 for justice.
else:
    ArrayLike = NewType('ArrayLike', _ArrayLike)

So who is going to use type aliases if you can't use them under Python ≤ 3.11 without soul-destroying boilerplate and can only use them under Python ≥ 3.12 subject to a litany of context-sensitive caveats that even your family lawyer who routinely represents banana-monkeys can't get right, exactly?

Nobody. The answer is nobody.


that feeling when you realize you wasted ten minutes of your life

Unrecognized Subscripted Builtin Type Hint: Yeah, We Do That Too

A brief history in futility and the sound of one keyboard clapping.

A decade (but what feels like a lifetime) ago, CPython devs made the ignominious decision to externalize all type hints for the standard library into a third-party package inaccessible to runtime type-checkers named typeshed. According to the Python mailing list, "Accurate typing is hard!" I am now heaving my emaciated arms up into the air. You already did the hard work in the typeshed, CPython devs. Can't you literally copy-paste type hints from the typeshed into the standard library? How hard is repeatedly hitting two key chords on a keyboard? This isn't rocket science or even figuring out how to bottle-feed a disgusting slurry of processed fish guts to a vicious Bengal cat with a deplorable attitude stricken by Calicivirus. That was rocket science. This is only disappointment.

To compound matters, CPython, typeshed, and mypy authors (whose Venn diagram is a perfect circle) quietly collude to implement non-standard type hints. The way this nefarious social network works is pretty simple: it's absolutely not simple

  1. typeshed devs intend to annotate something in the standard library for which no standard type hints exist.
  2. CPython devs quietly augment an existing type to support subscription (i.e., the __getitem__() dunder method) without documenting anything or standardizing the semantic meaning of the resulting object.
  3. mypy devs quietly interpret subscription of that type as a non-standard type hint in an obscure and undocumented mypy-specific manner.
  4. typeshed devs annotate things in the standard library using those non-standard type hints in an obscure and undocumented mypy-specific manner.

I'm not bitter. I just look bitter. My face is permanently frozen into a weatherbeaten rictus of crag lines, worry warts, and wrinkle canyons.

The point is that weird undocumented type hints internally used throughout the CPython community now exist. Is that a problem for us? Yes. Their avoidance of the PEP standards process makes this our problem. The @beartype userbase has somehow become aware of and now wants us to support these weird undocumented type hints – including:

  • weakref.weakref[...] type hints. Pretty obvious what that semantically means, right? weakref.weakref[str] is a weak reference to a string, for example. Makes sense. But then what about...

  • os.PathLib[...] type hints. "uhh wat?" Yeah. It's undocumented, but you can actually subscript the standard os.PathLib type by another type. The semantic interpretation is not at all obvious; since nothing is documented, I had to reverse engineer the semantic interpretation by grepping the mypy issue tracker for a hot minute. Surprisingly, this is what os.PathLib[T] means:

    from typing import Generic, TypeVar, Union
    
    T = TypeVar('T', bound=Union[str, bytes])
    
    class PathLike(Generic[T]):
        def __fspath__(self) -> T: ...  # <-- hoh, boy

The point is that this sucks. Because this sucks, @beartype 0.17.0 now shallowly type-checks all weird undocumented type hints internally used throughout the CPython community by reducing those hints to their origin class (i.e., by just stripping subscription from those hints). For example, @beartype now reduces:

  • All weakref.weakref[T] type hints to the weakref.weakref class.
  • All os.PathLib[T] type hints to the os.PathLib class.

@beartype 0.17.0: we support bad stuff, because we support you. Wait... That one-liner kinda didn't sound right. You're not bad stuff. You're awesome sauce. But your awesome sauce needs bad stuff. Let's try this one more time.

@beartype 0.17.0: all the bad stuff, now in one easy convenient package.


only a flying water bottle on fire holds the key to planetary salvation

Isomorphic Non-closure Wrapper: Changelog from Hell Continues

ohmygodswontthischangelogquitalready

Apparently, this is fine now:

from beartype import beartype
from functools import wraps

def muh_func(muh_arg: int):  # <-- wat!? no @beartype!? but how can this be?
    pass

@beartype  # <-- oh, okay. here's the @beartype. phew. that was close
@wraps(f)  # <-- standard decorator idiom
def muh_wrapper(*args, **kwargs):
    pass

When a changelog just needs to stop already, animated Ultraman GIF.


Cobra Guy encourages at-risk codebases to try a bit harder

@patrick-kidger: If He Didn't Type It, You Shouldn't Use It

Lastly, @beartype 0.17.0 officially:

  • Supports @patrick-kidger's Equinox now. JAX + ML + @beartype = pretty sure you just got a raise.

  • Does not support nptyping, because (A) nptyping is dead and (B) nptyping is bad. Dead and bad are both bad. If you attempt to use nptyping-based type hints under @beartype ≥ 0.17.0, @beartype will raise fatal exceptions informing you that you are bad by association: e.g.,

    beartype.roar.BeartypeDecorHintPep604Exception: Type hint
    NDArray[Shape['N, N'], Float] inconsistent with respect to repr()
    strings. Since @beartype requires consistency between type hints and
    repr() strings, this hint is unsupported by @beartype. Consider
    reporting this issue to the third-party developer implementing this
    hint: e.g.,
            >>> repr(NDArray[Shape['N, N'], Float])
            NDArray[Shape['N, N'], Float]  # <-- this is fine
            >>> repr(NDArray[Shape['N, N'], Float] | int)
            nptyping.ndarray.NDArray | int  # <-- *THIS IS REALLY SUPER BAD*
    
            # Ideally, that output should instead resemble:
            >>> repr(NDArray[Shape['N, N'], Float] | int)
            NDArray[Shape['N, N'], Float] | int  # <-- what @beartype wants!

What @beartype is saying is that nptyping is currently unmaintained, suffers over 20 severe issues, and emits 7 severe NumPy deprecation warnings. nptyping is a ticking time bomb about to explode your codebase into radiating black bodies. Bear bros don't let bear bros import nptyping. Not even once.

Thankfully, @patrick-kidger exists. hard to prove, but likely true Everybody wants to transition from nptyping to @patrick-kidger's Google-adjacent jaxtyping package already. Unlike nptyping, jaxtyping provides well-maintained type hints covering NumPy, JAX, PyTorch, and TensorFlow. It's kinda intense. It's also @beartype's official FAQ recommendation for type-checking NumPy arrays.

The data science pipeline you save might just be your own. It's probably too late, though.


your codebase when you realize you used nptyping everywhere

Biosecurity Tickets: No Longer Something I Grovel on the Floor For

Previously, I publicly begged for GitHub Sponsors with a flashy logo believed to be reminiscent of sketchy underground raves – which, of course, none of us know anything about or have ever attended. awkward collar tugging

Instead, I now implore you to donate that same money to local charities, food banks, animal shelters, homeless shelters, and every other institution that does tangible real-world good. Humanity isn't doing so h0t in 2024. We don't talk about that, because we're supposed to do fun here. Coding is our wheelhouse.

But it's largely institutions – both corporate and governmental – that probably should have supported open-source initiatives like @beartype. That should never have fallen on the heavy shoulders of individuals. As actual persons, our responsibilities are mostly to the local: local lives, local families, and local communities.

I'm not closing my GitHub Sponsors, because I put too much work into that flashy logo and now I'm emotionally invested. But I am beginning to moulder and stew in my brain sauces and think:

"What next? How do we positively incentivize open-source volunteerism without disproportionately burdening the individuals that already bear up too much?"

I don't know. I'm just a gawping code monkey that loves Ultraman. But I do know that the solution almost certainly involves gifting turn-based strategy and role-playing and Yakuza videogames on Steam to @leycec.


@beartype userbase: make a better world through glowing orbs

To End a Changelog: Greets to the Greatest

Thanks so much, everybody. These bear bros went above and beyond the mating call of Spring to deliver a better QA UX for us all:

@langfield, @patrick-kidger, @KyleKing, @felixchenier, @posita, @wesselb, @justinchuby, @EtaoinWu, @kaparoo, @kasium, @peske, @tvdboom, @MaximilienLC, @fleimgruber, @alexoshin, @AdrienPensart, @uriyasama, @sunildkumar, @mentalisttraceur, and @skeggse, @reikdas, @mentalisttraceur, and @tusharsadhwan – may your usernames reign supreme and then lord that supremacy over all of us.

These GitHubbers hugged @beartype by starring it recently. Little did they know, but they were inviting a disastrous chain of consequences culminating in my now pinging them:

@Vol0kin, @alimoezzi, @phsilva, @shyamsn97, @antoniomdk, @procore, @hbakri, @dcharatan, @motherwort, @jxu, @doraut, @joaoapel, @AntoineD, @double-thinker, @tcl326, @Guiforge, @Valendrew, @jeffswt, @lxdlam, @padraic-shafer, @bzczb, @xhiroga, @justincase-jp, @thapecroth, @ZachariahPang, @axrn, @mjwen, @calebj, @Holocor, @anagri, @nhanarT, @dfundingsland, @DavidHernandez21, @namurphy, @makaronma, @urimandujano, @damtien444, @deutschmn, @Fizzadar, @AnjieCheng, @Bullish-Design, @ericfeunekes, @cameronraysmith, @sfrieds3, @garthk, @LiamBrenner, @abecciu, and @moddyz – may @beartype 0.17.0 give you everything you've always wanted except deep type-checking of dictionaries.

It's my birthday tomorrow. I will be eating cake covered in ice cream and playing video games all day. Thank you. May @beartype bless your codebase.


@beartype feels that feeling, too

Don't miss a new beartype release

NewReleases is sending notifications on new releases.