pypi beartype 0.16.0
Beartype 0.16.0: Super Unsexy Stabilization Asinine Force (SUSAF)

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

@beartype 0.16.0 [codename: Super Unsexy Stabilization Asinine Force (SUSAF)] boringly stabilizes everything unstable about @beartype that made your coworker who only wears skinny jeans while squinting whenever you mention @beartype stop doing those things. Fix everything bad, SUSAF!

python3 -m pip install --upgrade beartype

@beartype 0.16.0 mostly introduces nothing new. Instead, @beartype 0.16.0 just fixes everything that's been broken for several years. We all tried to pretend that those things worked. I'm grateful for that. You were willing to look the other way while @beartype insanely stumbled around with its paws up in the air. Now, @beartype finally makes good on one or two of its historical promises.

To introduce our SUSAF feature list, @beartype presents... super unsexy anime ugly man Rock Lee!


Seriously. Those eyes. Those eyebrows. That bowl cut. Seriously.

All of these things and more now fully work as intended:

  • Complex forward references like "List[MuhClass[str]]". Party! ๐Ÿฅณ
  • PEP 563 via from __future__ import annotations. Never actually do this. ๐Ÿ•บ
  • Class properties via @classmethod + @property. Never actually do this, either. ๐Ÿ’ƒ
  • Nested typing.NewType() objects. Compose those new types together, because you can! ๐Ÿ˜‘
  • Truncated beartype.claw warnings. beartype.claw now tries to be quieter, but fails! ๐Ÿ˜ฎโ€๐Ÿ’จ
  • beartype.claw + python -m {your_package}.{your_module}. It just works! Maybe! ๐Ÿ˜ƒ
  • Immutable types like @beartype + enum.StrEnum. You know somebody wanted this! ๐Ÿณ

@beartype now supports basically everything. @beartype should no longer raise decoration-time exceptions about unsupported type hints, because all type hints should now be supported. Because we just used weasel words like "basically" and "should," you "basically" "should" not trust anything I just wrote. Instead, please throw beartype.claw.beartype_this_package() against the bug-stained windshield of your favourite codebase. Let us know if @beartype is still broke af for your use case.

Previously, everyone blacklisted @beartype from decorating one or more problematic classes, methods, or functions with the standard @no_type_check decorator. Now, you no longer need to that. Unleash the bear. Let @beartype claw its disturbingly hungry way through your disconcerting infestation of bugs bug-free codebase.

Taiko drum roll, please.


Above: @leycec in a troubled moment during the 0.16.0 release cycle.

GitHub Sponsors: You Make the Hard Work Less Work

But first... a huge thank you to @beartype's growing retinue of one-time and recurring sponsors! I'm incredibly touched in a good way by everyone's outpouring of hard-earned biosecurity tickets. You know who you are... but nobody else did. Until now.

@beartype sponsors, please line up to receive your complimentary sugar-free Bear Claw of Undulating Adulation & Unending Congratulations:

I also kinda feed bad. I do intend to deliver a high-quality QA product of blood, sweat, tears, memes, and fun โ€“ but I also frequently drop the ball and fall short of that lofty goal. Moreover, money is increasingly hard for many to find in 2023. The post-scarcity future that Ian Banks promised that I personally would live to see in depressing sci-fi books about Culture โ€“ the mythological utopian society that I will probably never live to see โ€“ has never seemed further away.

Thank you. So much.


The true form of @beartype is revealed โ€“ and also thanks you.

Up next... @beartype 0.16.0 did actually do something novel. Surprise! It's:

${BEARTYPE_IS_COLOR}: Make the Bad Color Go Away

