Skip to content

Beartype 0.15.0

Compare
Choose a tag to compare
@github-actions github-actions released this 22 Jul 05:24
· 241 commits to main since this release

@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.

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?