@beartype 0.16.0 introduces our first official shell environment variable: ${BEARTYPE_IS_COLOR}. Through the power of ${BEARTYPE_IS_COLOR}, you too can now enforce a global colour policy by externally configuring our popular BeartypeConf.is_color option from the command line. As with is_color, ${BEARTYPE_IS_COLOR} is a tri-state boolean with three possible string values:

  • BEARTYPE_IS_COLOR='True', forcefully instantiating all beartype configurations across all Python processes with the is_color=True parameter.
  • BEARTYPE_IS_COLOR='False', forcefully instantiating all beartype configurations across all Python processes with the is_color=False parameter.
  • BEARTYPE_IS_COLOR='None', forcefully instantiating all beartype configurations across all Python processes with the is_color=None parameter.

Force beartype to obey your unthinking hatred of the colour spectrum. You canโ€™t be wrong!

BEARTYPE_IS_COLOR=False python3 -m monochrome_retro_app.its_srsly_cool


When @beartype makes your eyes bleed, call ${BEARTYPE_IS_COLOR}.

Forward References: This Day All Circular Imports Die

Previously, @beartype only supported simple forward references consisting of one or more "."-delimited Python identifiers like "MuhClass" and muh_package.muh_module.MuhClass. Now, @beartype finally supports complex forward references consisting of arbitrary combinations of references to classes that have yet to be defined both subscripted by and subscripting type hint factories.

In other words, forward references just work now. Bask in the ruthless complexity of @beartype now fully resolving a subscripted generic type alias forward reference (i.e., a stringified type hint referring to a global attribute that has yet to be defined whose value is a subscripted generic that also has yet to be defined). It hurts โ€“ but @beartype is here to help:

from beartype import beartype
from beartype.typing import Generic, List, TypeVar

@beartype
def this_is_fine(still_fine: 'WonkyTypeAlias') -> 'List[WonkyTypeAlias]':
    return [still_fine]

# Type variable. Once upon a time you dressed so fine, @beartype.
T = TypeVar('T')

# Generic. Threw the bums a dime in your prime, didn't you, @beartype?
class WonkyGeneric(Generic[T]): pass

# Type alias. People call say, "Beware, doll. You're bound to fall, @beartype."
WonkyTypeAlias = WonkyGeneric[int]

# Prints:
#     [<__main__.WonkyGeneric object at 0x7f27e21b1f50>]
print(this_is_fine(WonkyGeneric())

Like a rolling stone, @beartype now brings it all back home.

Sadly, none of this is free. There are now space and time costs associated with forward references that you should be aware of. For most codebases, these costs will be negligible and thus ignorable. For some codebases, however, ...especially codebases enabling PEP 563 via from __future__ import annotations, these costs could gradually become non-negligible and thus unignorable. Let's unpack what exactly is happening above.

  1. First, @beartype notices that your this_is_fine() function is annotated by two stringified type hints (i.e., type hints that are strings describing standard type hints rather than standard type hints).
  2. Next, @beartype de-stringifies these stringified type hints as follows. For each stringified type hint:
    1. First, @beartype attempts to replace each stringified attribute in that hint with a previously defined attribute of the same name in the current scope. Since the List attribute has been previously imported, @beartype replaces List in the return annotation with the previously imported beartype.typing.List attribute. Now:
      • The signature of this_is_fine() resembles def this_is_fine(still_fine: 'WonkyTypeAlias') -> List['WonkyTypeAlias']:. The only remaining stringified attributes refer to 'WonkyTypeAlias', which has yet to be defined.
    2. Next, @beartype replaces each remaining stringified attribute in that hint with a forward reference proxy (i.e., a type dynamically created by @beartype unique to that attribute). Forward reference proxies transparently delay the importation of the actual attributes they refer to until those attributes are actually needed โ€“ under the optimistic assumption that those attributes will be defined (and thus importable) at that point. Forward reference proxies are internal (and thus private) to @beartype. Since @beartype dynamically fabricates one forward reference proxy type for each unresolved stringified attribute in your codebase, forward reference proxies have strange classnames like _BeartypeForwardRefIndexable. (It's best not to think too hard about this. @leycec sure didn't.) Now:
      • The signature of this_is_fine() resembles def this_is_fine(still_fine: _BeartypeForwardRefIndexable('WonkyTypeAlias')) -> List[_BeartypeForwardRefIndexable('WonkyTypeAlias')]:. No stringified attributes remain. All previously stringified type hints have now been replaced by standard type hints. Well, sort of standard type hints. I mean, we're all just squinting at this point.
  3. Next, @beartype wraps your this_is_fine() function with a dynamically generated wrapper function performing runtime type-checking. B-b-but... how does @beartype even do that!?!? After all, neither the WoknyGeneric nor WonkyTypeAlias global have been defined yet. Ah-ha! Here is where the magic and the madness happens. Remember those forward reference proxies like _BeartypeForwardRefIndexable('WonkyTypeAlias') that @beartype previously injected into your type hints? Right. Those now do something. Each forward reference proxy now pretends that it is a normal class. From this point on, as far as @beartype and everything else in the Python ecosystem is concerned, forward reference proxies are just normal classes. You type-check them like you would any normal class (i.e., by passing them as the second argument to the isinstance() builtin). @beartype's code generation algorithm doesn't know any better. Ignorance is bliss when you are tired, it's Friday night, and you just want to play five hours more of Baldur's Gate 3 already.
  4. Next, you define both your WonkyGeneric and WonkyTypeAlias global attributes.
  5. Last, you call your this_is_fine() function. Magic and madness erupt. At last! Remember those forward reference proxies? They're baaaaaack. They finally do something. The type-checking code that @beartype previously wrapped this_is_fine() with kicks in by:
    1. Passing each forward reference proxy as the second argument to the isinstance() builtin, which then...
    2. Implicitly calls the subversive __instancecheck__() dunder method defined by the private metaclass _BeartypeForwardRefMeta of that forward reference proxy, which then...
    3. Explicitly calls the is_instance() method defined by that forward reference proxy, which then...
    4. Dynamically imports the global attribute WonkyTypeAlias from your module, which has now been defined.
    5. Detects that that attribute is actually a subscripted generic WonkyGeneric[int] rather than an actual type.
    6. Reduces that subscripted generic to the unsubscripted generic WonkyGeneric, which is an actual type.
    7. Internally caches that type inside that forward reference proxy (to optimize subsequent type-checks).
    8. Performs the desired type-check by passing that type as the second argument to the isinstance() builtin.
    9. B-b-but... what is even going on anymore? I myself hardly know.

"My head just exploded and I hurt all over. Please. Stop talking." โ€“ you, probably

This is an understandable response when @leycec starts talking. The laborious point I am trying and failing to make here is that forward references now incur costs. Ignoring the ridiculous explosion in code complexity, there are now unavoidable space and time costs here. Types are being cached all over. eval() is being called all over. Dynamic forward reference proxy classes are being instantiated all over.

@beartype optimizes this as much as feasible, because that's why we're here. Realistically, though? There's just soooo much magical dynamism here. @beartype can only safely optimize so much. You don't even want to know what @beartype does for fully-qualified forward references like 'muh_package.muh_module.MuhGeneric[T]'. Let us just say that it is Trash Day, your trash bins are now overflowing with stinky refuse, and Python's garbage collector is parked out front eviscerating the steaming mountain of short-lived objects in generation 0 that you have inflicted upon it.

Sometimes, you just need to forward reference. That's fine. @beartype is now here for you. Just try to keep it low-key. KK?

Oh โ€“ and here's what @leycec's idealistic and better-adjusted younger self had to say about this:

omg! OMG! OMGGGGG!!! @TeamSpen210: Your brilliant five-sentence abstract on the nature of Python vis-a-vis forward references actually worked! It actually worked, bro! Punch the air like you just don't care, 'cause that's what we're doin'.

do it like this

Naturally, your "simpler approach" was horrifying. My once-beautific bald face is crinkled with wrinkles, my twitchy eye is even more twitchy, and our cat won't stop pacing the room. I spent the entirety of my vacation doing this โ€“ and I don't even need this in production code. It just became a titanic struggle of @leycec versus @beartype versus Python: a three-way untelevised cage match that could only end with one man hurtling fifty feet onto another man lying prostrate on a table perched on top of another man curled in the fetal position while bleeding out live on GitHub.

mankind is sad
...dat the best u can do, python?

wat
wat is even happening here

uh gods
When your masterplan only ends in tragedy for everybody.

Seriously. I've got five hundred line comments devsplaining single one-liners. I've got 100KB of incomprehensible pure-Python isolated to four new submodules sprawled across a new subpackage devoted exclusively to inchoate madness. I've got abstract base classes, abstract metaclasses, dynamic type factories, dunder methods on sunder methods on abstract cached properties. I've got friggin' metaclass __getattr__() methods performing deferred module attribute importation via dynamic class attribute lookups of attributes that don't actually physically exist anywhere. I've got two whole dict.__missing__() implementations that don't even really have anything to do with one another. This is inchoate madness.

I don't even know how any of this works anymore. I've already jettisoned all knowledge pertaining to this topic so I can just sleep tonight without fitfully crying out in the night for my childhood stuffed octopus. I wish to awaken from the real-world nightmare I have unwittingly exposed myself to. It turns out Bloodborne is more a state of mind rather than a mere video game.

who knew
Python. It do be like that.

Forward references: do not go there, lest this animated reenactment of @leycec's life corrupt your once-pure essence.

I'm pretty sure (but not certain) that I may have inadvertently summoned the Eldritch She-God Shub-Niggurath, The Black Goat of the Woods with a Thousand Young, during the resolution of this feature request. I apologize, humanity. I'm so sorry! I didn't mean to open that portal. It just opened of its own accord while the guard cat was indolently passed out last night.

It happened. @leycec remembers.


Kumamoto Bear remembers, too.

PEP 563: You Still Shouldn't Do It. Now, You Can.

And then there was PEP 563 via from __future__ import annotations โ€“ the dread PEP whose name shall not be spake that we just spake. We fundamentally reimplemented our entire PEP 563 pipeline in terms of our forward resolution mechanism from Hell. (See: above.)

From @beartype's perspective, PEP 563 basically no longer exists. @beartype no longer particularly cares whether you explicitly stringify your type hint likes 'List[WonkyTypeAlias]' or implicitly stringify your type hints via from __future__ import annotations. The end result is the exact same: stringified type hints.

Do we all now understand why PEP 563 is bad? We do. But let us tiresomely enumerate the ways, anyway:

  • PEP 563 was supposed to increase efficiency.
  • But we have just demonstrated that resolving stringified type hints reduces efficiency โ€“ often, dramatically.
  • Therefore, we conclude that PEP 563 actually reduces efficiency โ€“ often, dramatically.

Here. We're gonna give it to you straight. If you enable PEP 563 across only a small number of modules, it's unlikely that you will see a measurable performance hit. If you enable PEP 563 across your entire friggin' codebase, however? Yeah. Expect those performance hits. Your apps will startup slower, run slower even after they spin up, and possibly even catastrophically fail in pernicious edge cases.

Don't be that guy. Don't enable PEP 563. Not even once.


Would you trust a man whose neck is also his chin? @beartype would.

Plum: It Just Works Better

@wesselb's magnum opus to @beartype-based multiple dispatch now works better. Notably:

  • Plum users may now safely enable PEP 563 via from __future__ import annotations. I mean, you shouldn't. But you can. Maybe. But... yeah. I still wouldn't.
  • Multiple dispatch across PEP 586-compliant typing.Literal[...] type hints now works considerably better. @PhilipVinc: you know who you are.


Plum: even the poor little doggo agrees

Shoutouts to the Beasts in Back

Greets to @empyrealapp, @PhilipVinc, @rlkelly, @KyleKing, @RomainBrault, @tolomea, @skeggse, @alexander-c-b, @gabrieldemarmiesse, @machow. You know who you are. You are... awesome!


i luv u

It was SUSAF. It was... @beartype 0.16.0.

Don't miss a new beartype release

NewReleases is sending notifications on new releases.