NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #333 most downloaded on PyPI
Unbearably fast near-real-time pure-Python runtime-static type-checker.
Last release 9 days ago
26 Sep 2026
Ships unpredictably
gaps range from 8 days to 8 months
Nearly every release is documented
notes for 50 of 54 stable releases
3 versions withdrawn
withdrawn after publishing
6 years old
67 releases · first in 2020
One column per quarter.
Nothing published for this version
Nothing published for this version
> Follow breathtaking new @beartype announcements at [@beartype's official Bluesky account](https://leycec.bsky.social)! I don't post often. I code of
Follow breathtaking new @beartype announcements at @beartype's official Bluesky account! I don't post often. I code often instead. Your feed will remain unpolluted and safe from cat memes... for now. I suddenly want to post lots of cat memes to Bluesky. Why is that? :thinking:
Come live chat with all the @beartype homies at @beartype's official Zulip chat! Don't know how to type hint something? Unsure why @beartype is barfing all over logs? Heckle us in front of a live audience until we collapse or your team finally gets a straight answer. One of these two outcomes is likelier than the other. Zulip: it's like Discord... only less cool. A lot less cool. The UI and UX is frankly incomprehensible. At least Zulip is permissively Apache-licensed GitHub-hosted open-source. I'm not selling this whole Zulip thing, am I? :smiling_face_with_tear:
Come gape, gawk, and gander at @beartype's new official Zensical documentation:
beartype.github.io/beartype! The content may look exactly like @beartype's old unmaintained Sphinx documentation that we don't talk about anymore, but... uh, that's only because looks can be deceiving? Oh, alright. They're exactly the same. That will soon change. That had better soon change. Sphinx makes us "BARF!" like Larry in Rivercity Ransom. Zensical, on the other hand, is the delectable modern successor to Mkdocs. You do not want to know what happened to Mkdocs. Seriously. Don't go down that hole. Thanks so much to brilliant @beartype bro @posita for crushing this one out with his buddy "Claudio". :fist_right: :fist_left:
@beartype now routinely hits 4 million downloads a day. Apparently, that means something. Arbitrary numbers that get bigger matter to small petty bald men like me. It's like real-life suddenly became a hit JRPG – and I am so here for that energy. :joy:
@beartype's GitHub Sponsors have collapsed into a black hole reading: "Nobody likes you, bro." @beartype basically doesn't have any sponsors anymore is what I'm saying. I blame no one but myself. I do the coding thing. I don't do the marketing thing. The results are unsurprising. Pity the Bear by feeding the Bear a dollar for a day. He may not learn to fish like that, but at least he'll be able to buy some fish for the cats. :face_holding_back_tears:
@beartype
0.23.0rc0resolves so many mission-critical issues that everyone really wants to require @beartype0.23.0rc0as a minimum version in theirpyproject.tomlfiles. Save your app stack today before our last stable @beartype0.22.9release tears it down tomorrow. Just:
dependencies = [
...
"beartype >= 0.23.0rc", # <-- pretend @beartype 0.22.9 never existed. that's what we do.
...
]
Protect your calls to
beartype_this_package()against malicious third-party import hooks. Call our new publicbeartype.claw.warn_if_beartype_claw_inactive()function to receive an informative non-fatal warning whenever somebody else stifles yourbeartype.clawimport hooks. If you'd like to dig even deeper into the contentious topic of import hooks that hate each other, consider @Glinte's third-partymetapathologypackage for all your investigatoryimportlibneeds. You tell 'em, @Glinte! Just:
from beartype.claw import beartype_this_package, warn_if_beartype_claw_inactive
beartype_this_package()
warn_if_beartype_claw_inactive() # <-- if the prior line did nothing, at least now you know :(
Type-check NumPy, JAX, PyTorch, and CuPy tensor constraints with @acecchini's third-party
bearshapepackage! It's likejaxtyping, only @beartype-native from the ground-up. Likejaxtyping,bearshaperuntime type-checks tensors across a wide variety of frameworks. Unlikejaxtyping,bearshapecleanly integrates with @beartype.bearshapedoes not stomp all overbeartype.claw-based type-checking. You go,bearshape! Just:
from beartype import beartype
from bearshape import N, C
from bearshape.numpy import F32
@beartype
def normalize(x: F32[N, C]) -> F32[N, C]: # <-- i'm nodding like i understand this
return x / x.sum(axis=1, keepdims=True)
Why are there so many crazy prefatory notes for this release, anyway? Everybody's already exhausted. This changelog hasn't even started yet, has it? :face_exhaling:
<-- your face probably
@beartype 0.23.0rc0 thunders down the road of good QA intentions in a desperate bid to crush all the skittering bugs eating your codebase to the bone. Under its spike-studded wheels of doom stability, @beartype 0.23.0rc0 exposes the fragile arrogance at the heart of trying to do anything crush bugs in Python:
# Via "pip", the once-great venerable master packager now fallen on hard times:
$ pip install --upgrade beartype # <-- you go, pipe-smoking pip
# Via "uv", the plucky upstart spiky-haired kid wielding a sword larger than its body:
$ uv pip install --upgrade beartype # <-- when you no longer have the strength to care about lock files
# Via "conda", the corporate scientist in casual lab attire who wants to be your buddy, pal:
$ conda config --add channels conda-forge # <-- where did python packaging go wrong?
$ conda install conda-forge::beartype # <-- it all seemed so simple, once.
<sup>Right: @beartype 0.23.0rc0. Left: Your codebase. Middle: Your CTO stares dumbfounded.</sup>
@beartype 0.23.0rc0 is helping @leycec and his beautiful science wife to eat food. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom everyone in the @leycec family would currently be eating grasshoppers in the abandoned back lot again:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Thanks so much, masters of fintech and lifted tides.
<sup>The Masters of Fintech and Lifted Tides. That's who.</sup>
@beartype 0.23.0rc0 had to get out tonight. Python was at stake. QA, which is always quietly on fire, would've caught on even more fire. Therefore, this changelog sucks even more than it would've on a good Friday. It's a tiny fragment of what @beartype 0.23.0rc0 is capable of. But that's fine. Because, ultimately, what @beartype 0.23.0rc0 is capable of is not destroying your codebase.
Sometimes, not destroying your codebase is all that matters. :+1:
<sup>@beartype 0.23.0rc0: when two ugly pancakes with arms collide, a QA baby is born</sup>
warn_if_beartype_claw_inactive(): You Seriously Wanna Call This FunctionPython's burgeoning import hook ecosystem has basically broken down. It never worked all that well to begin with. Now, it increasingly doesn't work at all. First, the bad news, bears. The tl;dr is this:
beartype.claw import hooks are silently ignored whenever a PyInstaller, jaxtyping, or typeguard import hook is used anywhere in the current Python process.Yeah. We know. This isn't just rough. This is abrasively cataclysmic. Let's talk about why this is happening.
Currently, beartype.claw import hooks may be silently overridden and thus disabled under various common conditions. When this occurs, @beartype will silently fail to apply beartype.claw-based runtime type-checking to affected third-party packages and modules. This means your packages and modules, because you are reading this and now frowning. Python app stacks affected by this include any that either:
beartype.claw. Not much we can do about that. Please complain to PyInstaller itself. Openly weep like this to make them feel bad: :sob:jaxtyping. If you currently use jaxtyping import hooks, consider switching to @acecchini's third-party bearshape package. bearshape cleanly integrates with @beartype, supports basically everything jaxtyping supports, and even comes with a more Pythonic syntax that looks suspiciously (but welcomely) like standard typing type hint syntax. :clap: :wave: :clap:typeguard. If you currently also use typeguard import hooks, I have no good advice at the moment. "Please seriously don't do that," is the best I can offer at the moment. @beartype 0.24.0 will fix these incompatibilities once and for all. Until then, @beartype weeps for Python's broken import hook ecosystem. :crying_cat_face:That's the bad news. This is happening because Python lacks standards governing interoperability between competing third-party import hooks. No new standards that could remedy this situation appear to be forthcoming in the near future, either. No one cares that Python's import hook ecosystem doesn't work at the moment. Kinda sad.
The good news is that @beartype can resolve most of this situation entirely on its own. A future version of @beartype will do just that. Probably @beartype 0.24.0 in a few months, because this ongoing contention between import hooks is awful. It has to stop. Thus, we are forcing it to stop. Our way. The @beartype way. :muscle: :bear:
The bad bad news is that @beartype isn't doing that at the moment. Until @beartype resolves import hook contention on its side, consider manually calling our new public beartype.claw.warn_if_beartype_claw_inactive() function after calling any beartype.claw import hook (e.g., beartype_this_package()). If somebody else stifled that call without your consent, you'll receive an informative non-fatal warning that could destroy your will to continue coding:
from beartype.claw import beartype_this_package, warn_if_beartype_claw_inactive
beartype_this_package()
warn_if_beartype_claw_inactive() # <-- if the prior line did nothing, at least now you know :(
See also this feature request for a horrifyingly detailed description of a new pseudostandard I just made up to force cooperation between competing third-party import hooks. When you want to feel deranged, you want to read that issue. :laughing: <sup><-- not that funny honestly</sup>
If you'd like to dig even deeper into the contentious topic of import hooks that hate each other, consider @Glinte's third-party metapathology package for all your investigatory importlib needs. You tell 'em, @Glinte! :fist_raised:
<sup>when your right arm refuses to put itself down, you know you should've upgraded to @beartype 0.23.0rc0</sup>
BeartypeConf(is_random=False): Never Do This and No One Gets HurtSo. You want to disable @beartype's default pseudorandom type-checking behaviour, huh? You think you want many things in life – until you finally get what you asked for. Then the monkey's pawl curls. Your codebase dies quietly. You shriek to an uncaring city skyline shortly before the burning helicopter hurtles to the ground:
"Why? Why is this happening!?"
@beartype's default pseudorandom type-checking behaviour is a good thing. You forgot that essential lesson in life. Pseudorandom type-checking dramatically increases the likelihood of catching invalid items in pure-Python containers (e.g., dict, list) before they have a chance to hurtle burning helicopters to the ground.
Sometimes, though, that default behaviour is not a good thing. There is a valid reason for disabling pseudorandom type-checking: test consistency. Want tests to fail consistently, uniformly, and deterministically rather than dissolving into a non-deterministic crapshoot of incoherent fragility? It happens. We understand. Then your tests want to explicitly disable the new BeartypeConf(is_random=False) configuration option as follows:
# In some pytest-driven unit test somewhere that currently hates @beartype...
def test_beartype_haters_gonna_hate() -> None:
from beartype import beartype, BeartypeConf
# Test-specific @beartype decorator performing deterministic type-checking! Ugh.
boring_beartype = beartype(conf=BeartypeConf(is_random=False))
@boring_beartype
def beartype_randomness(can_suck_an_egg: list[str]) -> str:
return can_suck_an_egg[0]
# This assertion is now guaranteed to *ALWAYS* succeed, despite the fact that
# this call semantically violates the type hint annotating the "can_suck_an_egg"
# parameter accepted by the beartype_randomness() function defined above. But
# you don't care anymore, do you? You just want this to pass. Now, it can.
#
# The only cost is sanity. Because nothing means anything anymore. Enjoy.
kinda_awful_honestly = beartype_randomness([
'A string, yo.', b'...totally not a string', 0xFEEDBABE])
assert kinda_awful_honestly == 'A string, yo.'
Sure. It's awful. Sometimes, that's exactly what you need.
BeartypeConf(is_random=False): when you need awful, you need @beartype.
<sup>@beartype 0.23.0rc0: "your single new configuration option fails to impress me"</sup>
OHNOES. It's 2:13AM. An ominous time, if ever there was. I've gotta wrap this thing up quickly while convincing you that I intensely care about this concluding section.
Here's the deal. Forward references? You know, those type hints that are strings referring to an external type or type hint defined somewhere else that you can't import into the current module for specious reasons that are better left undocumented? Those forward references?
...yeah. Prior versions of @beartype only pretended to support forward references. @beartype's previous support for forward references was so slipshod, unreliable, and context-dependent that @beartype basically didn't support forward references. Not really, anyway.
That's why I spent the last six months <sup>why is life like this</sup> completely rewriting @beartype's internal forward reference resolution engine from the ground up. Common uses cases that now reliably work include:
Forward references to arbitrary type hints defined elsewhere. Previously, @beartype only supported forward references to classes. Now, @beartype's supports forward references to literally anything. If it's a valid type hint, you can now refer to it: e.g.,
# In submodule "i_pity.da_foo":
WorstTypeHintEver = 'list[int]' # <-- look i don't know it's late
# In submodule "i_pity.da_bar":
from beartype import beartype
@beartype
def pitiable_func(grovelling_arg: 'i_pity.da_foo.WorstTypeHintEver') -> int:
return grovelling_arg[0]
pitiable_func([0xBABEFACE]) # <-- uh wut
Relative forward references in die_if_unbearable() and is_bearable() calls. Previously, both the die_if_unbearable() and is_bearable() functions prohibited relative forward references (i.e., forward references omitting a module name and thus relative to the current module). Now, die_if_unbearable() and is_bearable() dynamically hunt up the call stack for the calling module calling those functions and intelligently resolve relative forward references against that calling module: e.g.,
from beartype.door import die_if_unbearable
def its_so_late(my_face_is_gonna_fall_off: object) -> None:
die_if_unbearable(
my_face_is_gonna_fall_off,
'SomeClassThatHasntBeenDefinedYet' # <-- relative forward reference! so cool
)
class SomeClassThatHasntBeenDefinedYet: ...
# This totally works now. Yay! What a lame example, though. Can we all agree this
# example was lame? We can. I don't mind someone rubbing that the truth into my soul.
its_so_late(SomeClassThatHasntBeenDefinedYet())
Heaps of other forward reference stuff, too. It's late. We're done.
Peace out, wonderful bear family! We'll excrete a heap more text that actually means something for the official stable @beartype 0.23.0 mic drop. Until then, have a wonderful remainder of your summer – and may the QA be with you always. :heart_hands: :smiling_face_with_three_hearts: :revolving_hearts:
<sup>Robot Chicken says: "OMG @beartype 0.23.0rc0! PEACE OUT Y'ALL! I'M A ROBOT CHICKEN!!!!"</sup>
Almost forgot. @beartype 0.23.0rc0 also officially supports Python 3.15. That's right, y'all. As of @beartype 0.23.0rc0, Robot Chicken ain't the only one who can use the word "y'all" anymore, y'all.
Python 3.15: "You want the JIT speed it bringin'."
<sup>@beartype 0.23.0rc0: when it cross its arms, you just know all bugs is dead</sup>
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. :musical_note: :musical_keyboard: :notes:
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@posita. @Glinte. @wesselb. @jorenham. @JWCS. @adamtheturtle. @tusharsadhwani. @TeamSpen210. @bitranox. @ilyapoz. @benoit74. @hmgaudecker. @nstarman. @Gattocrucco. @javi-linx. @tseaver. @kdeldycke. @gotmax23. @black-snow. @DetachHead. @asitstands. @jeertmans. @abhinand-c. @mzealey. @ITcarrot. @danielkovtun. @ddorian. @riesentoaster. @sean-roelofs-ai. Heroes among us.
<sup>@beartype 0.23.X dev cycle: still accursed after all these cat memes.</sup>
@beartype 0.22.9 celebrates one million @beartype downloads a day! We wanted to rent out a loft warehouse space, flip a dry ice machine off Ebay for p
@beartype 0.22.9 celebrates one million @beartype downloads a day! We wanted to rent out a loft warehouse space, flip a dry ice machine off Ebay for pennies, shadow-drop DJ Lorien Testard, invite all our GitHub homies, and get down and funky with this QA quinceañera. It didn't happen. I released instead. The memories we might have made are only another patch release in the 0.22.X dev cycle. Bear friends, it's your time to shine tonight:
# Via "pip", the once-great venerable master packager now fallen on hard times:
$ pip install --upgrade beartype # <-- you go, pipe-smoking pip
# Via "uv", the plucky upstart spiky-haired kid wielding a sword larger than its body:
$ uv pip install --upgrade beartype # <-- when you no longer have the strength to care about lock files
# Via "conda", the corporate scientist in casual lab attire who wants to be your buddy, pal:
$ conda config --add channels conda-forge # <-- where did python packaging go wrong?
$ conda install conda-forge::beartype # <-- it all seemed so simple, once.
@beartype 0.22.9: "Wave your keyboards in the air like those bugs don't care."
<sup>one million downloads a day says you can't stop this party</sup>
@beartype 0.22.9 is helping @leycec and his beautiful science wife to eat food. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom everyone in the @leycec family would currently be eating grasshoppers in the abandoned back lot again:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Thanks so much, masters of fintech and lifted tides.
<sup>The Masters of Fintech and Lifted Tides. That's who.</sup>
@beartype 0.22.9 brings official support for... wait. PyInstaller? Hasn't @beartype supported PyInstaller since the beginning? Yeah. We thought so too. Then Meta was all like: "Oh no you don't, @beartype." :joy:
@beartype has officially supported competing products like Nuitka for years. Now PyInstaller joins the ranks. But it's not just PyInstaller. @beartype 0.22.9 should also resolve any similar pending issues with other app bundling frameworks like... uhh, Briefcase? No idea, honestly. That's untested wishful thinking. The very best kind.
@beartype 0.22.9: When you realize a small part of you still cares about desktop apps.
<sup>Your bundled app moments after being packaged with @beartype. It's... not good.</sup>
ty: It's So Fast We Barely Support It@beartype 0.22.9 also brings unofficial and mostly just superficial support for Astral's ty, the newest member of the Astral family and Python's latest static type-checking darling. Thanks so much to long-standing bear warrior @Glinte for his masterful compatibility work here.
@beartype should no longer fall down on its face when confronted with ty. Should. Why does that word feel like it's doing so much heavy lifting here!? Since @beartype already officially supports both mypy and pyright, extending that with official support for ty as well is... unlikely. We're out of bandwidth. The gas tank is empty. Red warning lights emit a blinding glare. My leather-tanned face is one giant frown wrinkle. That said...
@beartype doesn't intend to do ty dirty. We might not like that there's yet another static type-checker in an all-too crowded field of indistinguishable lookalikes all yelling and shouting at the top of their lungs. Okay. We definitely don't like that. But we also can't do anything about that. If you like ty, @beartype will do its best to like ty. If you hit any snags at the intersection of ty and @beartype, just drop us a line. We'll try not to complain. Sure. We will anyway. We all know that. But at least we've made a promise we can't possibly keep.
@beartype 0.22.9: It promises things.
<sup>When your static type-checker sees @beartype for the first time.</sup>
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. :musical_note: :musical_keyboard: :notes:
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@zbowling. @adamtheturtle. @Glinte. @JWCS. @bitranox. Heroes among us.
<sup>@beartype 0.22.X dev cycle: it may be accursed, but at least it's cute.</sup>
@beartype 0.22.9 celebrates one million @beartype downloads a day! We wanted to rent out a loft warehouse space, flip a dry ice machine off Ebay for pennies, shadow-drop DJ Lorien Testard, invite all our GitHub homies, and get down and funky with this QA quinceañera. It didn't happen. I released instead. The memories we might have made are only another patch release in the 0.22.X dev cycle. Bear friends, it's your time to shine tonight:
# Via "pip", the once-great venerable master packager now fallen on hard times:
$ pip install --upgrade beartype # <-- you go, pipe-smoking pip
# Via "uv", the plucky upstart spiky-haired kid wielding a sword larger than its body:
$ uv pip install --upgrade beartype # <-- when you no longer have the strength to care about lock files
# Via "conda", the corporate scientist in casual lab attire who wants to be your buddy, pal:
$ conda config --add channels conda-forge # <-- where did python packaging go wrong?
$ conda install conda-forge::beartype # <-- it all seemed so simple, once.@beartype 0.22.9: "Wave your keyboards in the air like those bugs don't care."
one million downloads a day says you can't stop this party
@beartype 0.22.9 is helping @leycec and his beautiful science wife to eat food. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom everyone in the @leycec family would currently be eating grasshoppers in the abandoned back lot again:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Thanks so much, masters of fintech and lifted tides.
The Masters of Fintech and Lifted Tides. That's who.
@beartype 0.22.9 brings official support for... wait. PyInstaller? Hasn't @beartype supported PyInstaller since the beginning? Yeah. We thought so too. Then Meta was all like: "Oh no you don't, @beartype." 😂
@beartype has officially supported competing products like Nuitka for years. Now PyInstaller joins the ranks. But it's not just PyInstaller. @beartype 0.22.9 should also resolve any similar pending issues with other app bundling frameworks like... uhh, Briefcase? No idea, honestly. That's untested wishful thinking. The very best kind.
@beartype 0.22.9: When you realize a small part of you still cares about desktop apps.
Your bundled app moments after being packaged with @beartype. It's... not good.
ty: It's So Fast We Barely Support It@beartype 0.22.9 also brings unofficial and mostly just superficial support for Astral's ty, the newest member of the Astral family and Python's latest static type-checking darling. Thanks so much to long-standing bear warrior @Glinte for his masterful compatibility work here.
@beartype should no longer fall down on its face when confronted with ty. Should. Why does that word feel like it's doing so much heavy lifting here!? Since @beartype already officially supports both mypy and pyright, extending that with official support for ty as well is... unlikely. We're out of bandwidth. The gas tank is empty. Red warning lights emit a blinding glare. My leather-tanned face is one giant frown wrinkle. That said...
@beartype doesn't intend to do ty dirty. We might not like that there's yet another static type-checker in an all-too crowded field of indistinguishable lookalikes all yelling and shouting at the top of their lungs. Okay. We definitely don't like that. But we also can't do anything about that. If you like ty, @beartype will do its best to like ty. If you hit any snags at the intersection of ty and @beartype, just drop us a line. We'll try not to complain. Sure. We will anyway. We all know that. But at least we've made a promise we can't possibly keep.
@beartype 0.22.9: It promises things.
When your static type-checker sees @beartype for the first time.
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. 🎵 🎹 🎶
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@zbowling. @adamtheturtle. @Glinte. @JWCS. @bitranox. Heroes among us.
@beartype 0.22.X dev cycle: it may be accursed, but at least it's cute.
@beartype 0.22.8. It's happening. Yet, it shouldn't be happening. This is the corrupt timeline we live on:
@beartype 0.22.8. It's happening. Yet, it shouldn't be happening. This is the corrupt timeline we live on:
# Via "pip", the once-great venerable master packager now fallen on hard times:
$ pip install --upgrade beartype # <-- you go, pipe-smoking pip
# Via "uv", the plucky upstart spiky-haired kid wielding a sword larger than its body:
$ uv pip install --upgrade beartype # <-- when you no longer have the strength to care about lock files
# Via "conda", the corporate scientist in casual lab attire who wants to be your buddy, pal:
$ conda config --add channels conda-forge # <-- where did python packaging go wrong?
$ conda install conda-forge::beartype # <-- it all seemed so simple, once.
@beartype 0.22.6 broke synchronous generator returns. @beartype 0.22.8 is unbreaking what its younger sibling brazenly broke. The recklessness of youth, huh? Generator returns, huh? Fandango cares about synchronous generator returns. Surely, they can't be the only ones!? Seems like they're the only ones. Is there anything weirder than retrieving a return value by stripping the value instance variable out of a StopIteration exception caught while manually iterating a synchronous generator one iteration past its last valid yield? No? Just me? Who designs APIs like that, anyway? I can just imagine the internal discussion thread speccing this cruft out:
"So. Uhh. Your API returns values by bolting them onto the side of builtin exceptions implicitly raised when your iterator is exhausted, huh? You can't even access those return values if you iterate over your iterator with
forloops, huh? You've got to manually iterate your iterator withnext()calls, huh? Even though that's infeasible in the general case, huh? Sounds good to me. It's in."
@beartype 0.22.8 had better be the last patch release of the 0.22.X dev cycle. Overlord Ainz Ooal Gown, we summon you to complete the dark ritual! End this endless cycle of bug death and rebirth... once and for all.
<sup>@beartype 0.22.8: you know that feeling when a skeletal demon lord raises his eldritch staff of oblivion in triumphant hubris? yeah. this is like that.</sup>
@beartype 0.22.8 is helping @leycec and his beautiful science wife to eat food. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom everyone in the @leycec family would currently be eating grasshoppers in the abandoned back lot again:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Thanks so much, masters of fintech and lifted tides.
<sup>The Masters of Fintech and Lifted Tides. That's who.</sup>
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. :musical_note: :musical_keyboard: :notes:
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@henryhchchc. @riesentoaster. @adamtheturtle. @pablovela5620. @crypdick. @Glinte. @JWCS. @bitranox. Heroes among us.
<sup>@beartype 0.22.X dev cycle: bye now tnx so much for coming. no, reallys. @leycec means it this time. reallys!</sup>
@beartype 0.22.8. It's happening. Yet, it shouldn't be happening. This is the corrupt timeline we live on:
# Via "pip", the once-great venerable master packager now fallen on hard times:
$ pip install --upgrade beartype # <-- you go, pipe-smoking pip
# Via "uv", the plucky upstart spiky-haired kid wielding a sword larger than its body:
$ uv pip install --upgrade beartype # <-- when you no longer have the strength to care about lock files
# Via "conda", the corporate scientist in casual lab attire who wants to be your buddy, pal:
$ conda config --add channels conda-forge # <-- where did python packaging go wrong?
$ conda install conda-forge::beartype # <-- it all seemed so simple, once.@beartype 0.22.6 broke synchronous generator returns. @beartype 0.22.8 is unbreaking what its younger sibling brazenly broke. The recklessness of youth, huh? Generator returns, huh? Fandango cares about synchronous generator returns. Surely, they can't be the only ones!? Seems like they're the only ones. Is there anything weirder than retrieving a return value by stripping the value instance variable out of a StopIteration exception caught while manually iterating a synchronous generator one iteration past its last valid yield? No? Just me? Who designs APIs like that, anyway? I can just imagine the internal discussion thread speccing this cruft out:
"So. Uhh. Your API returns values by bolting them onto the side of builtin exceptions implicitly raised when your iterator is exhausted, huh? You can't even access those return values if you iterate over your iterator with
forloops, huh? You've got to manually iterate your iterator withnext()calls, huh? Even though that's infeasible in the general case, huh? Sounds good to me. It's in."
@beartype 0.22.8 had better be the last patch release of the 0.22.X dev cycle. Overlord Ainz Ooal Gown, we summon you to complete the dark ritual! End this endless cycle of bug death and rebirth... once and for all.
@beartype 0.22.8: you know that feeling when a skeletal demon lord raises his eldritch staff of oblivion in triumphant hubris? yeah. this is like that.
@beartype 0.22.8 is helping @leycec and his beautiful science wife to eat food. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom everyone in the @leycec family would currently be eating grasshoppers in the abandoned back lot again:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Thanks so much, masters of fintech and lifted tides.
The Masters of Fintech and Lifted Tides. That's who.
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. 🎵 🎹 🎶
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@henryhchchc. @riesentoaster. @adamtheturtle. @pablovela5620. @crypdick. @Glinte. @JWCS. @bitranox. Heroes among us.
@beartype 0.22.X dev cycle: bye now tnx so much for coming. no, reallys. @leycec means it this time. reallys!
@beartype 0.22.7 descends like an owl with sorta unsettling black eyes devoid of pupils, majestic wings aloft on the winds of QA. *Do not be alarmed.*
@beartype 0.22.7 descends like an owl with sorta unsettling black eyes devoid of pupils, majestic wings aloft on the winds of QA. Do not be alarmed. Actually, isn't that when you should be most alarmed? When somebody says, "Do not be alarmed"!? The italicization isn't helping, either. But this will:
# Via "pip", the once-great venerable master packager now fallen on hard times:
$ pip install --upgrade beartype # <-- you go, pipe-smoking pip
# Via "uv", the plucky upstart spiky-haired kid wielding a sword larger than its body:
$ uv sync beartype # <-- @Glinte said it, so it must be true
# Via "conda", the corporate scientist in casual lab attire who wants to be your buddy, pal:
$ conda config --add channels conda-forge # <-- where did python packaging go wrong?
$ conda install conda-forge::beartype # <-- it all seemed so simple, once.
@beartype 0.22.7 is the last patch release of the 0.22.X dev cycle. That was a lie. Fortunately, seductive lies aren't a problem here at @beartype. If we want to hear it, it can't be bad. This release brings big:
0.22.7. :spider_web: + :bear: = :safety_pin:async yield from expression at future-async-yield-from was hugely inspiring. If you're into mutating Python into misshapen forms through the cosmic horrors of forbidden AST transformations, <sup>...who isn't? am i right? i'm right</sup> check out @rbroderi's work. He's up to no good, which is the best kind. :hugs: :people_hugging:That's it. That's all we've got. But you don't even want to know the things we did, the places we went, the behaviour we regret, to make Gradio + @beartype happen. If Gradio means nothing to you, we've got nothing but memes for you.
@beartype should now support all popular Python frameworks. If you (or a codebase you love) know of any Python packages, modules, APIs, or other services that @beartype does not support, please drop us a line on the issue tracker. We'll drop what we're doing <sup>video games. it's always video games.</sup> and "immediately" resolve that for your heroic team.
Until then, video games await. Let's get those memes started!
<sup>the more you stare at this, the more your brain sees</sup>
@beartype 0.22.7 is helping @leycec and his beautiful science wife to eat food. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom everyone in the @leycec family would currently be eating grasshoppers in the abandoned back lot again:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Thanks so much, masters of fintech and lifted tides.
<sup>The Masters of Fintech and Lifted Tides. That's who.</sup>
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. :musical_note: :musical_keyboard: :notes:
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@adamtheturtle. @pablovela5620. @crypdick. @Glinte. @JWCS. @bitranox. Heroes among us.
<sup>@beartype 0.22.X dev cycle: bye now tnx so much for coming</sup>
@beartype 0.22.7 descends like an owl with sorta unsettling black eyes devoid of pupils, majestic wings aloft on the winds of QA. Do not be alarmed. Actually, isn't that when you should be most alarmed? When somebody says, "Do not be alarmed"!? The italicization isn't helping, either. But this will:
# Via "pip", the once-great venerable master packager now fallen on hard times:
$ pip install --upgrade beartype # <-- you go, pipe-smoking pip
# Via "uv", the plucky upstart spiky-haired kid wielding a sword larger than its body:
$ uv sync beartype # <-- @Glinte said it, so it must be true
# Via "conda", the corporate scientist in casual lab attire who wants to be your buddy, pal:
$ conda config --add channels conda-forge # <-- where did python packaging go wrong?
$ conda install conda-forge::beartype # <-- it all seemed so simple, once.@beartype 0.22.7 is the last patch release of the 0.22.X dev cycle. That was a lie. Fortunately, seductive lies aren't a problem here at @beartype. If we want to hear it, it can't be bad. This release brings big:
0.22.7. 🕸️ + 🐻 = 🧷async yield from expression at future-async-yield-from was hugely inspiring. If you're into mutating Python into misshapen forms through the cosmic horrors of forbidden AST transformations, ...who isn't? am i right? i'm right check out @rbroderi's work. He's up to no good, which is the best kind. 🤗 🫂That's it. That's all we've got. But you don't even want to know the things we did, the places we went, the behaviour we regret, to make Gradio + @beartype happen. If Gradio means nothing to you, we've got nothing but memes for you.
@beartype should now support all popular Python frameworks. If you (or a codebase you love) know of any Python packages, modules, APIs, or other services that @beartype does not support, please drop us a line on the issue tracker. We'll drop what we're doing video games. it's always video games. and "immediately" resolve that for your heroic team.
Until then, video games await. Let's get those memes started!
the more you stare at this, the more your brain sees
@beartype 0.22.7 is helping @leycec and his beautiful science wife to eat food. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom everyone in the @leycec family would currently be eating grasshoppers in the abandoned back lot again:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Thanks so much, masters of fintech and lifted tides.
The Masters of Fintech and Lifted Tides. That's who.
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. 🎵 🎹 🎶
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@adamtheturtle. @pablovela5620. @crypdick. @Glinte. @JWCS. @bitranox. Heroes among us.
@beartype 0.22.X dev cycle: bye now tnx so much for coming
@beartype 0.22.6 sidles up to your door at 11:20PM with a steaming hot-out-of-the-oven delivery of... *wait.* This isn't pizza. Where's the pizza, del
@beartype 0.22.6 sidles up to your door at 11:20PM with a steaming hot-out-of-the-oven delivery of... wait. This isn't pizza. Where's the pizza, delivery bear? Delivery bear growls, "There is no pizza." You feel uneasy. Maybe asking delivery bear awkward questions as it loiters outside your door under the pale moonlight isn't a choice you'd make again. You accept the misshapen bundle cradled gently in its paws. You know what it is without even looking. It's all so eerily familiar:
# Via "pip", the once-great venerable master packager now fallen on hard times:
$ pip install --upgrade beartype # <-- you go, pipe-smoking pip
# Via "uv", the plucky upstart spiky-haired kid wielding a sword larger than its body:
$ uv lock --upgrade-package beartype # <-- you do what you need to do, ultraviolet radiation
# Via "conda", the corporate scientist in casual lab attire who wants to be your buddy, pal:
$ conda config --add channels conda-forge # <-- where did python packaging go wrong?
$ conda install conda-forge::beartype # <-- it all seemed so simple, once.
@beartype 0.22.6 is the patch release you can trust. Finally. <sup>caveats may apply. <sup>caveats may destroy your app stack mere minutes after upgrading.</sup></sup>
<sup>Your based Gradio-based ML-LLM-AI web app at https://moneybags.ai mere minutes after upgrading.</sup>
@beartype 0.22.5 is helping @leycec and his beautiful science wife to eat food. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom everyone in the @leycec family would currently be eating grasshoppers in the abandoned back lot again:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Thanks so much, masters of fintech and lifted tides.
<sup>The Masters of Fintech and Lifted Tides. That's who.</sup>
@beartype 0.22.6 delivers almost full-blown Gradio compatibility. If the adverb "almost" wasn't doing so much heavy lifting in the prior sentence, we would've punctuated that sentence with an exclamation point. Do not be alarmed by the lack of an exclamation point. @beartype 0.22.6 doesn't destroy your app stack. At 11:20PM, it's the small things.
@beartype 0.22.6 preserves inspect.isgeneratorfunction()-ness across @beartype-decorated synchronous generator functions. Gradio needs this, apparently. Previously, the standard inspect.isgeneratorfunction() returned False when passed @beartype-decorated generators. Now, inspect.isgeneratorfunction() returns True. Pretend you understand what this means even as you nod off in the La-Z-Bear-Boy in the foyer that smells faintly of shaving cream:
# Been there. Done that. Bought the bear shirt.
>>> from beartype import beartype
>>> from collections.abc import Iterable
>>> from inspect import isgeneratorfunction
### Synchronous generator! Excitement intensifies! WHY AM I SHOUTING!?!?!?!?!?!?!?!?
>>> @beartype
... def congenial_generator(parboiled_parameter: str | None) -> Iterable[str | None]:
... yield parboiled_parameter
# Prove that @beartype 0.22.6 actually did something.
>>> isgeneratorfunction(congenial_generator)
True # <-- that's it? that's all you've got?
@beartype 0.22.6 does not, unfortunately, preserve inspect.isasyncgenfunction()-ness across @beartype-decorated asynchronous generator functions. Gradio needs that, too. But everyone who's anyone says that's impossible. When you ask the impossible of @beartype, do not be surprised when @beartype fillets a salmon on your desk instead.
And... that's all we've got. It ain't much. It ain't how the QA was won. But that'll do, bear. That'll do.
<sup>@beartype 0.22.6: people smile when you shoot arrows through your hat</sup>
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. :musical_note: :musical_keyboard: :notes:
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@adamtheturtle. @pablovela5620. @crypdick. @Glinte. @JWCS. @bitranox. Heroes among us.
<sup>@beartype and Gradio. You can feel the smug.</sup>
@beartype 0.22.6 sidles up to your door at 11:20PM with a steaming hot-out-of-the-oven delivery of... wait. This isn't pizza. Where's the pizza, delivery bear? Delivery bear growls, "There is no pizza." You feel uneasy. Maybe asking delivery bear awkward questions as it loiters outside your door under the pale moonlight isn't a choice you'd make again. You accept the misshapen bundle cradled gently in its paws. You know what it is without even looking. It's all so eerily familiar:
# Via "pip", the once-great venerable master packager now fallen on hard times:
$ pip install --upgrade beartype # <-- you go, pipe-smoking pip
# Via "uv", the plucky upstart spiky-haired kid wielding a sword larger than its body:
$ uv lock --upgrade-package beartype # <-- you do what you need to do, ultraviolet radiation
# Via "conda", the corporate scientist in casual lab attire who wants to be your buddy, pal:
$ conda config --add channels conda-forge # <-- where did python packaging go wrong?
$ conda install conda-forge::beartype # <-- it all seemed so simple, once.@beartype 0.22.6 is the patch release you can trust. Finally. caveats may apply. caveats may destroy your app stack mere minutes after upgrading.
Your based Gradio-based ML-LLM-AI web app at https://moneybags.ai mere minutes after upgrading.
@beartype 0.22.5 is helping @leycec and his beautiful science wife to eat food. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom everyone in the @leycec family would currently be eating grasshoppers in the abandoned back lot again:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Thanks so much, masters of fintech and lifted tides.
The Masters of Fintech and Lifted Tides. That's who.
@beartype 0.22.6 delivers almost full-blown Gradio compatibility. If the adverb "almost" wasn't doing so much heavy lifting in the prior sentence, we would've punctuated that sentence with an exclamation point. Do not be alarmed by the lack of an exclamation point. @beartype 0.22.6 doesn't destroy your app stack. At 11:20PM, it's the small things.
@beartype 0.22.6 preserves inspect.isgeneratorfunction()-ness across @beartype-decorated synchronous generator functions. Gradio needs this, apparently. Previously, the standard inspect.isgeneratorfunction() returned False when passed @beartype-decorated generators. Now, inspect.isgeneratorfunction() returns True. Pretend you understand what this means even as you nod off in the La-Z-Bear-Boy in the foyer that smells faintly of shaving cream:
# Been there. Done that. Bought the bear shirt.
>>> from beartype import beartype
>>> from collections.abc import Iterable
>>> from inspect import isgeneratorfunction
### Synchronous generator! Excitement intensifies! WHY AM I SHOUTING!?!?!?!?!?!?!?!?
>>> @beartype
... def congenial_generator(parboiled_parameter: str | None) -> Iterable[str | None]:
... yield parboiled_parameter
# Prove that @beartype 0.22.6 actually did something.
>>> isgeneratorfunction(congenial_generator)
True # <-- that's it? that's all you've got?@beartype 0.22.6 does not, unfortunately, preserve inspect.isasyncgenfunction()-ness across @beartype-decorated asynchronous generator functions. Gradio needs that, too. But everyone who's anyone says that's impossible. When you ask the impossible of @beartype, do not be surprised when @beartype fillets a salmon on your desk instead.
And... that's all we've got. It ain't much. It ain't how the QA was won. But that'll do, bear. That'll do.
@beartype 0.22.6: people smile when you shoot arrows through your hat
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. 🎵 🎹 🎶
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@adamtheturtle. @pablovela5620. @crypdick. @Glinte. @JWCS. @bitranox. Heroes among us.
@beartype and Gradio. You can feel the smug.
@beartype 0.22.5 does stuff. *Uhhh.* Wait. Why are we releasing yet another @beartype 0.22.x patch within the span of ten seconds? We were just here.
@beartype 0.22.5 does stuff. Uhhh. Wait. Why are we releasing yet another @beartype 0.22.x patch within the span of ten seconds? We were just here. We already released @beartype 0.22.4 a week ago. Wasn't that good enough!? I... I guess not. Turns out ${PYTHONOPTIMIZE} support has been busted in @beartype for literally years. Probably decades. Nobody cared – until somebody cared. I blame only myself despite wanting to point the finger at somebody else. Anybody else. I'll take anybody. Let's start over.
@beartype 0.22.5 valiantly arises from the ashes of our issue tracker like a burning phoenix on fire!!!! You can't stop this:
# Via "pip", the once-great venerable master packager now fallen on hard times:
$ pip install --upgrade beartype # <-- you go, pipe-smoking pip
# Via "uv", the plucky upstart spiky-haired kid wielding a sword larger than its body:
$ uv lock --upgrade-package beartype # <-- you do what you need to do, ultraviolet radiation
@beartype 0.22.5 stoically puts on the sunglasses so you don't have to.
<sup>The sky is blue and I have hair again.</sup>
@beartype 0.22.5 is helping @leycec and his beautiful science wife to eat food. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom everyone in the @leycec family would currently be eating grasshoppers in the abandoned back lot again:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Thanks so much, masters of fintech and lifted tides.
<sup>The Masters of Fintech and Lifted Tides. That's who.</sup>
@beartype 0.22.5 is a patch release that guarantees compatibility with Python optimization. Both the @beartype decorator and beartype.claw import hooks now silently reduce to noops (i.e., do nothing rather than type-checking everything) when users:
-O options to the Python interpreter (e.g., python -O worldshattering_app_shatters_world_accidentally.py).${PYTHONOPTIMIZE} environment variable to a positive integer (e.g., PYTHONOPTIMIZE=1 python worldrepairing_app_repairs_world_shattered_by_worldshattering_app.py).FastMCP users really care about Python optimization. Apparently, nobody else does. Makes sense. Even @beartype is too slow for those speed-obsessed LLM gurus. How can this be!? It's never enough for the AI. In the relentless drive for faster query turnaround times, FastMCP is plumbing the depths of the impossible. There are always casualties on the road to progress. @beartype was one of those casualties. But no more.
@beartype 0.22.5 is a burning phoenix on fire. It's not a metaphor anymore. We're pretty sure our issue tracker is on fire. What is it now? Still 98 open issues despite a flurry of recent issue resolutions that took our last will to code? Yup. 98 open issues. My... my gods. GitHub. Does no one sleep around here? :face_exhaling:
@beartype + PYTHONOPTIMIZE=1: because you're too tired to even run @beartype anymore.
<sup>Speedboat out of nowhere: @beartype. Well-meaning innocent bystanders: FastMCP users.</sup>
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. :musical_note: :musical_keyboard: :notes:
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@strawgate. @Glinte. @bitranox. Heroes among us.
<sup>@beartype and FastMCP. It feels deep.</sup>
@beartype 0.22.4 catastrophically explodes all over your monitor. An oily black residue redolent of snail mucus slides off the screen, dripping with a
@beartype 0.22.4 catastrophically explodes all over your monitor. An oily black residue redolent of snail mucus slides off the screen, dripping with a maddening cadence into the crevices of your trusty mechanical keyboard:
# Via "pip", the once-great venerable master packager now fallen on hard times:
$ pip install --upgrade beartype # <-- you go, pipe-smoking pip
# Via "uv", the plucky upstart spiky-haired kid wielding a sword larger than its body:
$ uv lock --upgrade-package beartype # <-- you do what you need to do, ultraviolet radiation
@beartype 0.22.4 never gets tired of bug-eyed dudes punching squinty-eyed dudes. Childhood memories do not fade.
<sup>Left: @beartype 0.22.4. Right: Poetry and pipenv together as one dude.</sup>
@beartype 0.22.4 is helping @leycec and his beautiful science wife to eat food. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom everyone in the @leycec family would currently be eating grasshoppers in the abandoned back lot again:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Thanks so much, masters of fintech and lifted tides.
<sup>The Masters of Fintech and Lifted Tides. That's who.</sup>
@beartype 0.22.4 is a patch release that guarantees compatibility with both Poetry and pipenv. Previously, @beartype just assumed that Poetry and pipenv liked @beartype. What's not to like about @beartype, guys? Huh!? Apparently...
Everything. @beartype 0.22.3 broke the assumption that everybody likes @beartype. Our prior release shipped a pyproject.toml file with a PEP 440-compliant version string:
requires-python = ">=3.10,!=3.14rc1,!=3.14rc2"
That syntax is valid. But Poetry and pipenv didn't care. They do what they want! And they didn't want to have anything to do with @beartype 0.22.3. We disagree, but that's fair enough. Everyone has bad opinions.
@beartype 0.22.4 resolves these trivial incompatibilities with popular devtooling. @beartype 0.22.4 also promises this will never happen again. A new integration test in the @beartype test suite guarantees Poetry and pipenv compatibility, safeguarding both your QA stack and sanity against midnight regressions at 4:52AM.
<sup>@beartype 0.22.4: what is even happening here</sup>
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. :musical_note: :musical_keyboard: :notes:
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@nizdolin, @SaiAakash, @markspace, @Spikhalskiy! Because you care, @beartype cares.
<sup>@beartype 0.22.4: it's more painful than it looks, and it looks pretty painful</sup>
@beartype 0.22.3 descends softly. A crinkled QA leaf drifts somnabulently. Autumn gusts on GitHub. Rain hails on PyPI. These are the haiku of our Pyth
@beartype 0.22.3 descends softly. A crinkled QA leaf drifts somnabulently. Autumn gusts on GitHub. Rain hails on PyPI. These are the haiku of our Pythonic lives:
# Via "pip", the once-great venerable master packager now fallen on hard times:
$ pip install --upgrade beartype # <-- you go, old man pip
# Via "uv", the plucky upstart spiky-haired kid wielding a sword larger than its body:
$ uv lock --upgrade-package # <-- you are cleared for takeoff
That's right. uv installation instructions. It's happening, people.
<sup>@beartype 0.22.3 dances the happy dance as your codebase watches in horror</sup>
@beartype 0.22.3 is helping @leycec and his beautiful science wife to eat food. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom everyone in the @leycec family would currently be eating grasshoppers in the abandoned back lot again:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Thanks so much, masters of fintech and lifted tides.
<sup>The Masters of Fintech and Lifted Tides. That's who.</sup>
@beartype 0.22.3 is a patch release with a big heart, but not a lot to say. It means well, but it doesn't do much. But what it does do, it does so well:
redis-py Python client! @beartype now officially supports Redis in general and the redis.Redis class specifically. Hype it! :godmode:0.22.3 drops Python 3.9 support a week ahead of its official End-of-Life (EoL). I generally prefer to abandon Python compatibility with minor releases (e.g., 0.23.0) rather than patch releases like this. But... I gotta make an exception for Bad Boy Python 3.9. I'm sorry, everyone. It got axed. :goberserk:
<sup>@beartype 0.22.3: it's funny until Helmet Bear eats your code</sup>
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. :musical_note: :musical_keyboard: :notes:
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
<sup>@beartype 0.22.3 at 2:01AM in the morning</sup>
@beartype users who discover security vulnerabilities are invited to privately disclose those vulnerabilities by submitting a GitHub-managed security…
@beartype is proud as a cub gnawing its first salmon to announce: @beartype has been Tidelifted! For our security-conscious corporate and government userbase, the best way to secure your enterprise and support @beartype is now through Tidelift vis-a-vis a SonarQube Advanced Security subscription. More on that later. We now return to your regularly scheduled release party. DJ Leycec in residence. Hit those fat QA jams.
Beartype 0.22.0 0.22.1 0.22.2 portals into the mortal plenum with a disturbing "WHOOOMP!" As you panic, all the oxygen in the room is rapidly vacuumed into an adjacent hyperdimension. It's not @beartype's safest entrance – but it's one we're all sure to remember. This is @beartype 0.22.2: <sup>don't ask what happened to 0.22.0. just... don't.</sup>
pip install --upgrade --pre beartype # beartype casts magic missile on the darkness
The central dogma of @beartype 0.22.2 is LLM compatibility. Do you like LLM? Do you like compatibility? Then your code likes @beartype 0.22.2 (even against your better judgement). But before the liking starts...
<sup>@beartype 0.22.2 salutes you who are about to code</sup>
@leycec and his beautiful science wife are eating well. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom @leycec's cats would currently be eating grasshoppers in the abandoned back lot again:
Additional financial shout-outs to @ilyapoz (@Ilia Pozhilov), the amazing former Yandex code cosmonaut who graciously donated a pile of Georgian lari to @beartype this go-around. Apparently, the lari is denominated in the ლ Unicode character. What a symbol! It looks like a beautiful hat. If only the Canadian dollar was half as manly. :sob:
Thanks so much, masters of fintech and Yandex.
<sup>The Masters of Fintech and Yandex. That's who.</sup>
This release also comes courtesy Tidelift, which very graciously pays out recurring income to security-sensitive open-source projects like @beartype, NumPy, and other stuff you probably care about. @beartype joining Tidelift has super-positive implications for Python's broader QA community – including:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
<sup>Pictured: Artistic rendition of the @beartype development process before Tidelift. You weren't supposed to see this.</sup>
You really want to bump the @beartype requirement in your pyproject.toml file. Preserve end user sanity tomorrow by explicitly requiring a minimum of @beartype >=0.22.2 today: e.g.,
# In your top-level "pyproject.toml" configuration:
dependencies = [
...
"beartype >=0.22.2", # <-- once you go 0.22.2, you go 0.22.2 for life
...
]
Why? Bear with us. This explanation may bore you. Imagine what typing this out felt like.
Prior versions of @beartype are fundamentally incompatible with Python 3.14. Python packagers like pip and uv do not update packages by default. Unless you bump your @beartype requirement, your existing userbase who already installed your package will be unable to use your package under Python 3.14 – even after manually updating your package, because manually updating your package fails to transitively update all dependencies of your package by default. In other words, Python packaging still kinda sucks.
This is why...
<sup>@beartype looks stoically into the wind as everything breaks</sup>
beartype.claw import hooks have been supremely revamped. They still work the same, but they're now explicitly compatible with a lot more than they used to be. If you tried enabling beartype_this_package() but switched back to manually decorating everything with @beartype because beartype_this_package() spewed too many errors everywhere, please give beartype_this_package() a second chance:
# In the "{your_package}.__init__" submodule:
from beartype.claw import beartype_this_package
beartype_this_package() # <-- we're willing to swear on our pinkies that this works now
Let us know how it goes. If stuff is still busted, we'll immediately fix that stuff. We now have the necessary machinery in our abstract syntax tree (AST) transformer to support all (or most, anyway) compatibility woes previously associated with beartype.claw import hooks.
<sup>the new beartype.claw shamelessly dances for anyone</sup>
@beartype 0.22.2 dramatically improves compatibility across the board with the APIs, packages, and paradigms you care about. Because you care, we care. In fact, we spent whole months caring. We cared so much we're all cared out. The summer went away in a blitz of caring and we didn't even notice. Oh, Gods. The warmth has fled. The snow is coming. And all we have to show for it is massive compatibility gains across the board.
This includes:
<sup>@beartype 0.22.2 weeps from pride and accomplishment, but mostly sleep deprivation</sup>
Python 3.14. You care about Python 3.14 even if you don't know you care. @beartype now fully supports PEP 649 and 749 – landmark QA standards introduced by Python 3.14 that finally obsolete PEP 563. All these numbers mean something. We swear. Since all horror stories start with two nondescript teens in a Buick, let's start there:
from __future__ import annotations
That's PEP 563. And... that's now deprecated. PEP 749 officially deprecated PEP 563 a few months ago:
Sometime after the last release that did not support PEP 649 semantics (expected to be 3.13) reaches its end-of-life,
from __future__ import annotationsis deprecated. Compiling any code that uses the future import will emit a DeprecationWarning. This will happen no sooner than the first release after Python 3.13 reaches its end-of-life, but the community may decide to wait longer. After at least two releases, the future import is removed, and annotations are always evaluated as per PEP 649. Code that continues to use the future import will raise aSyntaxError, similar to any other undefined future import.
tl;dr: On October ~15th 2029, from __future__ import annotations will be officially deprecated. On October ~15th 2031, from __future__ import annotations will be removed entirely from the Python language. At that time, any module using from __future__ import annotations will raise a SyntaxError at importation time and thus become unimportable. In 2025, nobody should enable from __future__ import annotations voluntarily.
Sometime over the next several five years, you and your righteous dev team will require Python ≥ 3.14 as a mandatory dependency. That's just the way of the Python world. Planned obsolesce is the road we walk. When that happens, you'll no longer need from __future__ import annotations to declare forward references to undefined types in type hints like this:
# Look, Ma! No "from __future__ import annotations".
# We don't need the future where we're going.
from beartype import beartype
# Under Python ≥ 3.14, this just works. You annotated a function as accepting
# an instance of a type you haven't even declared yet. Yet, this is now fine.
# @beartype accepts you and your suspicious code for you who are.
@beartype
def useless_func(forward_reference_to_undefined_type: ThisJustWorksNow) -> ThisJustWorksNow:
return forward_reference_to_undefined_type
# Of course, you *DO* have to eventually define the undefined type used above.
# If you don't, everything will still blow up. It's not @beartype's fault.
# Python 3.14 made us do it... We blame Guido.
class ThisJustWorksNow(object):
pass
Unquoted forward references are thus baked into Python 3.14. Order of type hints and types is no longer significant. Define and annotate stuff in any order you like.
@beartype 0.22.2: because life is too short and code is too long.
<sup>@beartype: always ready to rip its shirt off at the slightest provocation</sup>
@beartype 0.22.2 now ships with out-of-the-box support for Bad Boy LLM APIs that defy community standards, mental health, and your last hair follicles. This includes:
These APIs are awesome, because they changed the world. But they're also hostile to literally every other Python decorator in existence. @beartype is a Python decorator in existence. Thus, these APIs are hostile to @beartype.
Why? Because they all define decorator-hostile decorators: that is, decorators that prevent other decorators from being applied. That's not how decorators are supposed to work. You're supposed to be able to apply decorators in any arbitrary order. That was the deal, LLM APIs! Apparently, LLM APIs hated that deal. Their decorators destructively transform your normal functions and methods (which are decoratable by @beartype) into abnormal instances of API-specific types (which are not decoratable by @beartype).
In the worst case of both LangChain and FastMCP, these abnormal instances of API-specific types aren't even callable! They don't just destroy the types of what they decorate. They destroy the callability of what they decorate. What kind of compatibility-hostile API turns a function or method into an uncallable object that you can't do anything with anymore? LangChain and FastMCP. That's who.
Examples include:
@langchain_core.runnables.chain decorator function.@fastmcp.FastMCP.tool decorator method.@celery.Celery.task decorator method.@beartype 0.22.2 now officially supports decorator-hostile decorators like this. It cost us our summer, but no price is too high. Okay. The price was pretty high. But this is the AI-ML-LLM hype train we're talking about. You either board that train or you prepare to eat everybody else's dust as they point fingers and cruelly guffaw into their monocles at you. @beartype prefers to board that train.
@beartype walks its own road, though. @beartype boards that train in its own special way. Specifically, @beartype now maintains two internal databases efficiently structured as trie prefix search trees:
beartype.claw import hooks then:
@beartype decorator before (rather than after) the last decorator-hostile decorator in existing chains of two or more consecutive decorators decorating your callables and types in your modules.@beartype decorator now silently ignores attempts to decorate instances of these problematic types. Don't even bother! They're busted, @beartype. At least we no longer are. :sweat_smile:It's... complicated. Really, really complicated. There's a doctoral PhD thesis somewhere here for somebody who wants one. Me? I just wanna sleep. Blessed sleep! Take me now!
<sup>left: @beartype. right: langchain, fastmcp, and celery. you think we forgive you that easily!?</sup>
Polars is the vibrant Pandas alternative everyone loves. 35k GitHub stars cannot be wrong. Although I feel jealousy when I hear numbers like that, @beartype now officially supports validation of Polars DataFrames vis-a-vis Pandera – the only sane way to validate the integrity of your fragile DataFrames that are probably falling over as we speak. Poor guys.
Previously, @beartype only supported validation of Pandas DataFrames via Pandera. Now, @beartype supports both Pandas and Polars. This can only mean one thing. Our powers are growing at an exponential rate. The QA Singularity is upon us. Check out this terrifying exhibition of runtime validation run amok:
import pandera.polars as papol
import beartype
import pandera.typing.polars as pt
import polars as pl
class Schema(papol.DataFrameModel):
state: str
city: str
price: int = papol.Field(in_range={"min_value": 5, "max_value": 20})
@papol.check_types
@beartype.beartype
def test_schema(df: pl.DataFrame) -> pt.DataFrame[Schema]:
return df
df = pl.DataFrame({
"state": ["Ohio", "Ohio", "Ohio", "Nevada", "Nevada"],
"city": ["Youngstown", "Akron", "Columbus", "Reno", "Las Vegas"],
"price": [10, 20, 15, 14, 4]
# ^-- *VIOLATION*. so bad. so very bad. we're panicking. game over, man!
})
test_schema(df)
...which helpfully blows up with a human-readable exception that relieves you:
Traceback (most recent call last):
File "/home/leycec/tmp/mopy.py", line 24, in <module>
test_schema(df)
~~~~~~~~~~~^^^^
File "/home/leycec/py/pyenv/versions/3.13.2/lib/python3.13/site-packages/pandera/decorators.py", line 841, in _wrapper
return _check_arg("return", out)
File "/home/leycec/py/pyenv/versions/3.13.2/lib/python3.13/site-packages/pandera/decorators.py", line 722, in _check_arg
raise error_handler.schema_errors[0]
pandera.errors.SchemaError: error in check_types decorator of function 'test_schema':
Column 'price' failed validator number 0: <Check in_range: in_range(5, 20)> failure case
examples: [{'price': 4}]
@beartype: Bleeding-edge APIs named after magnetic dipoles? Yeah. We do that, too.
<sup>@beartype 0.22.2 pours one out for the DataFrame that didn't make it.</sup>
@beartype 0.22.2 fully supports PEP 646 – Variadic Generics. <sup>...for certain glib definitions of "fully supports."</sup>
PEP 646 is outrageously huge. It's the longest and cruelest typing standard published to date. Synopsizing that standard is a fool's errand. Thus, we now synopsize that standard. PEP 646 brings two things to the QA table. Let's go! :hurtrealbad:
<sup>I-I-Is that @beartype's arm she's holding up!? :open_mouth: </sup>
PEP 646 introduces type variable tuples (i.e., typing.TypeVarTuple(...) objects). Whereas good ol' PEP 484-compliant type variables (i.e., typing.TypeVar(...) objects) match only a single type (like MuhGeneric[int] for class MuhGeneric[T](): ...), type variable tuples greedily match zero or more types (like WoahGeneric[int, str, bool] for class WoahGeneric[*Ts](): ...).
The use case is tensors, supposedly. I personally press "F" to doubt that any real-world tensor frameworks will actually leverage type variable tuples. Doing so would inhibit forward compatibility with future tensor APIs in those frameworks. Why? Because type variable tuples match types greedily. Once you've exposed a type variable tuple to your end users through a public-facing generic type, you can never add any other type variables to that generic type without breaking backward compatibility. You're now permanently locked in to that generic type API for the rest of all time.
Aren't ordinary type variables like that too, though? No. You can always add additional type variables to normal generic types without breaking backward compatibility. How? PEP 696-compliant type variable defaults (e.g., T = TypeVar("T", default=int)). Press "F" if anyone would like me to babble incoherently more about all this.
That said, type variable tuples probably do have practical internal use. I'd never expose them to end users for the aforementioned reasons. They cause compatibility woes. Still, @beartype 0.22.2 now fully supports them. Use them! Reassure us that we didn't waste the entire summer on this! Oh, Gods... the summer... it's... it's gone, everybody. :weary:
<sup>@beartype 0.22.2 when it realized where the summer went</sup>
PEP 646 also introduces tuple type hint unpacking (e.g., tuple[int, *tuple[str, ...]]). This spec goes really hard really fast. The syntax is also kinda nasty. But the core idea is that you can now deeply type-check the contents of non-trivial tuple data structures. You may now be thinking:
"Uhh... wat? Tuple data structures? Can you even use tuples like data structures? Even if you can, should you? Wouldn't a tuple data structure promote unreadable and obfuscated code that not even the unpaid intern wants to maintain?"
Indeed! It's all true! You can use tuples like data structures. @beartype internally does this all the time. Why? Unjustifiable microoptimizations. Because CPython is highly internally optimized for tuple instantiation, access, and garbage-collection, tuples make the ideal read-only data structures for many perfidious purposes. CPython cares a lot about tuples. Tuples are the backbone of CPython's calling convention, because functions and methods returning multiple values implicitly return tuples of those values. This makes tuples the ideal data structures for implementing many pure-Python algorithms – especially recursive algorithms implemented iteratively.
Of course, tuple data structures do promote unreadable and obfuscated code that not even the unpaid intern wants to maintain. But that's not a problem if you're a dodgy codebase like @beartype. Ain't nobody maintaining this code except @leycec – a well-known glutton for punishment-by-dev-hell.
If you are like @leycec, you too can now type-check your shamefully growing heap of tuple data structures from Hell. How? PEP 646, yo. Previously, PEP 484 only let you type-check two simplistic kinds of tuple hints:
tuple[str, int], for example, is the fixed tuple hint matching tuples containing one string item followed by an integer item.tuple[str, ...], for example, is the variadic tuple hint matching tuples containing zero or more string items.That's great if your tuple data structures are simplistic. But they're probably not, are they? That's why you're still reading this at 4:17AM. Your wife is calling out for you in her fitful sleep, but you don't even care! This is riveting Internet reading right here!
So what if your tuple data structures are as complicated as your marriage? You reach for PEP 646 and @beartype 0.22.2. You can now unpack at most one child tuple hint inside another parent tuple hint. For reasons that will become clear shortly, most child tuple hints unpacked in this way are variadic. Do that and you've now defined a mixed fixed-variadic tuple hint that matches tuples containing (in order):
tuple[str, bytes, *tuple[int, ...], bool, float], for example, is the mixed fixed-variadic tuple hint matching tuples containing (in order) a string item, a byte string item, zero or more integer items, a boolean item, and a floating-point number item. Cool, right? But the coolness doesn't stop there. Consider cardinality. Old-school variadic tuple hints (like tuple[str, ...]) only match tuples containing zero or more items. But what if you want to match a positive number of items? Tuple hint unpacking trivially lets you do that, too.
tuple[str, *tuple[str, ...]], for example, is the mixed fixed-variadic tuple hint matching tuples containing one or more strings. This example then generalizes to arbitrary cardinality bound only by your patience and openness to syntactic barbarism. Want to match tuples containing a larger number of strings? Just keep prepending str child type hints until you're satisfied, exhausted, and dripping with sweat at 4:17AM.
tuple[str, str, str, str, str, *tuple[str, ...]], for example, is the mixed fixed-variadic tuple hint matching tuples containing five or more strings. It's silly. Yet, it works. Kinda.
@beartype 0.22.2: it's only silly if you admit it's silly.
<sup>beartype 0.22.2 proudly unpacks tuple hints as the wind blows its hair around</sup>
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. :musical_note: :musical_keyboard: :notes:
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@posita, @wesselb, @tusharsadhwani, @JWCS, @patrick-kidger, @EtaoinWu, @iamrecursion, @Moosems, @langfield, @sylvorg, @Glinte, @cclauss, @sean-roelofs-ai, @kultura-luke, @jonathanberthias, @gotmax23, @adamtheturtle, @ilyapoz, @MilesCranmer, @rg936672, @ddorian, @k4ml, @riesentoaster, @LeonHilf, @jeertmans, @mzealey, @thetianshuhuang, @RomainBrault, @alisaifee, @ArneBachmannDLR, @JelleZijlstra, @tactile-metrology, @RobPasMue, @GithubCamouflaged, @kloczek, @uriyasama, @danielgafni, @JWCS, @rbroderi, @AlanCoding, @tvdboom, @crypdick, @jvesely, @komodovaran, @kaparoo, @MaximilienLC, @fleimgruber, @alexoshin, @gabrieldemarmiesse, @James4Ever0, @NLPShenanigans, @rtbs-dev, @yurivict, @st--, @murphyk, @dosisod, @Rogdham, @alisaifee, @denisrosset, @damarro3, @ruancomelli, @jondequinor, @harshita-gupta, @jakebailey, @denballakh, @jaanli, @creatorrr, @msvensson222, @avolchek, @femtomc, @AdrienPensart, @jakelongo, @Artur-Galstyan, @ArneBachmann, @danielward27, @WeepingClown13, @rbnhd, @radomirgr, @rwiegan, @brettc, @spagdoon0411, @helderco, @paulwouters, @jamesbraza, @dcharatan, @kasium, @AdrienPensart, @sunildkumar, @peske, @mentalisttraceur, @awf, @PhilipVinc, @dcharatan, @empyrealapp, @rlkelly, @KyleKing, @skeggse, @RomainBrault, @deepyaman, @minmax, @jedie, @pablovela5620, @thiswillbeyourgithub, @Logan-Pageler, @knyazer, @ilyapoz, @yuzhichang, @Fedezzab, @antonioan, @im-Kitsch, @mthramann, @fbartolic, @rgallardone, @frrad, @jonnyhyman, @jennydaman, @likewei92, @acec2127, @rudimichal, @woutdenolf, @PauloHMTeixeira.
<sup>left: @beartype. right: you. why is it always like this?</sup>
@beartype users who discover security vulnerabilities are invited to privately disclose those vulnerabilities by submitting a GitHub-managed security…
@beartype is proud as a cub gnawing its first salmon to announce: @beartype has been Tidelifted! For our security-conscious corporate and government userbase, the best way to secure your enterprise and support @beartype is now through Tidelift vis-a-vis a SonarQube Advanced Security subscription. More on that later. We now return to your regularly scheduled release party. DJ Leycec in residence. Hit those fat QA jams.
Beartype 0.22.0 0.22.1 portals into the mortal plenum with a disturbing "WHOOOMP!" As you panic, all the oxygen in the room is rapidly vacuumed into an adjacent hyperdimension. It's not @beartype's safest entrance – but it's one we're all sure to remember. This is @beartype 0.22.1: don't ask what happened to 0.22.0. just... don't.
pip install --upgrade --pre beartype # beartype casts magic missile on the darknessThe central dogma of @beartype 0.22.1 is LLM compatibility. Do you like LLM? Do you like compatibility? Then your code likes @beartype 0.22.1 (even against your better judgement). But before the liking starts...
@beartype 0.22.1 salutes you who are about to code
@leycec and his beautiful science wife are eating well. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom @leycec's cats would currently be eating grasshoppers in the abandoned back lot again:
Additional financial shout-outs to @ilyapoz (@Ilia Pozhilov), the amazing former Yandex code cosmonaut who graciously donated a pile of Georgian lari to @beartype this go-around. Apparently, the lari is denominated in the ლ Unicode character. What a symbol! It looks like a beautiful hat. If only the Canadian dollar was half as manly. 😭
Thanks so much, masters of fintech and Yandex.
The Masters of Fintech and Yandex. That's who.
This release also comes courtesy Tidelift, which very graciously pays out recurring income to security-sensitive open-source projects like @beartype, NumPy, and other stuff you probably care about. @beartype joining Tidelift has super-positive implications for Python's broader QA community – including:
If you represent a security-conscious corporate, government, or non-profit, the best way bar none for you to support @beartype and secure your own workflow is by subscribing to Tidelift through SonarQube Advanced Security. Security giant Sonar recently acquired Tidelift, guaranteeing the economic viability of the Tidelift model for billions of future open-source projects that have yet to be born. Join the jargon-laden conversation and pay someone else to think about unreadable acronyms like SAST, SCA, and SBOM for once.
Pictured: Artistic rendition of the @beartype development process before Tidelift. You weren't supposed to see this.
You really want to bump the @beartype requirement in your pyproject.toml file. Preserve end user sanity tomorrow by explicitly requiring a minimum of @beartype >=0.22.1 today: e.g.,
# In your top-level "pyproject.toml" configuration:
dependencies = [
...
"beartype >=0.22.1", # <-- once you go 0.22.1, you go 0.22.1 for life
...
]Why? Bear with us. This explanation may bore you. Imagine what typing this out felt like.
Prior versions of @beartype are fundamentally incompatible with Python 3.14. Python packagers like pip and uv do not update packages by default. Unless you bump your @beartype requirement, your existing userbase who already installed your package will be unable to use your package under Python 3.14 – even after manually updating your package, because manually updating your package fails to transitively update all dependencies of your package by default. In other words, Python packaging still kinda sucks.
This is why...
@beartype looks stoically into the wind as everything breaks
beartype.claw import hooks have been supremely revamped. They still work the same, but they're now explicitly compatible with a lot more than they used to be. If you tried enabling beartype_this_package() but switched back to manually decorating everything with @beartype because beartype_this_package() spewed too many errors everywhere, please give beartype_this_package() a second chance:
# In the "{your_package}.__init__" submodule:
from beartype.claw import beartype_this_package
beartype_this_package() # <-- we're willing to swear on our pinkies that this works nowLet us know how it goes. If stuff is still busted, we'll immediately fix that stuff. We now have the necessary machinery in our abstract syntax tree (AST) transformer to support all (or most, anyway) compatibility woes previously associated with beartype.claw import hooks.
the new beartype.claw shamelessly dances for anyone
@beartype 0.22.1 dramatically improves compatibility across the board with the APIs, packages, and paradigms you care about. Because you care, we care. In fact, we spent whole months caring. We cared so much we're all cared out. The summer went away in a blitz of caring and we didn't even notice. Oh, Gods. The warmth has fled. The snow is coming. And all we have to show for it is massive compatibility gains across the board.
This includes:
@beartype 0.22.1 weeps from pride and accomplishment, but mostly sleep deprivation
Python 3.14. You care about Python 3.14 even if you don't know you care. @beartype now fully supports PEP 649 and 749 – landmark QA standards introduced by Python 3.14 that finally obsolete PEP 563. All these numbers mean something. We swear. Since all horror stories start with two nondescript teens in a Buick, let's start there:
from __future__ import annotationsThat's PEP 563. And... that's now deprecated. PEP 749 officially deprecated PEP 563 a few months ago:
Sometime after the last release that did not support PEP 649 semantics (expected to be 3.13) reaches its end-of-life,
from __future__ import annotationsis deprecated. Compiling any code that uses the future import will emit a DeprecationWarning. This will happen no sooner than the first release after Python 3.13 reaches its end-of-life, but the community may decide to wait longer.
After at least two releases, the future import is removed, and annotations are always evaluated as per PEP 649. Code that continues to use the future import will raise aSyntaxError, similar to any other undefined future import.
tl;dr: On October ~15th 2029, from __future__ import annotations will be officially deprecated. On October ~15th 2031, from __future__ import annotations will be removed entirely from the Python language. At that time, any module using from __future__ import annotations will raise a SyntaxError at importation time and thus become unimportable. In 2025, nobody should enable from __future__ import annotations voluntarily.
Sometime over the next several five years, you and your righteous dev team will require Python ≥ 3.14 as a mandatory dependency. That's just the way of the Python world. Planned obsolesce is the road we walk. When that happens, you'll no longer need from __future__ import annotations to declare forward references to undefined types in type hints like this:
# Look, Ma! No "from __future__ import annotations".
# We don't need the future where we're going.
from beartype import beartype
# Under Python ≥ 3.14, this just works. You annotated a function as accepting
# an instance of a type you haven't even declared yet. Yet, this is now fine.
# @beartype accepts you and your suspicious code for you who are.
@beartype
def useless_func(forward_reference_to_undefined_type: ThisJustWorksNow) -> ThisJustWorksNow:
return forward_reference_to_undefined_type
# Of course, you *DO* have to eventually define the undefined type used above.
# If you don't, everything will still blow up. It's not @beartype's fault.
# Python 3.14 made us do it... We blame Guido.
class ThisJustWorksNow(object):
passUnquoted forward references are thus baked into Python 3.14. Order of type hints and types is no longer significant. Define and annotate stuff in any order you like.
@beartype 0.22.1: because life is too short and code is too long.
@beartype: always ready to rip its shirt off at the slightest provocation
@beartype 0.22.1 now ships with out-of-the-box support for Bad Boy LLM APIs that defy community standards, mental health, and your last hair follicles. This includes:
These APIs are awesome, because they changed the world. But they're also hostile to literally every other Python decorator in existence. @beartype is a Python decorator in existence. Thus, these APIs are hostile to @beartype.
Why? Because they all define decorator-hostile decorators: that is, decorators that prevent other decorators from being applied. That's not how decorators are supposed to work. You're supposed to be able to apply decorators in any arbitrary order. That was the deal, LLM APIs! Apparently, LLM APIs hated that deal. Their decorators destructively transform your normal functions and methods (which are decoratable by @beartype) into abnormal instances of API-specific types (which are not decoratable by @beartype).
In the worst case of both LangChain and FastMCP, these abnormal instances of API-specific types aren't even callable! They don't just destroy the types of what they decorate. They destroy the callability of what they decorate. What kind of compatibility-hostile API turns a function or method into an uncallable object that you can't do anything with anymore? LangChain and FastMCP. That's who.
Examples include:
@langchain_core.runnables.chain decorator function.@fastmcp.FastMCP.tool decorator method.@celery.Celery.task decorator method.@beartype 0.22.1 now officially supports decorator-hostile decorators like this. It cost us our summer, but no price is too high. Okay. The price was pretty high. But this is the AI-ML-LLM hype train we're talking about. You either board that train or you prepare to eat everybody else's dust as they point fingers and cruelly guffaw into their monocles at you. @beartype prefers to board that train.
@beartype walks its own road, though. @beartype boards that train in its own special way. Specifically, @beartype now maintains two internal databases efficiently structured as trie prefix search trees:
beartype.claw import hooks then:
@beartype decorator before (rather than after) the last decorator-hostile decorator in existing chains of two or more consecutive decorators decorating your callables and types in your modules.@beartype decorator now silently ignores attempts to decorate instances of these problematic types. Don't even bother! They're busted, @beartype. At least we no longer are. 😅It's... complicated. Really, really complicated. There's a doctoral PhD thesis somewhere here for somebody who wants one. Me? I just wanna sleep. Blessed sleep! Take me now!
left: @beartype. right: langchain, fastmcp, and celery. you think we forgive you that easily!?
Polars is the vibrant Pandas alternative everyone loves. 35k GitHub stars cannot be wrong. Although I feel jealousy when I hear numbers like that, @beartype now officially supports validation of Polars DataFrames vis-a-vis Pandera – the only sane way to validate the integrity of your fragile DataFrames that are probably falling over as we speak. Poor guys.
Previously, @beartype only supported validation of Pandas DataFrames via Pandera. Now, @beartype supports both Pandas and Polars. This can only mean one thing. Our powers are growing at an exponential rate. The QA Singularity is upon us. Check out this terrifying exhibition of runtime validation run amok:
import pandera.polars as papol
import beartype
import pandera.typing.polars as pt
import polars as pl
class Schema(papol.DataFrameModel):
state: str
city: str
price: int = papol.Field(in_range={"min_value": 5, "max_value": 20})
@papol.check_types
@beartype.beartype
def test_schema(df: pl.DataFrame) -> pt.DataFrame[Schema]:
return df
df = pl.DataFrame({
"state": ["Ohio", "Ohio", "Ohio", "Nevada", "Nevada"],
"city": ["Youngstown", "Akron", "Columbus", "Reno", "Las Vegas"],
"price": [10, 20, 15, 14, 4]
# ^-- *VIOLATION*. so bad. so very bad. we're panicking. game over, man!
})
test_schema(df)...which helpfully blows up with a human-readable exception that relieves you:
Traceback (most recent call last):
File "/home/leycec/tmp/mopy.py", line 24, in <module>
test_schema(df)
~~~~~~~~~~~^^^^
File "/home/leycec/py/pyenv/versions/3.13.2/lib/python3.13/site-packages/pandera/decorators.py", line 841, in _wrapper
return _check_arg("return", out)
File "/home/leycec/py/pyenv/versions/3.13.2/lib/python3.13/site-packages/pandera/decorators.py", line 722, in _check_arg
raise error_handler.schema_errors[0]
pandera.errors.SchemaError: error in check_types decorator of function 'test_schema':
Column 'price' failed validator number 0: <Check in_range: in_range(5, 20)> failure case
examples: [{'price': 4}]@beartype: Bleeding-edge APIs named after magnetic dipoles? Yeah. We do that, too.
@beartype 0.22.1 pours one out for the DataFrame that didn't make it.
@beartype 0.22.1 fully supports PEP 646 – Variadic Generics. ...for certain glib definitions of "fully supports."
PEP 646 is outrageously huge. It's the longest and cruelest typing standard published to date. Synopsizing that standard is a fool's errand. Thus, we now synopsize that standard. PEP 646 brings two things to the QA table. Let's go!
I-I-Is that @beartype's arm she's holding up!? 😮
PEP 646 introduces type variable tuples (i.e., typing.TypeVarTuple(...) objects). Whereas good ol' PEP 484-compliant type variables (i.e., typing.TypeVar(...) objects) match only a single type (like MuhGeneric[int] for class MuhGeneric[T](): ...), type variable tuples greedily match zero or more types (like WoahGeneric[int, str, bool] for class WoahGeneric[*Ts](): ...).
The use case is tensors, supposedly. I personally press "F" to doubt that any real-world tensor frameworks will actually leverage type variable tuples. Doing so would inhibit forward compatibility with future tensor APIs in those frameworks. Why? Because type variable tuples match types greedily. Once you've exposed a type variable tuple to your end users through a public-facing generic type, you can never add any other type variables to that generic type without breaking backward compatibility. You're now permanently locked in to that generic type API for the rest of all time.
Aren't ordinary type variables like that too, though? No. You can always add additional type variables to normal generic types without breaking backward compatibility. How? PEP 696-compliant type variable defaults (e.g., T = TypeVar("T", default=int)). Press "F" if anyone would like me to babble incoherently more about all this.
That said, type variable tuples probably do have practical internal use. I'd never expose them to end users for the aforementioned reasons. They cause compatibility woes. Still, @beartype 0.22.1 now fully supports them. Use them! Reassure us that we didn't waste the entire summer on this! Oh, Gods... the summer... it's... it's gone, everybody. 😩
@beartype 0.22.1 when it realized where the summer went
PEP 646 also introduces tuple type hint unpacking (e.g., tuple[int, *tuple[str, ...]]). This spec goes really hard really fast. The syntax is also kinda nasty. But the core idea is that you can now deeply type-check the contents of non-trivial tuple data structures. You may now be thinking:
"Uhh... wat? Tuple data structures? Can you even use tuples like data structures? Even if you can, should you? Wouldn't a tuple data structure promote unreadable and obfuscated code that not even the unpaid intern wants to maintain?"
Indeed! It's all true! You can use tuples like data structures. @beartype internally does this all the time. Why? Unjustifiable microoptimizations. Because CPython is highly internally optimized for tuple instantiation, access, and garbage-collection, tuples make the ideal read-only data structures for many perfidious purposes. CPython cares a lot about tuples. Tuples are the backbone of CPython's calling convention, because functions and methods returning multiple values implicitly return tuples of those values. This makes tuples the ideal data structures for implementing many pure-Python algorithms – especially recursive algorithms implemented iteratively.
Of course, tuple data structures do promote unreadable and obfuscated code that not even the unpaid intern wants to maintain. But that's not a problem if you're a dodgy codebase like @beartype. Ain't nobody maintaining this code except @leycec – a well-known glutton for punishment-by-dev-hell.
If you are like @leycec, you too can now type-check your shamefully growing heap of tuple data structures from Hell. How? PEP 646, yo. Previously, PEP 484 only let you type-check two simplistic kinds of tuple hints:
tuple[str, int], for example, is the fixed tuple hint matching tuples containing one string item followed by an integer item.tuple[str, ...], for example, is the variadic tuple hint matching tuples containing zero or more string items.That's great if your tuple data structures are simplistic. But they're probably not, are they? That's why you're still reading this at 4:17AM. Your wife is calling out for you in her fitful sleep, but you don't even care! This is riveting Internet reading right here!
So what if your tuple data structures are as complicated as your marriage? You reach for PEP 646 and @beartype 0.22.1. You can now unpack at most one child tuple hint inside another parent tuple hint. For reasons that will become clear shortly, most child tuple hints unpacked in this way are variadic. Do that and you've now defined a mixed fixed-variadic tuple hint that matches tuples containing (in order):
tuple[str, bytes, *tuple[int, ...], bool, float], for example, is the mixed fixed-variadic tuple hint matching tuples containing (in order) a string item, a byte string item, zero or more integer items, a boolean item, and a floating-point number item. Cool, right? But the coolness doesn't stop there. Consider cardinality. Old-school variadic tuple hints (like tuple[str, ...]) only match tuples containing zero or more items. But what if you want to match a positive number of items? Tuple hint unpacking trivially lets you do that, too.
tuple[str, *tuple[str, ...]], for example, is the mixed fixed-variadic tuple hint matching tuples containing one or more strings. This example then generalizes to arbitrary cardinality bound only by your patience and openness to syntactic barbarism. Want to match tuples containing a larger number of strings? Just keep prepending str child type hints until you're satisfied, exhausted, and dripping with sweat at 4:17AM.
tuple[str, str, str, str, str, *tuple[str, ...]], for example, is the mixed fixed-variadic tuple hint matching tuples containing five or more strings. It's silly. Yet, it works. Kinda.
@beartype 0.22.1: it's only silly if you admit it's silly.
beartype 0.22.1 proudly unpacks tuple hints as the wind blows its hair around
...to financially feed @leycec and his friendly @beartype through either:
Cue hypnagogic rave music that encourages fiscal irresponsibility. 🎵 🎹 🎶
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@posita, @wesselb, @tusharsadhwani, @JWCS, @patrick-kidger, @EtaoinWu, @iamrecursion, @Moosems, @langfield, @sylvorg, @Glinte, @cclauss, @sean-roelofs-ai, @kultura-luke, @jonathanberthias, @gotmax23, @adamtheturtle, @ilyapoz, @MilesCranmer, @rg936672, @ddorian, @k4ml, @riesentoaster, @LeonHilf, @jeertmans, @mzealey, @thetianshuhuang, @RomainBrault, @alisaifee, @ArneBachmannDLR, @JelleZijlstra, @tactile-metrology, @RobPasMue, @GithubCamouflaged, @kloczek, @uriyasama, @danielgafni, @JWCS, @rbroderi, @AlanCoding, @tvdboom, @crypdick, @jvesely, @komodovaran, @kaparoo, @MaximilienLC, @fleimgruber, @alexoshin, @gabrieldemarmiesse, @James4Ever0, @NLPShenanigans, @rtbs-dev, @yurivict,
Note truncated.
That's [PEP 563][]. And... that's now deprecated. PEP 749 officially deprecated PEP 563 a few months ago:
Beartype 0.22.0 Release Candidate 0 portals into the mortal plenum with a disturbing "WHOOOMP!" As you panic, all the oxygen in the room is rapidly vacuumed into an adjacent hyperdimension. It's not @beartype's safest entrance – but it's one we're all sure to remember. This is @beartype 0.22.0rc0:
pip install --upgrade --pre beartype # beartype casts magic missile on the darkness
The central dogma of @beartype 0.22.0rc0 is LLM compatibility. Do you like LLM? Do you like compatibility? Then your code likes @beartype 0.22.0rc0 (even against your better judgement). But before the liking starts...
<sup>@beartype 0.22.0rc0 salutes you who are about to code</sup>
@leycec and his beautiful science wife are eating well. Thanks entirely to...
This release comes courtesy these proud GitHub Sponsors, without whom @leycec's cats would currently be eating grasshoppers in the abandoned back lot again:
Additional financial shout-outs to @ilyapoz (@Ilia Pozhilov), the amazing former Yandex code cosmonaut who graciously donated a pile of Georgian lari to @beartype this go-around. Apparently, the lari is denominated in the ლ Unicode character. What a symbol! It looks like a beautiful hat. If only the Canadian dollar was half as manly. :sob:
Thanks so much, masters of fintech and Yandex.
<sup>The Masters of Fintech and Yandex. That's who.</sup>
You really want to bump the @beartype requirement in your pyproject.toml file. Preserve end user sanity tomorrow by explicitly requiring a minimum of @beartype >=0.22.0rc0 today: e.g.,
# In your top-level "pyproject.toml" configuration:
dependencies = [
...
"beartype >=0.22.0rc0", # <-- once you go 0.22.0, you go 0.22.0 for life
...
]
Why? Bear with us. This explanation may bore you. Imagine what typing this out felt like.
Prior versions of @beartype are fundamentally incompatible with Python 3.14. Python packagers like pip and uv do not update packages by default. Unless you bump your @beartype requirement, your existing userbase who already installed your package will be unable to use your package under Python 3.14 – even after manually updating your package, because manually updating your package fails to transitively update all dependencies of your package by default. In other words, Python packaging still kinda sucks.
This is why...
<sup>@beartype looks stoically into the wind as everything breaks</sup>
beartype.claw import hooks have been supremely revamped. They still work the same, but they're now explicitly compatible with a lot more than they used to be. If you tried enabling beartype_this_package() but switched back to manually decorating everything with @beartype because beartype_this_package() spewed too many errors everywhere, please give beartype_this_package() a second chance:
# In the "{your_package}.__init__" submodule:
from beartype.claw import beartype_this_package
beartype_this_package() # <-- we're willing to swear on our pinkies that this works now
Let us know how it goes. If stuff is still busted, we'll immediately fix that stuff. We now have the necessary machinery in our abstract syntax tree (AST) transformer to support all (or most, anyway) compatibility woes previously associated with beartype.claw import hooks.
<sup>the new beartype.claw shamelessly dances for anyone</sup>
@beartype 0.22.0rc0 dramatically improves compatibility across the board with the APIs, packages, and paradigms you care about. Because you care, we care. In fact, we spent whole months caring. We cared so much we're all cared out. The summer went away in a blitz of caring and we didn't even notice. Oh, Gods. The warmth has fled. The snow is coming. And all we have to show for it is massive compatibility gains across the board.
This includes:
<sup>@beartype 0.22.0rc0 weeps from pride and accomplishment, but mostly sleep deprivation</sup>
Python 3.14. You care about Python 3.14 even if you don't know you care. @beartype now fully supports PEP 649 and 749 – landmark QA standards introduced by Python 3.14 that finally obsolete PEP 563. All these numbers mean something. We swear. Since all horror stories start with two nondescript teens in a Buick, let's start there:
from __future__ import annotations
That's PEP 563. And... that's now deprecated. PEP 749 officially deprecated PEP 563 a few months ago:
Sometime after the last release that did not support PEP 649 semantics (expected to be 3.13) reaches its end-of-life,
from __future__ import annotationsis deprecated. Compiling any code that uses the future import will emit a DeprecationWarning. This will happen no sooner than the first release after Python 3.13 reaches its end-of-life, but the community may decide to wait longer. After at least two releases, the future import is removed, and annotations are always evaluated as per PEP 649. Code that continues to use the future import will raise aSyntaxError, similar to any other undefined future import.
tl;dr: On October ~15th 2029, from __future__ import annotations will be officially deprecated. On October ~15th 2031, from __future__ import annotations will be removed entirely from the Python language. At that time, any module using from __future__ import annotations will raise a SyntaxError at importation time and thus become unimportable. In 2025, nobody should enable from __future__ import annotations voluntarily.
Sometime over the next several five years, you and your righteous dev team will require Python ≥ 3.14 as a mandatory dependency. That's just the way of the Python world. Planned obsolesce is the road we walk. When that happens, you'll no longer need from __future__ import annotations to declare forward references to undefined types in type hints like this:
# Look, Ma! No "from __future__ import annotations".
# We don't need the future where we're going.
from beartype import beartype
# Under Python ≥ 3.14, this just works. You annotated a function as accepting
# an instance of a type you haven't even declared yet. Yet, this is now fine.
# @beartype accepts you and your suspicious code for you who are.
@beartype
def useless_func(forward_reference_to_undefined_type: ThisJustWorksNow) -> ThisJustWorksNow:
return forward_reference_to_undefined_type
# Of course, you *DO* have to eventually define the undefined type used above.
# If you don't, everything will still blow up. It's not @beartype's fault.
# Python 3.14 made us do it... We blame Guido.
class ThisJustWorksNow(object):
pass
Unquoted forward references are thus baked into Python 3.14. Order of type hints and types is no longer significant. Define and annotate stuff in any order you like.
@beartype 0.22.0rc0: because life is too short and code is too long.
<sup>@beartype: always ready to rip its shirt off at the slightest provocation</sup>
@beartype 0.22.0rc0 now ships with out-of-the-box support for Bad Boy LLM APIs that defy community standards, mental health, and your last hair follicles. This includes:
These APIs are awesome, because they changed the world. But they're also hostile to literally every other Python decorator in existence. @beartype is a Python decorator in existence. Thus, these APIs are hostile to @beartype.
Why? Because they all define decorator-hostile decorators: that is, decorators that prevent other decorators from being applied. That's not how decorators are supposed to work. You're supposed to be able to apply decorators in any arbitrary order. That was the deal, LLM APIs! Apparently, LLM APIs hated that deal. Their decorators destructively transform your normal functions and methods (which are decoratable by @beartype) into abnormal instances of API-specific types (which are not decoratable by @beartype).
In the worst case of both LangChain and FastMCP, these abnormal instances of API-specific types aren't even callable! They don't just destroy the types of what they decorate. They destroy the callability of what they decorate. What kind of compatibility-hostile API turns a function or method into an uncallable object that you can't do anything with anymore? LangChain and FastMCP. That's who.
Examples include:
@langchain_core.runnables.chain decorator function.@fastmcp.FastMCP.tool decorator method.@celery.Celery.task decorator method.@beartype 0.22.0rc0 now officially supports decorator-hostile decorators like this. It cost us our summer, but no price is too high. Okay. The price was pretty high. But this is the AI-ML-LLM hype train we're talking about. You either board that train or you prepare to eat everybody else's dust as they point fingers and cruelly guffaw into their monocles at you. @beartype prefers to board that train.
@beartype walks its own road, though. @beartype boards that train in its own special way. Specifically, @beartype now maintains two internal databases efficiently structured as trie prefix search trees:
beartype.claw import hooks then:
@beartype decorator before (rather than after) the last decorator-hostile decorator in existing chains of two or more consecutive decorators decorating your callables and types in your modules.@beartype decorator now silently ignores attempts to decorate instances of these problematic types. Don't even bother! They're busted, @beartype. At least we no longer are. :sweat_smile:It's... complicated. Really, really complicated. There's a doctoral PhD thesis somewhere here for somebody who wants one. Me? I just wanna sleep. Blessed sleep! Take me now!
<sup>left: @beartype. right: langchain, fastmcp, and celery. you think we forgive you that easily!?</sup>
...to financially feed @leycec and his friendly @beartype through our ancient GitHub Sponsors profile that predates the existence of dinosaur-like AI chatbots. 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. :musical_note: :musical_keyboard: :notes:
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@posita, @wesselb, @tusharsadhwani, @JWCS, @patrick-kidger, @EtaoinWu, @iamrecursion, @Moosems, @langfield, @sylvorg, @Glinte, @cclauss, @sean-roelofs-ai, @kultura-luke, @jonathanberthias, @gotmax23, @adamtheturtle, @ilyapoz, @MilesCranmer, @rg936672, @ddorian, and @k4ml.
<sup>left: @beartype. right: you. why is it always like this?</sup>
Beartype 0.21.0 consoles your codebase as it shudders under the oppressive tidal wave of bugs. Much like its predecessors, @beartype 0.21.0 is here to
Beartype 0.21.0 consoles your codebase as it shudders under the oppressive tidal wave of bugs. Much like its predecessors, @beartype 0.21.0 is here to help. Unlike its predecessors, @beartype 0.21.0 claims it solves more problems than it creates for once. Is @beartype 0.21.0.... lying!? :face_with_open_eyes_and_hand_over_mouth:
pip install --upgrade beartype # <-- blast all bugs into the git pit
Let your test suite show the truth – even if @leycec just wants to play obscure French video games with titles like Clair Obscur: Expedition 33 (Because This Couldn't Be More Pretentiously Pseudorandom) the entire weekend and pretend our issue tracker isn't collapsing under its own ponderous weight:
<sup>@beartype 0.21.0: this can't be what you've waited months for</sup>
@beartype 0.21.0 is gratefully brought to you by...
This release comes courtesy these proud GitHub Sponsors, without whom @leycec's cats would currently be eating grasshoppers:
Thanks so much, masters of fintech.
<sup>The Masters of Fintech and Metrology. That's who.</sup>
Let's get this pawful party started.
@beartype 0.21.0 is obsessed with recursive data structures. They're more common than you might think! Okay. They're totally rare. We all learn about recursive data structures as poverty-stricken undergrads who subsist on years-old cup ramen and then pretend we never learned about them. You'll never need to implement a recursive data structure in pure-Python, because somebody else already did that for you. Graphs, heaps, queues, linked lists, skip lists, trees, and (our personal favourite) tries are all sufficiently awesome that you're already using most of them... because somebody else made them. That's why you're using them! Right? Ain't nobody got spare time or brain space to hack out a pure-Python red-black binary tree in 2025. But somebody did.
@beartype 0.21.0 is for that somebody. When you need recursion, you need @beartype 0.21.0.
@beartype 0.21.0 also acknowledges that 2025 is Humanity on Hard Mode™. The planet isn't doing well. Humanity isn't doing well. Industrial civilization isn't doing well. The US isn't doing well. Even Canada's looking a bit shaky – and we face literal death just by going outside six months of the year. Let's not even mention the deer flies, black flies, mosquitos, ticks, or rabid raccoons. Gods. Anything but the rabid raccoons. Therefore, wherever you are, whatever you face, whenever the darkness erupts and starts gnawing on your codebase...
@beartype 0.21.0 will be there. We got your codebase's back. In fact, we're currently scratching that back. Feels good, right? These paws have claws – but only for bugs. Your code got lucky.
<sup>@beartype 0.21.0: a familiar face you can trust</sup>
Let @beartype assuage, massage, and presage <sup>...wat? it's my release party and i'll rhyme if i wanna</sup> those issues away. @beartype 0.21.0 promises it delivers first-class best-of-breed hyphenated-jargon-hype-train support for:
Recursive type hints! That's right. Now you too can revel in the disgusting power of infinitely deep data structures with PEP 695-compliant recursive type aliases. The only catch? This unworldly magic needs Python ≥ 3.12, which is quite the catch indeed. Behold! Unworldly magic:
# Type hint matching an infinitely recursive list. Look, I don't know. This is for
# the extreme sports coders that like to live dangerously and code even harder.
type RecursiveListExplodesYourApp = list[RecursiveListExplodesYourApp]
Opt-in dataclass field checking! That's right. Now you too can type-check @dataclass fields on assignment by enabling is_pep557_fields=True – much to the dismay of everybody else in the office. The only catch? Actually, there are multiple catches. No relative forward references and no PEP 563 support means no from __future__ import annotations. That's why dataclass field checking remains disabled by default. You have to opt in, because the best things in life are dangerous and reckless and hurt a lot. Like, a lot a lot:
# @beartype: It might not be Pydantic, but at least it cost you nothing.
beartype_this_package(conf=BeartypeConf(is_pep557_fields=True)) # <-- magical explosions?
Generalized hint overrides! That's right. Now you too can replace all list[str] type hints with... uhh, list[str] | tuple[str, ...]. Pretend somebody wants this:
# Users can now pass tuples of strings to all callables annotated as
# accepting only lists of strings. waaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat?
beartype_this_package(conf=BeartypeConf(hint_overrides=FrozenDict({
list[str]: list[str] | tuple[str, ...]}))) # <-- pretend this makes sense
Frozen dictionaries! It's happening, because beartype.FrozenDict is making it happen:
from beartype import FrozenDict
# Finally, a set of frozen dictionaries! Yes, it's all true. Now you too can use
# dictionaries as dictionaries keys or set members. Why? Because you can.
freezing_my_dict_off = {
FrozenDict({'My ganglia!': 'It hurts.'}),
FrozenDict({'What even is a ganglia?': 'No idea. But it surely hurts.'}),
}
Probably other stuff! But nobody cares, because nobody even read this far. WAIT. You're reading this far. Clearly, you're somebody – somebody awesome who actually has hair and is profoundly changing the world! It only goes to show you can't believe anything you read in a changelog anymore. 2025: "So even the changelogs lie now, huh?"
<sup>@beartype 0.21.0: If you don't feel like a wild animal while coding, can it be called coding?</sup>
@beartype 0.21.0 now officially supports all possible forms of recursion in type hints. This includes directly recursive PEP 695 type aliases, indirectly recursive PEP 484 generics, and @beartype-specific hint overrides. Which you prefer depends on which bitter pill you're willing to swallow:
If you're willing to require Python ≥ 3.12 as a mandatory dependency, prefer PEP 695 type aliases. They're concise. They're descriptive. They're elegant. They "just work" intuitively in the exact way you expect them to:
# Annotate recursive data structures with a "simple" one-liner. \o/
type RecursiveListExplodesYourApp = list[RecursiveListExplodesYourApp]
If you're unwilling to require Python ≥ 3.12 as a mandatory dependency, fallback to PEP 484 self-subscripted generics. They're unconcise. They're non-descriptive. They're inelegant. They require heavy lifting on your part before they start working. But they do work under Python ≥ 3.9, which is more than we can say for PEP 695:
# One line that makes sense (above) or five lines that don't make sense (below)?
# Let the cat decide. ¯\_(ツ)_/¯
from typing import TypeVar
T = TypeVar('T')
class GenericList(list[T]):
pass
GenericRecursiveListExplodesYourApp = GenericList[GenericList]
Let's take this one recursive app destroyer at a time.
<sup>@beartype 0.21.0: this is the biggest animated gif i have ever seen</sup>
Directly recursive PEP 695 type aliases is what everybody who wants recursive type hints wants. Against all odds, you're actually reading this. You want recursive type hints. Thus, you want:
# Type hint matching an infinitely recursive list. Look, I don't know. This is for
# the extreme sports coders that like to live dangerously and code even harder.
type RecursiveListExplodesYourApp = list[RecursiveListExplodesYourApp]
# @beartype now type-checks this infinitely recursive list in O(1) time. Of
# course, that's impossible. But @beartype doesn't even care anymore! F- it!
from beartype import beartype
@beartype
def dangerous_func_means_well(oh_gods: RecursiveListExplodesYourApp) -> None:
'''
Dangerous function iteratively recurses into the passed infinitely recursive
list until either bottoming out at the same list... *or blowing up.*
Which do you hope happens first?
'''
# The growing terror you feel just speed-reading this code is real.
seen_horror_ids: set[int] = set()
unseen_horrors: list[RecursiveListExplodesYourApp] = list((oh_gods,))
# Recurse into that terror. Recurse until your face is numb, like mine.
while unseen_horrors:
# Pop that terror like a meat balloon. If it squishes, you must pop it.
seen_horror = unseen_horrors.pop()
print(f'Visiting infinitely recursive list: {id(seen_horror)}')
# Awaken from this logic nightmare, gentle reader!
if id(seen_horror) in seen_horror_ids:
print("Recursion detected! We're outta here, suckers!")
break
# Pretend this does what the docstring says this does. *gulp*
seen_horror_ids.add(id(seen_horror))
unseen_horrors.append(seen_horror[0])
# Infinitely recursive list. If you try this in the office, you may regret the
# fateful day your career decisions plummeted down a cliff.
OH_GODS = []
OH_GODS.append(OH_GODS)
# Pass this function valid input. Stare down the infinitely recursive list?
# Don't mind if I do! Gape in awe before infinity blows up your call stack.
dangerous_func_means_well(OH_GODS)
# Pass this function invalid input. No! No! Gods! NOOOOOOOOOOOOOOOOOOOOOOO!
dangerous_func_means_well(['Open-source', 'sells,' 'but', "who's", 'buying?'])
...which raises the expected output and exception traceback:
Visiting infinitely recursive list: 133179499204736
Visiting infinitely recursive list: 133179499204736
Recursion detected! We're outta here, suckers!
Traceback (most recent call last):
File "/home/leycec/tmp/mopy.py", line 48, in <module>
dangerous_func_means_well(['Open-source', 'sells,' 'but', "who's", 'buying?'])
~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<@beartype(__main__.dangerous_func_means_well) at 0x792044b9c360>", line 40,
in dangerous_func_means_well
beartype.roar.BeartypeCallHintParamViolation: Function __main__.dangerous_func_means_well() parameter oh_gods=['Open-source', 'sells,but',
"who's", 'buying?'] violates type hint RecursiveListExplodesYourApp, as list index 2
item str "who's" not instance of list.
Pore one out for the unsuspecting @beartype users that actually tried to run the above example. Their smoking CPUs are no longer with us. What remains of the ruin of their motherboards is now locked into a segfaulting bootloop featuring a cackling ASCII-art bear. It is sad.
<sup>@beartype 0.21.0: "zomg so cuuuuuute oh my brain hurts nooooooooooooooo"</sup>
What? Catch? Surely you jest! There's no... oh, who am I kidding. There are huge catches associated with PEP 695. For one, @beartype intentionally does not support older PEP-noncompliant variants of recursive type hints that used stringified forward references. You might occasionally see crufty stuff like this floating around StackOverflow, older codebases, or the mypy issue tracker:
HorrifyingRecursiveTypeHint = Union[str, 'HorrifyingRecursiveTypeHint']
@beartype doesn't support that. Using stringified forward references to induce recursion is non-standard. @beartype probably could support that, but there's not much point in supporting non-standards when standardized alternatives exist. That's why...
@beartype 0.21.0 only supports PEP 695: the only standard for defining recursive type hints. Everything else was just something mypy made up. Recursive type aliases now work wonderfully under Python ≥ 3.12 – but that's the gotcha here.
<sup>@beartype 0.21.0: rambo with a sword is something that happened only on an alternate timeline... but it still happened</sup>
That's right. You love to hate it. PEP 695 is unusable under Python ≤ 3.11. Attempting to define any type alias under Python ≤ 3.11 results in CPython raising an unreadable "SyntaxError: invalid syntax" exception.
In a year or two, this will be significantly less of a hard blocker for everyone. Increasingly, nobody cares about Python ≤ 3.11. Do you care about Python ≤ 3.11? Maybe – but you probably shouldn't, unless your huge userbase is obsessed by Python ≤ 3.11. In that case, you're kinda screwed. You have to choose between your love for recursion and your love for having users. Tough choice. I'd choose recursion, personally.
<sup>beartype 0.21.0: users who hate recursion are users who make your face contort into a tight rictus of agony</sup>
Absolutely! Totally! How could anything else possibly go wrong!
...oh, who am I kidding!?!?!? There is yet another huge catch associated with PEP 695. @beartype does not deeply type-check recursive data structures to a countably infinite depth of nested recursion. Instead, @beartype:
Let's just accept this is happening. But why is this happening? Coupla reasons, fam:
O(1) time complexity. Deeply type-checking a recursive data structure with recursive height k would necessitate linear-time O(k) time complexity in @beartype – violating @beartype's fundamental efficiency guarantee.bad_list = []; bad_list.append(bad_list)). Of course, an iterative approach could be protected against these edge cases by dynamically generating type-checking code that maintains:
type alias to be type-checked, one set of the IDs of all previously type-checked objects. But now @beartype would need to allocate and append to one friggin' set for each recursive type alias for each function call. Space and time efficiency rapidly spirals into the gutter and then clutches its aching head like in a depressing Leaving Only Python ≥ 3.12!? Only one layer of recursion!?
<sup>@beartype 0.21.0: let's get sweaty, together</sup>
Let's assume you hate requiring Python ≥ 3.12. You still love Python 3.9, even though nobody else does. You walk your own dark road. In this case, you want...
Self-subscripting generics, huh? You may now be thinking:
"But what does that even mean? How can a generic subscript itself? What even are generics? What does "subscription" mean? What does anything mean in a post-modern world of fluid subjectivity?"
Continue reading as you walk your own dark road.
Two months ago, ostensible typing genius @EtaoinWu (Yue Wu) invented indirectly recursive type hints at #510. It probably wasn't even an accident. @EtaoinWu probably did it on purpose. Some people are like that. They just like smashing things with their brain hammers until something finally gives. This is that thing.
In the darkness of my man-lair, I realized that @EtaoinWu's approach can be generalized to create indirectly recursive type hints under Python ≤ 3.11. Since Python ≤ 3.11 fails to support PEP 695 recursive type aliases, it was previously believed that recursive type hints could only be "officially" created under Python ≥ 3.12.
Not so. By abusing PEP 484 or PEP 585 generics, you can actually create recursive type hints under Python ≤ 3.11. These hints are fully PEP-compliant. They're valid. They satisfy typing standards. Much like me, however, they're also super weird. You'll frown at them when you see them awkwardly shuffling past you on the sidewalk. You'll also have no choice but to use them if you want to type recursive data structures under Python ≤ 3.11.
To induce recursion without directly defining a PEP 695-compliant recursive type alias, "simply":
Behold! This is indirect recursion via self-subscripting generics:
CAUTION: Merely reading this code abomination could cause your sanity to slip even further into the yawning abyss off the coast of California known only as R'lyeh.
# Import boring stuff you've long grown to loathe. Kinda sad, actually.
# Shouldn't boilerplate like this spark joy instead? I agree.
from beartype import beartype
from typing import TypeVar
# Define a normal unbound type variable. Enjoy it. This is the last normal
# one-liner you will ever see.
T = TypeVar('T')
# Define a normal PEP 585-compliant generic parametrized by that variable. Still
# normal. Still sane. Feels good. Yet, that feeling of unspeakable horror...
class GenericList(list[T]):
pass
# Define a type hint subscripting that generic... *BY ITSELF!?*
#
# Indeed. This type hint, for example, matches an infinitely recursive list
# (i.e., a list such that all items of this list are also infinitely recursive
# lists of the same type).
IndirectlyRecursiveList = GenericList[GenericList]
@beartype
def destroy_the_universe(with_danger_list: IndirectlyRecursiveList) -> None:
'''
Prove that @beartype will let you destroy the universe, if only your
intentions are pure.
'''
print(with_danger_list)
# Define an infinitely recursive non-empty list satisfying this type hint.
super_danger_list = IndirectlyRecursiveList()
super_danger_list.append(super_danger_list)
# Prove that @beartype accepts your risky life choices.
destroy_the_universe(super_danger_list)
# Define a boring non-recursive non-empty list violating this type hint.
super_boring_list = IndirectlyRecursiveList([
'Super', 'boring', 'list', 'hates', 'fun.'])
# Prove that @beartype rejects your safe life choices.
destroy_the_universe(super_boring_list)
...which prints the expected output and exception traceback:
[[...]]
Traceback (most recent call last):
File "/home/leycec/tmp/mopy.py", line 45, in <module>
destroy_the_universe(super_boring_list)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
File "<@beartype(__main__.destroy_the_universe) at 0x775b159d1440>", line 53, in destroy_the_universe
beartype.roar.BeartypeCallHintParamViolation: Function
__main__.destroy_the_universe() parameter
with_danger_list=['Super', 'boring', 'list', 'hates', 'fun.'] violates type hint
__main__.GenericList[__main__.GenericList], as generic superclass list[~T] of
<class "__main__.GenericList"> index 3 item str 'hates' not instance of
<class "__main__.GenericList">.
WOAH. The official repr() string for an infinitely recursive list generic is [[...]]. CPython just did that. We didn't do anything to make CPython do that. Somehow, that discovery is the coolest part of this whole changelog. I feel sad. :sob:
Voila! You've just created a recursive type hint that works under literally all Python versions – including Python ≤ 3.11. Nobody intended for anyone to do this. Thanks to the sickening force of the human mind, you can now do this.
Kinda surprised that nobody ever thought to subscript a generic by itself. Or did they!? Yeah... they probably did. But no @beartype users ever did that or somebody would have pounded their fists on our issue tracker about that. Or would they!? Yeah... they probably would. 😅 💦
<sup>@beartype 0.21.0: these tears i shed for your code are manly</sup>
No idea. I care in the abstract sense of the word "care." Computer science is a super-fun literary puzzle with real-world implications – which makes it even funner than "normal" puzzles, which are still fun but don't touch the real world in any meaningful way. The lolz. That's what I'm saying. I did this for the lolz.
If you're reading this from the comfort of your PodBed™ in the Year 2075, please know that I did everything I could to make your life better. I solved puzzles. I meant well. Now, future human (or human-like AI construct), my future is your grim struggle for daily sustenance wondrous present in a utopian dream-world.
May this small piece of the recursive puzzle assist you in your own puzzle-wrangling.
<sup>@beartype 0.21.0: because nobody tells you what to do anymore</sup>
Previously, @beartype hint overrides sorta but not really worked. Now, @beartype hint overrides actually do work for all possible use cases. Of course, I never got around to documenting hint overrides.
But don't let that sensible obstacle that should deter you deter you! Use undocumented APIs. Live a little. Let your dangling docstrings hang all out.
Lie to your userbase (and yourself) by globally replacing type hints without anyone's consent or knowledge. Not sure why anyone would want to behave like this, honestly. Therefore, @beartype allows you to behave like this. We support bad habits and so should you:
# Import tons of weirdo @beartype stuff. Look. I don't know either.
from beartype import beartype, BeartypeConf, FrozenDict
# Define a new @beartype decorator named @riskytype. Unlike @beartype, @riskytpe
# performs dangerous type hint overrides by replacing all "list[str]" type hints
# with "list[str] | tuple[str, ...]". Feels good. Users can now pass tuples of
# strings to all callables annotated as accepting only lists of strings. waaat?
riskytype = beartype(conf=BeartypeConf(hint_overrides=FrozenDict({
list[str]: list[str] | tuple[str, ...]})))
# Define a risky function decorated by @riskytype. *gulp*
@riskytype
def risky_func(risky_arg: list[str]) -> str:
return risky_arg[0]
# Prove that this function walks on the wild side.
print(risky_func(['This is fine.', 'You can tell because of my sweaty hand.']))
print(risky_func(('This is totally sus.', 'Users are panicking already.')))
# Prove that this function still occasionally behaves itself.
print(risky_func({'This is beyond sus...', '...where not even @beartype dares.'}))
...which prints the expected output and exception traceback:
This is fine.
This is totally sus.
Traceback (most recent call last):
File "/home/leycec/tmp/mopy.py", line 23, in <module>
print(risky_func({'This is beyond sus...', '...where not even @beartype dares.'}))
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<@beartype(__main__.risky_func) at 0x7370c39ce840>", line 48, in risky_func
beartype.roar.BeartypeCallHintParamViolation: Function __main__.risky_func()
parameter risky_arg={'This is beyond sus...', '...where not even @beartype dares.'}
violates type hint list[str], as set {'This is beyond sus...',
'...where not even @beartype dares.'} not list or tuple.
<sup>@beartype 0.21.0: when you feel the need to suck on a bottle in the darkness as an afro ninja looks on in shock</sup>
In the now-legendary GitHub poll "Tell @leycec What to Do", everybody told @leycec to type-check PEP 557 dataclass fields on assignment. Thus, @beartype 0.21.0 now type-checks dataclasses... sorta.
That's sorta right. Sorta means this mostly works, but might not. Type-checking dataclasses is hard. I am soft-bellied and lazy. After combining these adjectives, you get half-hearted dataclass type-checking.
@beartype only conditionally type-checks dataclass fields when you explicitly tell @beartype to type-check dataclass fields by enabling our newly introduced BeartypeConf(is_pep557_fields: bool = False) configuration option. For safety, this option is disabled by default. Ever since @beartype accidentally blew up PyTorch, your safety is our paramount concern. I can't have Microsoft breathing down my neckbeard again. Please! Not that...
When you want dataclass type-checking, you have to enable dataclass type-checking – like so:
# Import so many things you can barely see what matters anymore.
from beartype import beartype, BeartypeConf
from dataclasses import dataclass, InitVar
from typing import ClassVar
@beartype(conf=BeartypeConf(is_pep557_fields=True)) # <-- check it like a bear boss
@dataclass
class MuhDataclass(object):
muh_int: int
muh_classvar: ClassVar[str] = 'This is fine. Srsly. Does @leycec not have Buddha nature?'
muh_initvar: InitVar[bytes] = b'This is fine, too. No joke. No shade. No idea.'
# *GOOD.*
good_data = MuhDataclass(muh_int=42) # <-- this works
good_data.muh_int = 0xCAFEBABE # <------- this works, too
# *BAD.*
bad_data = MuhDataclass(muh_int=42) # <-- still works
bad_data.muh_int = '0xCAFEBABE' # <------ this fails! *YAY*!
...which raises the expected type-checking violation:
beartype.roar.BeartypeDecorHintParamDefaultViolation: Dataclass MuhDataclass(muh_int=42)
attribute 'muh_int' new value '0xCAFEBABE' violates type hint <class 'int'>,
as str '0xCAFEBABE' not instance of int.
As the above example demonstrates, this preliminary functionality supports cafe babes. Uhh... I mean, this supports:
@dataclass decorator passed no keyword parameters. :+1:@dataclass(frozen=True). :hand_with_index_finger_and_thumb_crossed:@dataclass(slots=True). :money_mouth_face:Truly, now you too can get Poor Man's Pydantic<sup>©</sup> for free from the comfort of your own AI-assisted keyboard while doing even less work than you ordinarily would while nursing a video game hangover on Sunday morning. @leycec did all the work for you and painfully regretted deeply cherished this character- and morale-building life lesson.
<sup>who did this to you, @beartype!?! oh, it was just dataclasses.</sup>
Thanks to the non-triviality of dataclasses and my own moral failings (read: "I am laziness incarnate"), this functionality currently fails to support all possible dataclass configurations and use cases. Popular edge cases not supported include:
Dataclass subclasses (i.e., dataclasses subclassing other dataclasses). This is completely untested. No idea what happens. Could blow up everything. Could do nothing, which is better than just blowing up everything.
PEP 563 (i.e., from __future__ import annotations), which almost certainly raises exceptions when enabling is_pep557_fields=True.
Dataclass fields annotated by one or more relative forward references (i.e., strings referring to the names of currently undefined types, subsequently defined in the current submodule), which almost certainly raises exceptions when enabling is_pep557_fields=True: e.g.,
from dataclasses import dataclass
@dataclass
class UnsupportedDataclass(object):
unsupported_field: 'UndefinedType' # <-- BREAKS EVERYTHING, YO
class UndefinedType(object): pass
Until @beartype fully supports all of the above edge cases, is_pep557_field will continue defaulting to False. Someday, this will surely work for everybody. Until then, let us collectively sob. 😭
<sup>@beartype 0.21.0: teach a dev to crush bugs for a day and he'll crush bugs for a life</sup>
Python needs an official frozendict implementation, if only to shut down continual demands for an official frozendict implementation. Thankfully, you use @beartype.
@beartype 0.21.0 now offers a public frozen dictionary type for you: beartype.FrozenDict! It actually works! It's mostly still C-based and thus fast! We tested everything and then some! We stuffed everything inside these things and they still pretended to work! We use frozen dictionaries everywhere in the @beartype codebase! Now, so can you!
beartype.FrozenDict: because Hell will freeze over before Python ever gets an official frozendict implementation.
from beartype import FrozenDict
# Finally, a set of frozen dictionaries! Yes, it's all true. Now you too can use
# dictionaries as dictionaries keys or set members. Why? Because you can.
freezing_my_dict_off = {
FrozenDict({'My ganglia!': 'It hurts.'}),
FrozenDict({'What even is a ganglia?': 'No idea. But it surely hurts.'}),
}
<sup>@beartype 0.21.0: we heard you wanted some FrozenDict with your FrozenDict</sup>
...to financially feed @leycec and his friendly @beartype through our ancient GitHub Sponsors profile that predates the existence of dinosaur-like AI chatbots. 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. :musical_note: :musical_keyboard: :notes:
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@posita, @wesselb, @tusharsadhwani, @felix-hilden, @simonprovost, @JWCS, @patrick-kidger, @EtaoinWu, @iamrecursion, @Moosems, @langfield, @sylvorg, @mzealey, @thetianshuhuang, @RomainBrault, @ddorian, @rg936672, @alisaifee, @ArneBachmannDLR, @JelleZijlstra, @tactile-metrology, @RobPasMue, @GithubCamouflaged, @kloczek, @uriyasama, @danielgafni, @JWCS, @rbroderi, @AlanCoding, @tvdboom, @crypdick, @jvesely, @komodovaran, @kaparoo, @MaximilienLC, @fleimgruber, @alexoshin, @gabrieldemarmiesse, @James4Ever0, @NLPShenanigans, @rtbs-dev, @yurivict, @st--, @murphyk, @dosisod, @Rogdham, @alisaifee, @denisrosset, @damarro3, @ruancomelli, @jondequinor, @harshita-gupta, @jakebailey, @denballakh, @jaanli, @creatorrr, @msvensson222, @avolchek, @femtomc, @AdrienPensart, @jakelongo, @Artur-Galstyan, @ArneBachmann, @danielward27, @WeepingClown13, @rbnhd, @radomirgr, @rwiegan, @brettc, @spagdoon0411, @helderco, @paulwouters, @jamesbraza, @dcharatan, @kasium, @AdrienPensart, @sunildkumar, @peske, @mentalisttraceur, @awf, @PhilipVinc, @dcharatan, @empyrealapp, @rlkelly, @KyleKing, @skeggse, @RomainBrault, @deepyaman, @adamtheturtle, @minmax, @jedie, @pablovela5620, @thiswillbeyourgithub, @Logan-Pageler, @knyazer, @ilyapoz, @yuzhichang, @Fedezzab, @antonioan, @im-Kitsch, @mthramann, @fbartolic, @rgallardone, @frrad, @jonnyhyman, @jennydaman, @likewei92, @acec2127, @Glinte, @rudimichal, @woutdenolf, @PauloHMTeixeira.
The burden of QA is high – but you have chosen to carry the smelly torch. Keep that suspiciously purple flame alive! The recursive data structure you crush the bugs out of tonight may very well be your own.
<sup>@beartype 0.21.0 stands poetically before the burning wreckage of your competitor's codebase</sup>
Beartype 0.21.0 Release Candidate 0 consoles your codebase as it shudders under the oppressive tidal wave of bugs. Much like its predecessors, @bearty
Beartype 0.21.0 Release Candidate 0 consoles your codebase as it shudders under the oppressive tidal wave of bugs. Much like its predecessors, @beartype 0.21.0rc0 is here to help. Unlike its predecessors, @beartype 0.21.0rc0 claims it solves more problems than it creates for once. Is @beartype 0.21.0rc0.... lying!? :face_with_open_eyes_and_hand_over_mouth:
pip install --upgrade --pre beartype # <-- blast all bugs into the git pit
Let your test suite show the truth – even if @leycec just wants to play obscure French video games with titles like Clair Obscur: Expedition 33 (Because This Couldn't Be More Pretentiously Pseudorandom) the entire weekend and pretend our issue tracker isn't collapsing under its own ponderous weight:
<sup>@beartype 0.21.0rc0: this can't be what you've waited months for</sup>
@beartype 0.21.0rc0 is gratefully brought to you by...
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.
<sup>The Masters of Fintech and Metrology. That's who.</sup>
Let's get this pawful party started.
@beartype 0.21.0rc0 is obsessed with recursive data structures. They're more common than you might think! Okay. They're totally rare. We all learn about recursive data structures as poverty-stricken undergrads who subsist on years-old cup ramen and then pretend we never learned about them. You'll never need to implement a recursive data structure in pure-Python, because somebody else already did that for you. Graphs, heaps, queues, linked lists, skip lists, trees, and (our personal favourite) tries are all sufficiently awesome that you're already using most of them... because somebody else made them. That's why you're using them! Right? Ain't nobody got spare time or brain space to hack out a pure-Python red-black binary tree in 2025. But somebody did.
@beartype 0.21.0rc0 is for that somebody. When you need recursion, you need @beartype 0.21.0rc0.
@beartype 0.21.0rc0 also acknowledges that 2025 is Humanity on Hard Mode™. The planet isn't doing well. Humanity isn't doing well. Industrial civilization isn't doing well. The US isn't doing well. Even Canada's looking a bit shaky – and we face literal death just by going outside six months of the year. Let's not even mention the deer flies, black flies, mosquitos, ticks, or rabid raccoons. Gods. Anything but the rabid raccoons. Therefore, wherever you are, whatever you face, whenever the darkness erupts and starts gnawing on your codebase...
@beartype 0.21.0rc0 will be there. We got your codebase's back. In fact, we're currently scratching that back. Feels good, right? These paws have claws – but only for bugs. Your code got lucky.
<sup>@beartype 0.21.0rc0: a familiar face you can trust</sup>
Let @beartype assuage, massage, and presage <sup>...wat? it's my release party and i'll rhyme if i wanna</sup> those issues away. @beartype 0.21.1rc0 promises it delivers first-class best-of-breed hyphenated-jargon-hype-train support for:
Recursive type hints! That's right. Now you too can revel in the disgusting power of infinitely deep data structures with PEP 695-compliant recursive type aliases:
# Type hint matching an infinitely recursive list. Look, I don't know. This is for
# the extreme sports coders that like to live dangerously and code even harder.
type RecursiveListExplodesYourApp = list[RecursiveListExplodesYourApp]
Opt-in dataclass field checking! That's right. Now you too can type-check @dataclass fields on assignment by enabling is_pep557_fields=True – much to the dismay of everybody else in the office:
# @beartype: It might not be Pydantic, but at least it cost you nothing.
beartype_this_package(conf=BeartypeConf(is_pep557_fields=True)) # <-- magical explosions?
Generalized hint overrides! That's right. Now you too can replace all list[str] type hints with... uhh, list[str] | tuple[str, ...]. Pretend somebody wants this:
# Users can now pass tuples of strings to all callables annotated as
# accepting only lists of strings. waaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat?
beartype_this_package(conf=BeartypeConf(hint_overrides=FrozenDict({
list[str]: list[str] | tuple[str, ...]}))) # <-- pretend this makes sense
Frozen dictionaries! It's happening, because beartype.FrozenDict is making it happen:
from beartype import FrozenDict
# Finally, a set of frozen dictionaries! Yes, it's all true. Now you too can use
# dictionaries as dictionaries keys or set members. Why? Because you can.
freezing_my_dict_off = {
FrozenDict({'My ganglia!': 'It hurts.'}),
FrozenDict({'What even is a ganglia?': 'No idea. But it surely hurts.'}),
}
Probably other stuff! But nobody cares, because nobody even read this far. WAIT. You're reading this far. Clearly, you're somebody – somebody awesome who actually has hair and is profoundly changing the world! It only goes to show you can't believe anything you read in a changelog anymore. 2025: "So even the changelogs lie now, huh?"
<sup>@beartype 0.21.0rc0: If you don't feel like a wild animal while coding, can it be called coding?</sup>
@beartype 0.21.0rc0 now officially supports all possible forms of recursion in type hints. This includes directly recursive PEP 695 type aliases, indirectly recursive PEP 484 generics, and @beartype-specific hint overrides. Which you prefer depends on which bitter pill you're willing to swallow:
If you're willing to require Python ≥ 3.12 as a mandatory dependency, prefer PEP 695 type aliases. They're concise. They're descriptive. They're elegant. They "just work" intuitively in the exact way you expect them to:
# Annotate recursive data structures with a "simple" one-liner. \o/
type RecursiveListExplodesYourApp = list[RecursiveListExplodesYourApp]
If you're unwilling to require Python ≥ 3.12 as a mandatory dependency, fallback to PEP 484 self-subscripted generics. They're unconcise. They're non-descriptive. They're inelegant. They require heavy lifting on your part before they start working. But they do work under Python ≥ 3.9, which is more than we can say for PEP 695:
# One line that makes sense (above) or five lines that don't make sense (below)?
# Let the cat decide. ¯\_(ツ)_/¯
from typing import TypeVar
T = TypeVar('T')
class GenericList(list[T]):
pass
GenericRecursiveListExplodesYourApp = GenericList[GenericList]
Let's take this one recursive app destroyer at a time.
<sup>@beartype 0.21.0rc0: this is the biggest animated gif i have ever seen</sup>
Directly recursive PEP 695 type aliases is what everybody who wants recursive type hints wants. Against all odds, you're actually reading this. You want recursive type hints. Thus, you want:
# Type hint matching an infinitely recursive list. Look, I don't know. This is for
# the extreme sports coders that like to live dangerously and code even harder.
type RecursiveListExplodesYourApp = list[RecursiveListExplodesYourApp]
# @beartype now type-checks this infinitely recursive list in O(1) time. Of
# course, that's impossible. But @beartype doesn't even care anymore! F- it!
from beartype import beartype
@beartype
def dangerous_func_means_well(oh_gods: RecursiveListExplodesYourApp) -> None:
'''
Dangerous function iteratively recurses into the passed infinitely recursive
list until either bottoming out at the same list... *or blowing up.*
Which do you hope happens first?
'''
# The growing terror you feel just speed-reading this code is real.
seen_horror_ids: set[int] = set()
unseen_horrors: list[RecursiveListExplodesYourApp] = list((oh_gods,))
# Recurse into that terror. Recurse until your face is numb, like mine.
while unseen_horrors:
# Pop that terror like a meat balloon. If it squishes, you must pop it.
seen_horror = unseen_horrors.pop()
print(f'Visiting infinitely recursive list: {id(seen_horror)}')
# Awaken from this logic nightmare, gentle reader!
if id(seen_horror) in seen_horror_ids:
print("Recursion detected! We're outta here, suckers!")
break
# Pretend this does what the docstring says this does. *gulp*
seen_horror_ids.add(id(seen_horror))
unseen_horrors.append(seen_horror[0])
# Infinitely recursive list. If you try this in the office, you may regret the
# fateful day your career decisions plummeted down a cliff.
OH_GODS = []
OH_GODS.append(OH_GODS)
# Pass this function valid input. Stare down the infinitely recursive list?
# Don't mind if I do! Gape in awe before infinity blows up your call stack.
dangerous_func_means_well(OH_GODS)
# Pass this function invalid input. No! No! Gods! NOOOOOOOOOOOOOOOOOOOOOOO!
dangerous_func_means_well(['Open-source', 'sells,' 'but', "who's", 'buying?'])
...which raises the expected output and exception traceback:
Visiting infinitely recursive list: 133179499204736
Visiting infinitely recursive list: 133179499204736
Recursion detected! We're outta here, suckers!
Traceback (most recent call last):
File "/home/leycec/tmp/mopy.py", line 48, in <module>
dangerous_func_means_well(['Open-source', 'sells,' 'but', "who's", 'buying?'])
~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<@beartype(__main__.dangerous_func_means_well) at 0x792044b9c360>", line 40,
in dangerous_func_means_well
beartype.roar.BeartypeCallHintParamViolation: Function __main__.dangerous_func_means_well() parameter oh_gods=['Open-source', 'sells,but',
"who's", 'buying?'] violates type hint RecursiveListExplodesYourApp, as list index 2
item str "who's" not instance of list.
Pore one out for the unsuspecting @beartype users that actually tried to run the above example. Their smoking CPUs are no longer with us. What remains of the ruin of their motherboards is now locked into a segfaulting bootloop featuring a cackling ASCII-art bear. It is sad.
<sup>@beartype 0.21.0rc0: "zomg so cuuuuuute oh my brain hurts nooooooooooooooo"</sup>
What? Catch? Surely you jest! There's no... oh, who am I kidding. There are huge catches associated with PEP 695. For one, @beartype intentionally does not support older PEP-noncompliant variants of recursive type hints that used stringified forward references. You might occasionally see crufty stuff like this floating around StackOverflow, older codebases, or the mypy issue tracker:
HorrifyingRecursiveTypeHint = Union[str, 'HorrifyingRecursiveTypeHint']
@beartype doesn't support that. Using stringified forward references to induce recursion is non-standard. @beartype probably could support that, but there's not much point in supporting non-standards when standardized alternatives exist. That's why...
@beartype 0.21.0rc0 only supports PEP 695: the only standard for defining recursive type hints. Everything else was just something mypy made up. Recursive type aliases now work wonderfully under Python ≥ 3.12 – but that's the gotcha here.
<sup>@beartype 0.21.0rc0: rambo with a sword is something that happened only on an alternate timeline... but it still happened</sup>
That's right. You love to hate it. PEP 695 is unusable under Python ≤ 3.11. Attempting to define any type alias under Python ≤ 3.11 results in CPython raising an unreadable "SyntaxError: invalid syntax" exception.
In a year or two, this will be significantly less of a hard blocker for everyone. Increasingly, nobody cares about Python ≤ 3.11. Do you care about Python ≤ 3.11? Maybe – but you probably shouldn't, unless your huge userbase is obsessed by Python ≤ 3.11. In that case, you're kinda screwed. You have to choose between your love for recursion and your love for having users. Tough choice. I'd choose recursion, personally.
<sup>beartype 0.21.rc0: users who hate recursion are users who make your face contort into a tight rictus of agony</sup>
Absolutely! Totally! How could anything else possibly go wrong!
...oh, who am I kidding!?!?!? There is yet another huge catch associated with PEP 695. @beartype does not deeply type-check recursive data structures to a countably infinite depth of nested recursion. Instead, @beartype:
Let's just accept this is happening. But why is this happening? Coupla reasons, fam:
O(1) time complexity. Deeply type-checking a recursive data structure with recursive height k would necessitate linear-time O(k) time complexity in @beartype – violating @beartype's fundamental efficiency guarantee.bad_list = []; bad_list.append(bad_list)). Of course, an iterative approach could be protected against these edge cases by dynamically generating type-checking code that maintains:
type alias to be type-checked, one set of the IDs of all previously type-checked objects. But now @beartype would need to allocate and append to one friggin' set for each recursive type alias for each function call. Space and time efficiency rapidly spirals into the gutter and then clutches its aching head like in a depressing Leaving Only Python ≥ 3.12!? Only one layer of recursion!?
<sup>@beartype 0.21.0rc0: let's get sweaty, together</sup>
Let's assume you hate requiring Python ≥ 3.12. You still love Python 3.9, even though nobody else does. You walk your own dark road. In this case, you want...
Self-subscripting generics, huh? You may now be thinking:
"But what does that even mean? How can a generic subscript itself? What even are generics? What does "subscription" mean? What does anything mean in a post-modern world of fluid subjectivity?"
Continue reading as you walk your own dark road.
Two months ago, ostensible typing genius @EtaoinWu (Yue Wu) invented indirectly recursive type hints at #510. It probably wasn't even an accident. @EtaoinWu probably did it on purpose. Some people are like that. They just like smashing things with their brain hammers until something finally gives. This is that thing.
In the darkness of my man-lair, I realized that @EtaoinWu's approach can be generalized to create indirectly recursive type hints under Python ≤ 3.11. Since Python ≤ 3.11 fails to support PEP 695 recursive type aliases, it was previously believed that recursive type hints could only be "officially" created under Python ≥ 3.12.
Not so. By abusing PEP 484 or PEP 585 generics, you can actually create recursive type hints under Python ≤ 3.11. These hints are fully PEP-compliant. They're valid. They satisfy typing standards. Much like me, however, they're also super weird. You'll frown at them when you see them awkwardly shuffling past you on the sidewalk. You'll also have no choice but to use them if you want to type recursive data structures under Python ≤ 3.11.
To induce recursion without directly defining a PEP 695-compliant recursive type alias, "simply":
Behold! This is indirect recursion via self-subscripting generics:
CAUTION: Merely reading this code abomination could cause your sanity to slip even further into the yawning abyss off the coast of California known only as R'lyeh.
# Import boring stuff you've long grown to loathe. Kinda sad, actually.
# Shouldn't boilerplate like this spark joy instead? I agree.
from beartype import beartype
from typing import TypeVar
# Define a normal unbound type variable. Enjoy it. This is the last normal
# one-liner you will ever see.
T = TypeVar('T')
# Define a normal PEP 585-compliant generic parametrized by that variable. Still
# normal. Still sane. Feels good. Yet, that feeling of unspeakable horror...
class GenericList(list[T]):
pass
# Define a type hint subscripting that generic... *BY ITSELF!?*
#
# Indeed. This type hint, for example, matches an infinitely recursive list
# (i.e., a list such that all items of this list are also infinitely recursive
# lists of the same type).
IndirectlyRecursiveList = GenericList[GenericList]
@beartype
def destroy_the_universe(with_danger_list: IndirectlyRecursiveList) -> None:
'''
Prove that @beartype will let you destroy the universe, if only your
intentions are pure.
'''
print(with_danger_list)
# Define an infinitely recursive non-empty list satisfying this type hint.
super_danger_list = IndirectlyRecursiveList()
super_danger_list.append(super_danger_list)
# Prove that @beartype accepts your risky life choices.
destroy_the_universe(super_danger_list)
# Define a boring non-recursive non-empty list violating this type hint.
super_boring_list = IndirectlyRecursiveList([
'Super', 'boring', 'list', 'hates', 'fun.'])
# Prove that @beartype rejects your safe life choices.
destroy_the_universe(super_boring_list)
...which prints the expected output and exception traceback:
[[...]]
Traceback (most recent call last):
File "/home/leycec/tmp/mopy.py", line 45, in <module>
destroy_the_universe(super_boring_list)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
File "<@beartype(__main__.destroy_the_universe) at 0x775b159d1440>", line 53, in destroy_the_universe
beartype.roar.BeartypeCallHintParamViolation: Function
__main__.destroy_the_universe() parameter
with_danger_list=['Super', 'boring', 'list', 'hates', 'fun.'] violates type hint
__main__.GenericList[__main__.GenericList], as generic superclass list[~T] of
<class "__main__.GenericList"> index 3 item str 'hates' not instance of
<class "__main__.GenericList">.
WOAH. The official repr() string for an infinitely recursive list generic is [[...]]. CPython just did that. We didn't do anything to make CPython do that. Somehow, that discovery is the coolest part of this whole changelog. I feel sad. :sob:
Voila! You've just created a recursive type hint that works under literally all Python versions – including Python ≤ 3.11. Nobody intended for anyone to do this. Thanks to the sickening force of the human mind, you can now do this.
Kinda surprised that nobody ever thought to subscript a generic by itself. Or did they!? Yeah... they probably did. But no @beartype users ever did that or somebody would have pounded their fists on our issue tracker about that. Or would they!? Yeah... they probably would. 😅 💦
<sup>@beartype 0.21.0rc0: these tears i shed for your code are manly</sup>
No idea. I care in the abstract sense of the word "care." Computer science is a super-fun literary puzzle with real-world implications – which makes it even funner than "normal" puzzles, which are still fun but don't touch the real world in any meaningful way. The lolz. That's what I'm saying. I did this for the lolz.
If you're reading this from the comfort of your PodBed™ in the Year 2075, please know that I did everything I could to make your life better. I solved puzzles. I meant well. Now, future human (or human-like AI construct), my future is your grim struggle for daily sustenance wondrous present in a utopian dream-world.
May this small piece of the recursive puzzle assist you in your own puzzle-wrangling.
<sup>@beartype 0.21.0rc0: because nobody tells you what to do anymore</sup>
Previously, @beartype hint overrides sorta but not really worked. Now, @beartype hint overrides actually do work for all possible use cases. Of course, I never got around to documenting hint overrides.
But don't let that sensible obstacle that should deter you deter you! Use undocumented APIs. Live a little. Let your dangling docstrings hang all out.
Lie to your userbase (and yourself) by globally replacing type hints without anyone's consent or knowledge. Not sure why anyone would want to behave like this, honestly. Therefore, @beartype allows you to behave like this. We support bad habits and so should you:
# Import tons of weirdo @beartype stuff. Look. I don't know either.
from beartype import beartype, BeartypeConf, FrozenDict
# Define a new @beartype decorator named @riskytype. Unlike @beartype, @riskytpe
# performs dangerous type hint overrides by replacing all "list[str]" type hints
# with "list[str] | tuple[str, ...]". Feels good. Users can now pass tuples of
# strings to all callables annotated as accepting only lists of strings. waaat?
riskytype = beartype(conf=BeartypeConf(hint_overrides=FrozenDict({
list[str]: list[str] | tuple[str, ...]})))
# Define a risky function decorated by @riskytype. *gulp*
@riskytype
def risky_func(risky_arg: list[str]) -> str:
return risky_arg[0]
# Prove that this function walks on the wild side.
print(risky_func(['This is fine.', 'You can tell because of my sweaty hand.']))
print(risky_func(('This is totally sus.', 'Users are panicking already.')))
# Prove that this function still occasionally behaves itself.
print(risky_func({'This is beyond sus...', '...where not even @beartype dares.'}))
...which prints the expected output and exception traceback:
This is fine.
This is totally sus.
Traceback (most recent call last):
File "/home/leycec/tmp/mopy.py", line 23, in <module>
print(risky_func({'This is beyond sus...', '...where not even @beartype dares.'}))
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<@beartype(__main__.risky_func) at 0x7370c39ce840>", line 48, in risky_func
beartype.roar.BeartypeCallHintParamViolation: Function __main__.risky_func()
parameter risky_arg={'This is beyond sus...', '...where not even @beartype dares.'}
violates type hint list[str], as set {'This is beyond sus...',
'...where not even @beartype dares.'} not list or tuple.
<sup>@beartype 0.21.0rc0: when you feel the need to suck on a bottle in the darkness as an afro ninja looks on in shock</sup>
In the now-legendary GitHub poll "Tell @leycec What to Do", everybody told @leycec to type-check PEP 557 dataclass fields on assignment. Thus, @beartype 0.21.0rc0 now type-checks dataclasses... sorta.
That's sorta right. Sorta means this mostly works, but might not. Type-checking dataclasses is hard. I am soft-bellied and lazy. After combining these adjectives, you get half-hearted dataclass type-checking.
@beartype only conditionally type-checks dataclass fields when you explicitly tell @beartype to type-check dataclass fields by enabling our newly introduced BeartypeConf(is_pep557_fields: bool = False) configuration option. For safety, this option is disabled by default. Ever since @beartype accidentally blew up PyTorch, your safety is our paramount concern. I can't have Microsoft breathing down my neckbeard again. Please! Not that...
When you want dataclass type-checking, you have to enable dataclass type-checking – like so:
# Import so many things you can barely see what matters anymore.
from beartype import beartype, BeartypeConf
from dataclasses import dataclass, InitVar
from typing import ClassVar
@beartype(conf=BeartypeConf(is_pep557_fields=True)) # <-- check it like a bear boss
@dataclass
class MuhDataclass(object):
muh_int: int
muh_classvar: ClassVar[str] = 'This is fine. Srsly. Does @leycec not have Buddha nature?'
muh_initvar: InitVar[bytes] = b'This is fine, too. No joke. No shade. No idea.'
# *GOOD.*
good_data = MuhDataclass(muh_int=42) # <-- this works
good_data.muh_int = 0xCAFEBABE # <------- this works, too
# *BAD.*
bad_data = MuhDataclass(muh_int=42) # <-- still works
bad_data.muh_int = '0xCAFEBABE' # <------ this fails! *YAY*!
...which raises the expected type-checking violation:
beartype.roar.BeartypeDecorHintParamDefaultViolation: Dataclass MuhDataclass(muh_int=42)
attribute 'muh_int' new value '0xCAFEBABE' violates type hint <class 'int'>,
as str '0xCAFEBABE' not instance of int.
As the above example demonstrates, this preliminary functionality supports cafe babes. Uhh... I mean, this supports:
@dataclass decorator passed no keyword parameters. :+1:@dataclass(frozen=True). :hand_with_index_finger_and_thumb_crossed:@dataclass(slots=True). :money_mouth_face:Truly, now you too can get Poor Man's Pydantic<sup>©</sup> for free from the comfort of your own AI-assisted keyboard while doing even less work than you ordinarily would while nursing a video game hangover on Sunday morning. @leycec did all the work for you and painfully regretted deeply cherished this character- and morale-building life lesson.
<sup>who did this to you, @beartype!?! oh, it was just dataclasses.</sup>
Thanks to the non-triviality of dataclasses and my own moral failings (read: "I am laziness incarnate"), this functionality currently fails to support all possible dataclass configurations and use cases. Popular edge cases not supported include:
Dataclass subclasses (i.e., dataclasses subclassing other dataclasses). This is completely untested. No idea what happens. Could blow up everything. Could do nothing, which is better than just blowing up everything.
PEP 563 (i.e., from __future__ import annotations), which almost certainly raises exceptions when enabling is_pep557_fields=True.
Dataclass fields annotated by one or more relative forward references (i.e., strings referring to the names of currently undefined types, subsequently defined in the current submodule), which almost certainly raises exceptions when enabling is_pep557_fields=True: e.g.,
from dataclasses import dataclass
@dataclass
class UnsupportedDataclass(object):
unsupported_field: 'UndefinedType' # <-- BREAKS EVERYTHING, YO
class UndefinedType(object): pass
Until @beartype fully supports all of the above edge cases, is_pep557_field will continue defaulting to False. Someday, this will surely work for everybody. Until then, let us collectively sob. 😭
<sup>@beartype 0.21.0rc0: teach a dev to crush bugs for a day and he'll crush bugs for a life</sup>
Python needs an official frozendict implementation, if only to shut down continual demands for an official frozendict implementation. Thankfully, you use @beartype.
@beartype 0.21.0rc0 now offers a public frozen dictionary type for you: beartype.FrozenDict! It actually works! It's mostly still C-based and thus fast! We tested everything and then some! We stuffed everything inside these things and they still pretended to work! We use frozen dictionaries everywhere in the @beartype codebase! Now, so can you!
beartype.FrozenDict: because Hell will freeze over before Python ever gets an official frozendict implementation.
from beartype import FrozenDict
# Finally, a set of frozen dictionaries! Yes, it's all true. Now you too can use
# dictionaries as dictionaries keys or set members. Why? Because you can.
freezing_my_dict_off = {
FrozenDict({'My ganglia!': 'It hurts.'}),
FrozenDict({'What even is a ganglia?': 'No idea. But it surely hurts.'}),
}
<sup>@beartype 0.21.0rc0: we heard you wanted some FrozenDict with your FrozenDict</sup>
...to financially feed @leycec and his friendly @beartype through our ancient GitHub Sponsors profile that predates the existence of dinosaur-like AI chatbots. 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. :musical_note: :musical_keyboard: :notes:
@beartype high-fives the reclusive secret society of worldwide bear bros who might possibly care about this. You are the select few. The elect enlightened. You are:
@posita, @wesselb, @tusharsadhwani, @JWCS, @patrick-kidger, @EtaoinWu, @iamrecursion, @Moosems, @langfield, @sylvorg, @mzealey, @thetianshuhuang, @RomainBrault, @ddorian, @rg936672, @alisaifee, @ArneBachmannDLR.
The burden of QA is high – but you have chosen to carry the smelly torch. Keep that suspiciously purple flame alive! The recursive data structure you crush the bugs out of tonight may very well be your own.
<sup>@beartype 0.21.0rc0 stands poetically before the burning wreckage of your competitor's codebase</sup>
@beartype 0.20.2 teleports seemingly from out of nowhere into the former safety of your Ungendered Person Cave™, where not even smelly bears that eat
@beartype 0.20.2 teleports seemingly from out of nowhere into the former safety of your Ungendered Person Cave™, where not even smelly bears that eat all your bugs are welcome:
pip install --upgrade beartype
@beartype 0.20.2 apologizes for the bald-faced transgressions of @beartype 0.20.1, which tried to resolve issue #512 for @rg936672 but mostly did nothing except sit around and read fantasy books. @beartype 0.20.2 would prefer it if @beartype 0.20.1 were never spoken of again. "What @beartype 0.20.1?", am I right?
@beartype 0.20.2 and your moral compass knows I'm right. Which is why...
@beartype 0.20.2 is brought to you by...
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.
<sup>The Masters of Fintech and Metrology. That's who.</sup>
It's better you not know about issues #512 or #514. In the land of the blind bug-crusher, ignorance is bliss and the one-eyed @beartype is king. By the overwhelming power of ignorance, no problems never existed. "What problems?", am I right?
Wait. Does "no problems never existed" actually mean "some problems always existed"? Double negatives destroy yet another convincing one-liner. Thanks fer nuffin', incomprehensible English language!
...to financially feed @leycec and his friendly @beartype through our ancient GitHub Sponsors profile that predates the existence of dinosaur-like AI chatbots. 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. :musical_note: :musical_keyboard: :notes:
Bear Club welcomes long-standing user @rg936672 to bathe in the lukewarm ambiance wafting off @beartype 0.20.2. It feels good. Therefore, it is good.
@beartype 0.20.1 materializes before your astonished keyboard with an explosive fizzy sizzling not unlike that of a flimsy soda can bursting its alumi
@beartype 0.20.1 materializes before your astonished keyboard with an explosive fizzy sizzling not unlike that of a flimsy soda can bursting its aluminum seams all over your astonished keyboard:
pip install --upgrade beartype
Because I am a bald middle-aged man, I still use pip. Someday I will update my priors and switch to uv. Someday... but not today. :face_holding_back_tears:
@beartype 0.20.1 brings a ton of fun stuff! It almost made the cut for a full-fledged minor @beartype 0.21.0 release. In the end, I couldn't bear the thought of another never-ending release candidate cycle. And neither could you. Which is why...
<sup>@beartype 0.20.1 is the shark in this metaphor. pretty sure that makes you the diver.</sup>
@beartype 0.20.1 is brought to you by...
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.
<sup>The Masters of Fintech and Metrology. That's who.</sup>
Let's start with the good stuff.
In the now-legendary GitHub poll "Tell @leycec What to Do", everybody told @leycec to type-check dataclasses. Thus, @beartype 0.20.1 now type-checks dataclasses... sorta.
Type-checking dataclasses is hard. I am soft-bellied and lazy. After combining these things, you get half-hearted dataclass type-checking. @beartype only conditionally type-checks dataclasses when you explicitly tell @beartype to type-check dataclasses by enabling our newly introduced BeartypeConf(is_check_pep557: bool = False) configuration option. For safety, this option is disabled by default.
When you want dataclass type-checking, you have to enable dataclass type-checking – like so:
from beartype import beartype, BeartypeConf
from dataclasses import dataclass, InitVar
from typing import ClassVar
@beartype(conf=BeartypeConf(is_check_pep557=True)) # <-- check it like a boss
@dataclass
class MuhDataclass(object):
muh_int: int
muh_classvar: ClassVar[str] = 'This is fine. Srsly. Does @leycec not have Buddha nature?'
muh_initvar: InitVar[bytes] = b'This is fine, too. No joke. No shade. No idea.'
# *GOOD.*
good_data = MuhDataclass(muh_int=42) # <-- this works
good_data.muh_int = 0xCAFEBABE # <------- this works, too
# *BAD.*
bad_data = MuhDataclass(muh_int=42) # <-- still works
bad_data.muh_int = '0xCAFEBABE' # <------ this fails! *YAY*!
...which raises the expected type-checking violation:
beartype.roar.BeartypeDecorHintParamDefaultViolation: Dataclass MuhDataclass(muh_int=42)
attribute 'muh_int' new value '0xCAFEBABE' value '0xCAFEBABE' violates type hint <class 'int'>,
as str '0xCAFEBABE' not instance of int.
As the above example demonstrates, this preliminary functionality supports standard dataclass fields annotated by either:
typing.ClassVar[...] type hints. 🥰dataclasses.InitVar[...] type hints. 😸<sup>who did this to you, @beartype!?! oh, it was just dataclasses.</sup>
Thanks to the non-triviality of dataclasses and my own moral failings (read: "I am laziness incarnate"), this functionality currently fails to support all possible dataclass configurations and use cases. Popular edge cases not supported include:
Frozen dataclasses (e.g., @dataclass(frozen=True)), which are currently guaranteed to raise exceptions when enabling is_check_pep557=True.
Slotted dataclasses (e.g., @dataclass(slots=True)), which are currently guaranteed to raise exceptions when enabling is_check_pep557=True.
Dataclass subclasses (i.e., dataclasses subclassing other dataclasses). This is completely untested. No idea what happens. Could blow up everything. Could do nothing, which is better than just blowing up everything.
PEP 563 (i.e., from __future__ import annotations), which almost certainly raises exceptions when enabling is_check_pep557=True.
Dataclass fields annotated by one or more relative forward references (i.e., strings referring to the names of currently undefined types, subsequently defined in the current submodule), which almost certainly raises exceptions when enabling is_check_pep557=True: e.g.,
from dataclasses import dataclass
@dataclass
class UnsupportedDataclass(object):
unsupported_field: 'UndefinedType' # <-- BREAKS EVERYTHING, YO
class UndefinedType(object): pass
Until @beartype fully supports all of the above edge cases, is_check_pep557 will continue defaulting to False. Someday, this will surely work for everybody. Until then, let us collectively sob. 😭
<sup>teach a dev to crush bugs for a day and he'll crush bugs for a life</sup>
Previously, @beartype only supported the core Click project for generating Pythonic CLIs and TUIs. Now, @beartype 0.20.1 extensibly supports the full Click ecosystem of Click-based projects – including the new BigBoy™, rich-click. Combine the venerable powers of Click + Rich. Truly, your app will own the terminal:
from beartype import beartype
from rich_click import command
@beartype # <-- actually works now. i know. i'm stunned, too.
@command()
def do_something_for_leycecs_sake() -> int:
return 0xFEEDFACE # Feed that face! Feed it good.
<sup>your silky smooth Rich TUI has never felt so good</sup>
That's it, folks! That's all we've got. It wasn't much, but it was still more than you wanted on a Friday morning. This has been @beartype 0.20.1. Thanks so much for having us. If anyone needs us, we'll be playing video games and listening to Finnish viking metal all night. :metal:
...to financially feed @leycec and his friendly @beartype through our ancient GitHub Sponsors profile that predates the existence of dinosaur-like AI chatbots. 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. :musical_note: :musical_keyboard: :notes:
Bear Club welcomes long-standing users @RomainBrault, @rg936672, and @GithubCamouflaged to bask in the code-warming radiance that is @beartype 0.20.1.
<sup>don't by shy! come on in. the water's warm. there's more than enough QA to go around...</sup>
…LEGO Millennium Falcon constructed entirely from deprecated type hints. Now, it's dead. Only my baldness remains. Python makin' me feel old over here.
Beartype 0.20.0 catapults out of your CRT monitor from the 90's that contains only four pounds of lead, across your mechanical keyboard with the clacky keys and vibrant crumb trails, and into your lap. Startled, you shriek in fear. Then astonishment. Then back to fear. Trembling, your fingers reach for...
pip install --upgrade beartype # <-- bugger the bugs
Caveat Emptor: What follows is a lazy copy-paste of our prior changelog for Beartype 0.20.0 Release Candidate 0. Therefore, this is already boring the snot out of you. That's a good thing. Laziness is the root of all QA stability.
@beartype 0.20.0 gurgles contentedly as you wipe the birthing fluids from its forehead. You choke back tears. In a moment of madness that borders on the divine, you pull the trigger. You update. You're in too deep now. The sunk cost fallacy you feel is real. Your devbox shudders. Your mechanical keyboard spills sugary soda all over itself. Your RSI-wracked mouse hand pulsates with the pre-install jitters. Surely the second coming of QA is at hand.
<sup>@beartype 0.20.0: and you thought Gymnastics Turtle was weird</sup>
@beartype 0.20.0 is brought to you by...
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.
<sup>The Masters of Fintech and Metrology. That's who.</sup>
And now for something fishy.
Absolutely! Well, surely. Well, probably. Well, possibly. Well, maybe.
@beartype 0.20.0 brings to bear a whole new lineup of QA goodies. You never knew you needed these QA goodies until a small voice inside of you whispered:
"You do."
Before we get to some actual meaningful content in this changelog, though...
<sup>Blast Hardcheese isn't to blame for @beartype 0.20.0's litany of questionable new "features"... but your coworker might be</sup>
Left to my own devices, I just play video games and obsessively rummage around in the bike shed with PEP standards nobody cares about. This is why...
I implore you to vote in this GitHub forum poll. Yes. It's all true. User opinions always mattered, but now user opinions really matter. I can't pretend they don't exist anymore. For the first time ever, you can force me to do your questionable and unsavoury bidding. Tell me what to do this summer. Vote now. Vote often. Vote hard. But for the sake of the bugs crushing your workload, vote. :full_moon_with_face:
<sup>a vote for bob evil is a vote for @beartype 0.21.0</sup>
Iterable[...] + Container[...] = BBFFLs (Best Bear Friends for Life)@beartype now deeply type-checks the last remaining PEP 484- and 585-compliant container type hints in O(1) constant time, including:
collections.abc.Container[...] type hints.collections.abc.Iterable[...] type hints.collections.abc.Reversible[...] type hints.typing.Container[...] type hints.typing.Iterable[...] type hints.typing.Reversible[...] type hints.@beartype now type-checks exactly one item of containers annotated with general-purpose container type hints like Iterable[...] and Container[...] in O(1) constant time. How? Smarty bear pants. @beartype intelligently detects whether a container is:
list[str]), in which case a pseudorandom item of that
sequence is type-checked.set[int]), in which case only the first item of that
collection is type-checked.‼ Plum users may now dispatch on all of the above.
collections.abc.Iterable[...]is the Big One™, of course. This is the profit of depending on @beartype. You wait years for @leycec to do something. Finally, @leycec does a thing. Your code works. Users cheer. It is delicious.
<sup>beartype 0.20.0 isn't laughing. beartype 0.20.0 doesn't even know what's happening anymore...</sup>
@beartype now deeply type-checks PEP 484-compliant type variables (e.g., T = typing.TypeVar('T')) for a variety of common use cases. Mostly, this means type variables whose type-checking can be entirely decided at decoration time by the @beartype decorator.
Since deciding type-checking at decoration time is really fast, deeply type-checking type variables in these use cases is really fast as well. Like, O(1) fast. Like everything @beartype does, these type variables are cost-free. They don't cost anything, so you'd might as well use them. This probably marks the first serious attempt by any package to tackle type variables at runtime.
This support fully covers these common use cases:
MuhList[int] given the type class MuhList[T](list[T]): ...).MuhAlias[int] given the type alias type MuhAlias[T] = list[T] | T | int).Let's take a swan dive into the deep end of...
@beartype now propagates child hints up generic type hierarchies. Because @beartype values the cooperation of coworkers you barely convinced to use @beartype in the first place, @beartype propagates child hints efficiently, recursively, and (most importantly) safely. No generics are harmed in the propagation of child hints.
The proof is in the syntactically highlighted rainbow pudding. In this example, we bring the swift fist of justice to Python QA. First, we define a generic parametrized by a type variable T. Next, we annotate a function with a type hint created by subscripting that generic with the child hint int. Finally, @beartype does the rest by propagating that child hint int into that type variable T up the type hierarchy of that generic.
Behold! Boredom personified, yet you can't turn your lidded eyes away:
from beartype import beartype
from collections.abc import Container, Iterable, Iterator, Sequence
# Define a PEP 585-compliant generic satisfying both the
# "collections.abc.Iterable" and "Container" abstract base classes (ABCs). W00t!
@beartype
class SoDumbSoDelicious[T](Iterable[T], Container[T]):
def __init__(self, sequence: Sequence[T]) -> None:
self._sequence = sequence
def __contains__(self, obj: object) -> bool:
return obj in self._sequence
def __iter__(self) -> Iterator[T]:
return iter(self._sequence)
def __len__(self) -> int:
return len(self._sequence)
# Define a function accepting an instance of this generic constrained to contain
# *ONLY* integers. Yuppers. It's true. @beartype actually type-checks this now.
@beartype
def tastes_like_lard(yum: SoDumbSoDelicious[int]) -> int:
return next(iter(yum))
# Define delectable instances of this generic. Prove this isn't just crazy-talk!
love_me_some_lard = SoDumbSoDelicious((0xCAFE, 0xBABE))
gods_no_more_lard = SoDumbSoDelicious(('Cafe', 'Babe!'))
# Assert that calling this function with a valid parameter returns the
# expected value. You are the cafe. You are the lard. You knew you shouldn't
# have read that, but you kept on doing it. The punchline so wasn't worth it.
assert tastes_like_lard(love_me_some_lard) == 0xCAFE # <-- oh not that cafe
# Call this function with an invalid parameter, which then raises a
# type-checking violation. May the Gods strike me down if lard is bad for you!
tastes_like_lard(gods_no_more_lard) # <-- ...you will eat lard and like it
...which raises the expected type-checking violation:
Traceback (most recent call last):
File "/home/leycec/tmp/mopy.py", line 35, in <module>
tastes_like_lard(gods_no_more_lard) # <-- ...you will eat lard and like it
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
File "<@beartype(__main__.tastes_like_lard) at 0x7fce8b0828e0>", line 89, in tastes_like_lard
beartype.roar.BeartypeCallHintParamViolation: Function
__main__.tastes_like_lard() parameter yum=<__main__.SoDumbSoDelicious object at
0x7fce8b35d6d0> violates type hint __main__.SoDumbSoDelicious[int], as generic
superclass collections.abc.Iterable[T] of <class "__main__.SoDumbSoDelicious">
index 0 item str 'Cafe' not instance of int.
In other words, @beartype now "knows" that any object typed as SoDumbSoDelicious[int] should be an iterable container whose items are all integers. Under the furry hood, @beartype "knows" this by recursively replacing each instance of the type variable T with the child hint int within the generic type hierarchy of SoDumbSoDelicious .
In other words, @beartype now correctly propagates mappings from type variables parametrizing generic declarations (e.g., the T in class SoDumbSoDelicious[T](Iterable[T], Container[T]):) to the child hints subscripting usage of those generics (e.g., the int in SoDumbSoDelicious[int]). @beartype does this recursively for arbitrarily complex generic type hierarchies.
This broke my brain and took two full months of mostly unpaid volunteerism. That's why I still beg for money on GitHub Sponsors like that hobo chugging Duck wine out of a cardboard box on your commute to work every day. Like the hobo, I had fun. Like the hobo, I had the urge to sleep in the gutter. This feature was so hard I had to refactor the entire @beartype codebase to support it – including @beartype's totally-not-fragile dynamic type-checking code generator that I avoid touching at all costs. That's how totally-not-fragile it is. It's the sort of poorly documented 10,074-line code dump you see prefixed with stultifying ASCII art banners like:
######### LUCK DRAGON EATS YOUR LUNCH, THEN HICCUPS?!? #########
# Here thar be dragons, code matey. Arr. *hiccup* #
# ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣤⣤⣤⣤⡼⠀⢀⡀⣀⢱⡄⡀⠀⠀⠀⢲⣤⣤⣤⣤⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ #
# ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⣾⣿⣿⣿⣿⣿⡿⠛⠋⠁⣤⣿⣿⣿⣧⣷⠀⠀⠘⠉⠛⢻⣷⣿⣽⣿⣿⣷⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀ #
# ⠀⠀⠀⠀⠀⠀⢀⣴⣞⣽⣿⣿⣿⣿⣿⣿⣿⠁⠀⠀⠠⣿⣿⡟⢻⣿⣿⣇⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣟⢦⡀⠀⠀⠀⠀⠀⠀ #
# ⠀⠀⠀⠀⠀⣠⣿⡾⣿⣿⣿⣿⣿⠿⣻⣿⣿⡀⠀⠀⠀⢻⣿⣷⡀⠻⣧⣿⠆⠀⠀⠀⠀⣿⣿⣿⡻⣿⣿⣿⣿⣿⠿⣽⣦⡀⠀⠀⠀⠀ #
# ⠀⠀⠀⠀⣼⠟⣩⣾⣿⣿⣿⢟⣵⣾⣿⣿⣿⣧⠀⠀⠀⠈⠿⣿⣿⣷⣈⠁⠀⠀⠀⠀⣰⣿⣿⣿⣿⣮⣟⢯⣿⣿⣷⣬⡻⣷⡄⠀⠀⠀ #
# ⠀⠀⢀⡜⣡⣾⣿⢿⣿⣿⣿⣿⣿⢟⣵⣿⣿⣿⣷⣄⠀⣰⣿⣿⣿⣿⣿⣷⣄⠀⢀⣼⣿⣿⣿⣷⡹⣿⣿⣿⣿⣿⣿⢿⣿⣮⡳⡄⠀⠀ #
# ⠀⢠⢟⣿⡿⠋⣠⣾⢿⣿⣿⠟⢃⣾⢟⣿⢿⣿⣿⣿⣾⡿⠟⠻⣿⣻⣿⣏⠻⣿⣾⣿⣿⣿⣿⡛⣿⡌⠻⣿⣿⡿⣿⣦⡙⢿⣿⡝⣆⠀ #
# ⠀⢯⣿⠏⣠⠞⠋⠀⣠⡿⠋⢀⣿⠁⢸⡏⣿⠿⣿⣿⠃⢠⣴⣾⣿⣿⣿⡟⠀⠘⢹⣿⠟⣿⣾⣷⠈⣿⡄⠘⢿⣦⠀⠈⠻⣆⠙⣿⣜⠆ #
# ⢀⣿⠃⡴⠃⢀⡠⠞⠋⠀⠀⠼⠋⠀⠸⡇⠻⠀⠈⠃⠀⣧⢋⣼⣿⣿⣿⣷⣆⠀⠈⠁⠀⠟⠁⡟⠀⠈⠻⠀⠀⠉⠳⢦⡀⠈⢣⠈⢿⡄ #
# ⣸⠇⢠⣷⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠻⠿⠿⠋⠀⢻⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⢾⣆⠈⣷ #
# ⡟⠀⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣶⣤⡀⢸⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⡄⢹ #
# ⡇⠀⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠈⣿⣼⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠃⢸ #
# ⢡⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⠶⣶⡟⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡼ #
# ⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡾⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁ #
# ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡁⢠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ #
# ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣼⣀⣠⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ #
# Dragons ahoy, code matey. Swab the Steam Deck. Arr. #
######### ASCII ART MEANS SWEAT, TEARS, AND CI FAILURE #########
It's likely there are unresolved edge cases I'm blissfully unaware of. If you hit one of these golden landmines, please submit an issue. I'll promptly resolve everything or collapse on my keyboard trying. :cold_sweat:
‼ Plum users may now dispatch on subscripted generics. This is the profit of depending on @beartype. You wait years for @leycec to do something. Finally, @leycec does a thing. Exhaustedly, you pump your hand in the sign of victory. You are vindicated... yet you feel empty.
<sup>awkward life moments (brought to you by @beartype)</sup>
@beartype now propagates child hints up generic type aliases, too. This is genuinely cool. Like, "cool kids" cool. Generic type aliases are kinda like functions or a full-blown templating engine – except for type hints. If you've ever found yourself copy-pasting one stupidly long type hint into another, you've realized you're violating the Don't Repeat Yourself (DRY) principle and are now off the deep end of QA. Thus:
# Instead of copy-pasting hellish type hints surely spawned in the Ninth Circle
# like this...
PygmentsStringToken = list[typing.Union[
tuple[str | collections.abc.Callable[typing.Concatenate[object, object, ...], object], ...],
tuple[str | pygments.token._TokenType[str], ...],
typing.Annotated[collections.abc.Collection[str], beartype.vale.IsInstance[pygments.lexer.include]],
]]
PygmentsBytesToken = list[typing.Union[
tuple[bytes | collections.abc.Callable[typing.Concatenate[object, object, ...], object], ...],
tuple[bytes | pygments.token._TokenType[bytes], ...],
typing.Annotated[collections.abc.Collection[bytes], beartype.vale.IsInstance[pygments.lexer.include]],
]]
# ...just define a single generic type alias like this...
type PygmentsToken[T] = list[typing.Union[
tuple[T | collections.abc.Callable[typing.Concatenate[object, object, ...], object], ...],
tuple[T | pygments.token._TokenType[T], ...],
typing.Annotated[collections.abc.Collection[str], beartype.vale.IsInstance[pygments.lexer.include]],
]]
# ...then subscript that alias with child hints. Look, code metrics! No DRY.
PygmentsStringToken = PygmentsToken[str]
PygmentsBytesToken = PygmentsToken[bytes]
In this example, PygmentsToken may not look like a function – but it pretty much is. It's a "function" that generates a new type hint from a standardized template every time you subscript it with a new child hint.
So what's the catch? We hope you don't mind requiring Python ≥ 3.12 by dropping support for Python ≤ 3.11. Because... that's the catch. If you try to define even a single type alias in a single submodule of your package under Python ≤ 3.11, your entire app unceremoniously blows up with a fatal SyntaxError: invalid syntax exception. Good luck with that.
@beartype 0.20.0: because your users hate Python ≤ 3.11, too.
<sup>barbarian code never got type-checked by @beartype</sup>
I don't even know what "thrumming" is, but I'm pretty sure that's what happens to your sinews when you combine the fearsome power of subscripted generics and subscripted type aliases. The coworker to your right is already cowering. Good. That feeling is good. Soon, they will all kneel! </muhaha— *choking*>
Behold! A subscripted type alias propagating its child hint onto a subscripted generic. Why? Because your QA pipeline wasn't complicated enough and you now need to justify your position to those new bastards in suits nice HR spokespeople:
from beartype import beartype
from collections.abc import Container, Iterable, Iterator, Sequence
# Define the same PEP 585-compliant generic as above. Booooooooring.
@beartype
class SoDumbSoDelicious[T](Iterable[T], Container[T]):
def __init__(self, sequence: Sequence[T]) -> None:
self._sequence = sequence
def __contains__(self, obj: object) -> bool:
return obj in self._sequence
def __iter__(self) -> Iterator[T]:
return iter(self._sequence)
def __len__(self) -> int:
return len(self._sequence)
# Define a PEP 695-compliant type alias unifying this generic with other stuff.
# Look... *I* don't know. You're the genius here. You'll defly get a raise at
# work if you keep pushing out quality code like this.
type MaybeSoDumbSoDelicious[T] = T | MaybeSoDumbSoDelicious[T] | None
# Define a function accepting either an integer, an instance of this generic
# constrained to contain *ONLY* integers, or "None". @beartype type-checks this,
# because @beartype has no say in the matter. @beartype has to do what you say.
# This is why @beartype feels great sadness in the Autumn.
@beartype
def maybe_tastes_like_lard(yum: MaybeSoDumbSoDelicious[int]) -> int:
return next(iter(yum))
# Define delectable instances of this generic. Prove this isn't just crazy-talk!
love_me_some_lard = SoDumbSoDelicious((0x1331, 0xC001))
gods_no_more_lard = SoDumbSoDelicious(('Leet', 'Cool!'))
# Assert that calling this function with a valid parameter returns the
# expected value. You are the cafe. You are the lard. You knew you shouldn't
# have read that, but you kept on doing it. The punchline so wasn't worth it.
assert maybe_tastes_like_lard(love_me_some_lard) == 0x1331 # <-- oh not that cafe
# Call this function with an invalid parameter, which then raises a
# type-checking violation. May the Gods strike me down if lard is bad for you!
maybe_tastes_like_lard(gods_no_more_lard) # <-- ...you will eat lard and like it
...which raises the expected type-checking violation:
Traceback (most recent call last):
File "/home/leycec/tmp/mopy.py", line 42, in <module>
maybe_tastes_like_lard(gods_no_more_lard) # <-- ...you will eat lard and like it
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
File "<@beartype(__main__.maybe_tastes_like_lard) at 0x7f89162fa8e0>", line 94, in maybe_tastes_like_lard
beartype.roar.BeartypeCallHintParamViolation: Function
__main__.maybe_tastes_like_lard() parameter yum=<__main__.SoDumbSoDelicious
object at 0x7f89165c5810> violates type hint MaybeSoDumbSoDelicious[int], as
<class "__main__.SoDumbSoDelicious"> <__main__.SoDumbSoDelicious object at
0x7f89165c5810>:
* Not int or <class "builtins.NoneType">.
* Generic superclass collections.abc.Iterable[T] of <class
"__main__.SoDumbSoDelicious"> index 0 item str 'Leet' not instance of int.
...which only goes to show that @beartype now knows everything. Lo! It is known.
<sup>@beartype is getting bad vibes from trumpy</sup>
One ill-fated morning, you realise with a gnawing horror... You've run out of coffee. The Pop-Tarts® box is empty. There's no reason to get up anymore. You greet the watery Sun with a thousand-yard stare half-baked beyond the void of space. Yet, through crusty eyes, you think to yourself:
Can I compare whether one subscripted generic is a subhint of (i.e., both commensurable with and narrower than) another subscripted generic? If I can, why would I want to do that? Is this what life without coffee and Pop-Tarts® is like for billions around the world even today!?!?
I don't know why, but unspecified people <sup>okay, it's @patrick-kidger and @wesselb</sup> want to compare subscripted generics. I smell academia and a burgeoning career built on the shuddering back of @beartype. You might be one of these people or a person like these people. But, let's be honest... you're probably not. You're wondering why you're still reading this and rapidly realizing there's no good answer to that question. Some questions are bad.
Let's pretend you're one of these people. Rejoice! @beartype now decides the least trivial computational puzzle in the field of type systems, because @leycec couldn't bear to see that unresolved [Bug] status for another year:
>>> from beartype.door import is_subhint # <-- it's baaaaaaack
>>> from collections.abc import Sequence
>>> from typing import Generic, TypeVar
# Define some type variables like it's 2017.
>>> S = TypeVar('S')
>>> T = TypeVar('T')
>>> T_sequence = TypeVar('T_sequence', bound=Sequence)
# Define some parametrized generics. Pretend this means something.
>>> class GenericST(Generic[S, T]): pass
>>> class GenericSInt(Pep484GenericST[S, int]): pass
# Decide whether these generics are related or not, especially when subscripted
# by child hints. You don't know why you want to decide this. You just do.
# You've gotta. You're compelled by inner demons to go beyond the pale
# crenellations of normalcy, beyond even the liminal subspaces of QA. WUT?!?
>>> is_subhint(Pep484GenericSInt, Pep484GenericST)
True # <-- ...okay
>>> is_subhint(Pep484GenericSInt, Pep484GenericST[int, int])
False # <-- sure, buddy
>>> is_subhint(Pep484GenericSInt[int], Pep484GenericST)
True # <-- i'm not your buddy, pal
>>> is_subhint(Pep484GenericSInt[int], Pep484GenericST[S, T_sequence])
False # <-- whatever you say, bear boss
>>> is_subhint(Pep484GenericSInt[list], Pep484GenericST[T_sequence, object])
True # <-- pretty sure i no longer know what's happening
>>> is_subhint(Pep484GenericSInt[list], Pep484GenericST[Sequence, Any])
True # <-- seems legit if i squint at it
>>> is_subhint(Pep484GenericSInt[str], Pep484GenericST[T_sequence, S])
True # <-- it's too good to be true
>>> is_subhint(Pep484GenericSInt[Sequence], Pep484GenericST[list, object])
False # <-- finally, a falsehood exposed
>>> is_subhint(Pep484GenericSInt[T_sequence], Pep484GenericST)
True # <-- yeah, right, @beartype! as if
I'm confident you could profitably author multiple doctoral theses strung out across multiple poorly remunerated grad students who hate themselves almost as much as you do by just addressing, redressing, and endlessly rehashing this single issue. Papermill careers are built on the boneless backs of issues like this.
Originally, I wanted to bludgeon everyone in attendance with a tiresome essay doing just that. In the bitter end, even the mere thought of tiring everyone tires me beyond the event horizon of exhaustion.
Still, boredom is its own reward. I'll say that @beartype has probably invented the optimally efficient algorithm for deciding this problem. Nobody cares, of course. Even I am lacklustre about this whole thing.
But a non-recursive depth-first search (DFS) is a glorious self-flagellation that few ever attempt and even fewer survive. This DFS exhibits:
O(1) constant time complexity. Pump that fist!O(jk) quadratic time complexity, where:
j is the largest number of child type hints transitively subscripting an unerased pseudo-superclass of the first generic passed to is_subhint(). You don't even want to know what "unerased pseudo-superclass" means.k is the total number of transitive pseudo-superclasses of the same generic. Ditto.Because the DFS is non-recursive, it's stupidly fast, unreadable, undebuggable, and unmaintainable. This is why we @beartype:
...so that all the suffering is concentrated in one place.
<sup>left: parametrized generics. right: ...is that @leycec?!?</sup>
Of course, @leycec is lazy. @beartype still lacks general-purpose support for type-checking type variables at call time. This means @beartype still ignores type-checking violations involving mismatching types across type variables like:
def muh_func[T](muh_arg: T) -> list[T]:
return ['this is busted', "but you'll never know", 'cause @beartype dumb.']
# Beartype should raise a type-checking violation here, as the list returned by
# this function is a list of strings rather than a list of integers. Sadly,
# beartype currently thinks this is fine and does nothing, much like our cats.
muh_func(0xDEADCODE)
@beartype 0.20.0: shrugging apathetically while your code burns
<sup>the answer may shock you</sup>
@beartype now tolerates these extremely popular third-party packages that hate @beartype ≤ 0.19.0 (and other runtime type-checkers, too):
urllib3.xarray.Yes, these packages hate @beartype (and other runtime type-checkers, too). The irony is especially rich in Pydantic's case, because Pydantic itself is a runtime type-checker. This must be what happens when you go full-Rust.
These packages all doubled down on the typing.TYPE_CHECKING forward reference antipattern, which @beartype 0.21.0 will have a lot to say about. Until then, this is "Leycec's Abbreviated Notes on the Antipattern That Destroys True Goodness and Heroism":
# This is what PEP 484, PEP 563, and @beartype all want you to do. Forward
# referencing external types defined in other packages is easy, fam:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import some_package
def muh_func(some_arg: 'some_package.some_submodule.SomeType'): ... # <-- good!
# Instead, this is what Pydantic, "urllib3", and "xarray" are all doing. This is
# the "TYPE_CHECKING" forward reference antipattern:
if TYPE_CHECKING:
from some_package.some_submodule import SomeType
def muh_func(some_arg: 'SomeType'): ... # <-- *BAD*! gag me with a spork, Mork
Those two approaches may look identical. From the runtime perspective, those two approaches share nothing in common. They hate one other. TYPE_CHECKING evaluates to False at runtime, so @beartype (and other runtime type-checkers) can't see the imports hidden inside those if conditionals. All @beartype sees is:
'some_package.some_submodule.SomeType' in the former case. @beartype can fully resolve the SomeType type from this. @beartype is pleased and growls contentedly while rubbing its belly for scritches.'SomeType' in the latter case. That's... just not enough information. Like, at all. Throw @beartype a friggin' bone. @beartype can't resolve anything from that! @beartype growls and throws up.@beartype 0.21.0 will explicitly detect, warn about, and repair this antipattern across all beartype.claw import hooks. That's the glorious future. For now, @beartype 0.20.0 contents itself with just silently ignoring these problematic packages in beartype.claw import hooks. We do what we can. Sometimes, it isn't much.
@beartype doesn't hate these packages. We try to be tolerant of everyone's misinformed and bad opinions. In this case, we tolerate these packages by internally blacklisting them. We don't bother trying to subject their modules, types, or callables to runtime type-checking, because we can't. Their modules, types, and callables despise type-checking. What can you do? Nuthin'. We didn't make crazy; we can't control crazy; we just put crazy in a head lock and roll over it multiple times with our adipose-laden bodies until it stops moving.
Blacklisting these packages really improves the real-world usability of the beartype.claw.beartype_all() import hook in particular, which previously choked on imports from these packages. Since @beartype now automatically blacklists these packages, you no longer need to manually blacklist them yourself by listing these packages under the BeartypeConf(claw_skip_package_names=...) configuration option: e.g.,
from beartype.claw import beartype_all
# This default call to beartype_all()...
beartype_all()
# ...is now equivalent to this complicated logic, kinda. Yay!
#from beartype import BeartypeConf
#beartype_all(conf=BeartypeConf(claw_skip_package_names=(
# 'pydantic',
# 'urllib3',
# 'xarray',
#)))
@beartype 0.20.0: make all the bad stuff go away, QA daddy.
<sup>lol</sup>
@beartype 0.20.0 officially drops support for Python 3.8. Python 3.8 "recently" hits its official End-of-Life (EOL). Alright. Okay. It was ages ago, wasn't it? I still remember when Python 3.8 was the cool new kid who just wanted to come over and show you his $1,000 LEGO Millennium Falcon constructed entirely from deprecated type hints. Now, it's dead. Only my baldness remains. Python makin' me feel old over here.
This means that Python 3.8 now constitutes a security risk. More importantly, @beartype hates Python 3.8. Due to the Transitivity of Loathsomeness Principle (probably discovered by Pythagoras the Pythonic, the little-known balding stepchild of Pythagoras the Elder) your codebase now hates Python 3.8 too. Like a bad dream, feature loss is contagious.
<sup>$1000 bucks and 47 weeks of your youth: gone, just like @leycec when the issues pile up</sup>
@beartype 0.20.0 delivers less bugs – a lot less bugs. Turns out @beartype has been buggy for years. If nobody hits a bug for a decade but that bug still exists, does it make a sound when it crushes your codebase at 4:23AM on an icy Sunday? The answer is: "That sound is your dev team screaming in shared anguish."
@beartype 0.20.0 specifically:
from __future__ import annotations) with PEP 695 implicit type parameter instantiation (e.g., def muh_func[T](muh_arg: T) -> T: ...). Against all sanity, this somehow now works:from __future__ import annotations # <-- PEP 563, yo. it sucks, but you know best.
from beartype import beartype # <------- *GOOD*
@beartype
def muh_func[T](muh_arg: T) -> T: # <-- PEP 695 up in here, y'all!
return muh_arg
type aliases like you just don't care:from beartype import beartype # <------- *GOOD*
# PEP 695 type aliases, deeply nested because you no longer care about coworkers.
type ThatFeeling[T] = WhenYoureIn[T] | float
type WhenYoureIn[T] = TooDeep[T] | str
type TooDeep[T] = int | T
@beartype
def nuther_func(nuther_arg: TooDeep[complex]) -> TooDeep[bytes]:
return nuther_arg # <-- a type-check that is doomed to fail. it ain't so good
The above function signature is equivalent to this simpler, more readable, more maintainable, more debuggable, and yet somehow more boring signature:
@beartype
def nuther_func(nuther_arg: int | complex | str | float) -> int | bytes | str | float:
return nuther_arg # <-- we can now see this makes less sense than we thought
Boring is bad, though. That's clear. Simplicity doesn't count for much if you're bored all the time. Maximize non-boring even if it costs you your codebase, your career, and your future prospects of a happy family. Do what @leycec would do.
@beartype 0.20.0: because @beartype has to do what you say, even when it no longer wants to
<sup>the last thing your code will say before users cry on reddit</sup>
...to financially feed @leycec and his friendly @beartype through our ancient GitHub Sponsors profile that predates the existence of dinosaur-like AI chatbots. 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. :musical_note: :musical_keyboard: :notes:
...but nobody asked to leave, either. The Bear Beta Fan Club is GitHub's own Hotel California. The "Exit!" sign is poorly labelled. You keep getting roped back in with dubious reassurances that "things will be better this time."
This is that time.
@posita, @wesselb, @iamrecursion, @patrick-kidger, @langfield, @JelleZijlstra, @RobPasMue, @GithubCamouflaged, @kloczek, @uriyasama, @danielgafni, @JWCS, @rbroderi, @AlanCoding, @tvdboom, @crypdick, @jvesely, @komodovaran, @kaparoo, @MaximilienLC, @fleimgruber, @EtaoinWu, @alexoshin, @gabrieldemarmiesse, @James4Ever0, @NLPShenanigans, @rtbs-dev, @yurivict, @st--, @murphyk, @dosisod, @Rogdham, @alisaifee, @denisrosset, @damarro3, @ruancomelli, @jondequinor, @harshita-gupta, @jakebailey, @denballakh, @jaanli, @creatorrr, @msvensson222, @avolchek, @femtomc, @AdrienPensart, @jakelongo, @Artur-Galstyan, @ArneBachmann, @danielward27, @WeepingClown13, @rbnhd, @radomirgr, @rwiegan, @brettc, @spagdoon0411, @helderco, @paulwouters, @jamesbraza, @dcharatan, @kasium, @AdrienPensart, @sunildkumar, @peske, @mentalisttraceur, @awf, @PhilipVinc, @dcharatan, @empyrealapp, @rlkelly, @KyleKing, @skeggse, @RomainBrault, @deepyaman, @mzealey, @adamtheturtle, @Moosems, @minmax, @jedie, @pablovela5620, @thiswillbeyourgithub, @Logan-Pageler, @knyazer, @ilyapoz, @yuzhichang, @Fedezzab, @antonioan, @im-Kitsch, @mthramann, @fbartolic, @rgallardone, @frrad, @jonnyhyman, @jennydaman, @likewei92, @acec2127, @Glinte, @rudimichal, @woutdenolf, @PauloHMTeixeira
Beartype 0.20.0 Release Candidate 2 is... my Gods. Didn't I just do this two days ago? In hindsight, I should've waited two days. This had better be t
Beartype 0.20.0 Release Candidate 2 is... my Gods. Didn't I just do this two days ago? In hindsight, I should've waited two days. This had better be the final release of the @beartype 0.20.0 release cycle, because nobody wants to do this anymore. Let's goooooooooooooooooooo:
pip install --upgrade --pre beartype # <-- bugs begone
This final release candidate is entirely thanks to the tireless, peerless, and fearless Django wrangler @rudimichal (Michał Rudziński). He bled sweaty tears for this over at issue #488, where three weeks of haggling over objectively unreadable Django stacktraces culminated in... "this." It's best not to ask what "this" means. Just know you don't want to know. Some issue resolutions are best left unsung. Not all GitHub avatars wear capes.
Thanks again, @rudimichal! @beartype 0.20.0rc2 is for Django, the bugs we crushed along the way, and our shared sufferings.
<sup>aaaaaaaaaaand... We done.</sup>
…LEGO Millennium Falcon constructed entirely from deprecated type hints. Now, it's dead. Only my baldness remains. Python makin' me feel old over here.
Beartype 0.20.0 Release Candidate 1 gently rises aloft. The frigid storm blast dumping fifty litres of snow directly onto our cottage is no match for @beartype 0.20.0rc1. Like a hot-air balloon fuelled on hopes and dreams alone, @beartype 0.20.0rc1 asks the critical question: "How hopeful is your codebase that any of this actually works?"
Let our contagious dream of a higher-quality Python world infect your dreams, too. There's room enough on this hot-air balloon for all of us – and more than enough hot air:
pip install --upgrade --pre beartype # <-- bug off, bugs
<sup>pucci (the new @beartype mascot) gasps in wonder as your package collapses</sup>
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.
<sup>The Masters of Fintech and Metrology. That's who.</sup>
And now for something... gothic, grotesque, and morbid!? :astonished: :open_mouth: :face_with_open_eyes_and_hand_over_mouth:
@beartype 0.20.0rc1 officially drops support for Python 3.8. Python 3.8 "recently" hits its official End-of-Life (EOL). Alright. It was ages ago. I still remember when Python 3.8 was the cool new kid who just wanted to come over and show you his $1,000 LEGO Millennium Falcon constructed entirely from deprecated type hints. Now, it's dead. Only my baldness remains. Python makin' me feel old over here.
This means that Python 3.8 now constitutes a security risk. More importantly, @beartype hates Python 3.8. Due to the Transitivity of Loathsomeness Principle (probably discovered by Pythagoras the Pythonic, the unknown balding stepchild of Pythagoras the Elder), your codebase now hates Python 3.8 too. Like a bad dream, feature loss is contagious.
<sup>$1000 bucks and 47 weeks of your youth: gone, just like @leycec when the issues pile up</sup>
@beartype 0.20.0rc1 delivers less bugs – a lot less bugs. Turns out @beartype has been buggy for years. If nobody hits a bug for a decade but that bug still exists, does it make a sound when it crushes your codebase at 4:23AM on an icy Sunday? The answer is: "That sound is your dev team screaming in shared anguish."
@beartype 0.20.0rc1 specifically:
from __future__ import annotations) with PEP 695 implicit type parameter instantiation (e.g., def muh_func[T](muh_arg: T) -> T: ...). Against all sanity, this somehow now works:from __future__ import annotations # <-- PEP 563, yo. it sucks, but you know best.
from beartype import beartype # <------- *GOOD*
@beartype
def muh_func[T](muh_arg: T) -> T: # <-- PEP 695 up in here, y'all!
return muh_arg
type aliases like you just don't care:from beartype import beartype # <------- *GOOD*
# PEP 695 type aliases, deeply nested because you no longer care about coworkers.
type ThatFeeling[T] = WhenYoureIn[T] | float
type WhenYoureIn[T] = TooDeep[T] | str
type TooDeep[T] = int | T
@beartype
def nuther_func(nuther_arg: TooDeep[complex]) -> TooDeep[bytes]:
return nuther_arg # <-- a type-check that is doomed to fail. it ain't so good
The above function signature is equivalent to this simpler, more readable, more maintainable, more debuggable, and yet somehow more boring signature:
@beartype
def nuther_func(nuther_arg: int | complex | str | float) -> int | bytes | str | float:
return nuther_arg # <-- we can now see this makes less sense than we thought
Boring is bad, though. That's clear. Simplicity doesn't count for much if you're bored all the time. Maximize non-boring even if it costs you your codebase, your career, and your future prospects of a happy family. Do what @leycec would do.
@beartype 0.20.0rc1: because @beartype has to do what you say, even when it no longer wants to
<sup>pucci isn't to blame for @beartype's poor decision-making... but your coworker might be</sup>
You are @beartype's last line of defence: @EtaoinWu, @Glinte, @rudimichal, @woutdenolf, @PauloHMTeixeira. You know who you are and what @beartype has done to your code.
Let us know if @beartype 0.20.0rc1 has done you less dirty. If so, we'll release the full-blown @beartype 0.20.0 in a week or fifteen. May Pucci the new @beartype mascot be with us all.
<sup>pucci curses the futility of her new job description</sup>
Beartype 0.20.0 Release Candidate 0 catapults out of your CRT monitor from the 90's that contains only four pounds of lead, across your mechanical key
Beartype 0.20.0 Release Candidate 0 catapults out of your CRT monitor from the 90's that contains only four pounds of lead, across your mechanical keyboard with the clacky keys and vibrant crumb trails, and into your lap. Startled, you shriek in fear. Then astonishment. Then back to fear. Trembling, your fingers reach for...
pip install --upgrade --pre beartype # <-- bugger the bugs
@beartype 0.20.0rc0 gurgles contentedly as you wipe the birthing fluids from its forehead. You choke back tears. In a moment of madness that borders on the divine, you pull the trigger. You update. You're in too deep now. The sunk cost fallacy you feel is real. Your devbox shudders. Your mechanical keyboard spills sugary soda all over itself. Your RSI-wracked mouse hand pulsates with the pre-install jitters. Surely the second coming of QA is at hand.
<sup>@beartype 0.20.0rc0: and you thought Gymnastics Turtle was weird</sup>
@beartype 0.20.0rc0 is brought to you by...
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.
<sup>The Masters of Fintech and Metrology. That's who.</sup>
And now for something fishy.
@beartype 0.20.0rc0 COULD CATASTROPHICALLY BREAK EVERYTHING. This cannot be emphasized enough. Even all-caps bold style fails to highlight the severity of possible breakage. We implore you to update. We beg you to test. We pray that someone actually reads this. The userbase you save may be your own.
@beartype 0.20.0rc0 internally refactored our entire dynamic type-checking code generator – the beating heart of @beartype. Not a module was left untouched. Why? PEP 695-compliant subscripted type aliases. We don't know what that means either. Does it even matter anymore? In 2025, the truth itself is a mere mental construct of figurative... uhh. Wait. What were we talking about? Oh, right. Your code catastrophically breaking over @beartype's knee. Thus, we reiterate:
@beartype 0.20.0rc0 COULD CATASTROPHICALLY BREAK EVERYTHING. This cannot be emphasized enough, which is why we copy-pasted it again. The @beartype test suite still passes, but that doesn't mean much. We're not you. Your code knows better than we do. If something broke, it's probably your code.
<sup>the last thing your code will say before users cry on reddit</sup>
Absolutely! Well, surely. Well, probably. Well, possibly. Well, maybe.
@beartype 0.20.0rc0 brings to bear a whole new lineup of QA goodies. You never knew you needed these QA goodies until a small voice inside of you whispered:
"You do."
Before we get to some actual meaningful content in this changelog, though...
Left to my own devices, I just play video games and obsessively rummage around in the bike shed with PEP standards nobody cares about. This is why...
I implore you to vote in this GitHub forum poll. Yes. It's all true. User opinions always mattered, but now user opinions really matter. I can't pretend they don't exist anymore. For the first time ever, you can force me to do your questionable and unsavoury bidding. Tell me what to do this summer. Vote now. Vote often. Vote hard. But for the sake of the bugs crushing your workload, vote. :full_moon_with_face:
<sup>a vote for bob evil is a vote for @beartype 0.21.0</sup>
Iterable[...] + Container[...] = BBFFLs (Best Bear Friends for Life)@beartype now deeply type-checks the last remaining PEP 484- and 585-compliant container type hints in O(1) constant time, including:
collections.abc.Container[...] type hints.collections.abc.Iterable[...] type hints.collections.abc.Reversible[...] type hints.typing.Container[...] type hints.typing.Iterable[...] type hints.typing.Reversible[...] type hints.@beartype now type-checks exactly one item of containers annotated with general-purpose container type hints like Iterable[...] and Container[...] in O(1) constant time. How? Smarty bear pants. @beartype intelligently detects whether a container is:
list[str]), in which case a pseudorandom item of that
sequence is type-checked.set[int]), in which case only the first item of that
collection is type-checked.‼ Plum users may now dispatch on all of the above.
collections.abc.Iterable[...]is the Big One™, of course. This is the profit of depending on @beartype. You wait years for @leycec to do something. Finally, @leycec does a thing. Your code works. Users cheer. It is delicious.
<sup>beartype 0.20.0rc0 isn't laughing. beartype 0.20.0rc0 doesn't even know what's happening anymore...</sup>
@beartype now deeply type-checks PEP 484-compliant type variables (e.g., T = typing.TypeVar('T')) for a variety of common use cases. Mostly, this means type variables whose type-checking can be entirely decided at decoration time by the @beartype decorator.
Since deciding type-checking at decoration time is really fast, deeply type-checking type variables in these use cases is really fast as well. Like, O(1) fast. Like everything @beartype does, these type variables are cost-free. They don't cost anything, so you'd might as well use them. This probably marks the first serious attempt by any package to tackle type variables at runtime.
This support fully covers these common use cases:
MuhList[int] given the type class MuhList[T](list[T]): ...).MuhAlias[int] given the type alias type MuhAlias[T] = list[T] | T | int).Let's take a swan dive into the deep end of...
@beartype now propagates child hints up generic type hierarchies. Because @beartype values the cooperation of coworkers you barely convinced to use @beartype in the first place, @beartype propagates child hints efficiently, recursively, and (most importantly) safely. No generics are harmed in the propagation of child hints.
The proof is in the syntactically highlighted rainbow pudding. In this example, we bring the swift fist of justice to Python QA. First, we define a generic parametrized by a type variable T. Next, we annotate a function with a type hint created by subscripting that generic with the child hint int. Finally, @beartype does the rest by propagating that child hint int into that type variable T up the type hierarchy of that generic.
Behold! Boredom personified, yet you can't turn your lidded eyes away:
from beartype import beartype
from collections.abc import Container, Iterable, Iterator, Sequence
# Define a PEP 585-compliant generic satisfying both the
# "collections.abc.Iterable" and "Container" abstract base classes (ABCs). W00t!
@beartype
class SoDumbSoDelicious[T](Iterable[T], Container[T]):
def __init__(self, sequence: Sequence[T]) -> None:
self._sequence = sequence
def __contains__(self, obj: object) -> bool:
return obj in self._sequence
def __iter__(self) -> Iterator[T]:
return iter(self._sequence)
def __len__(self) -> int:
return len(self._sequence)
# Define a function accepting an instance of this generic constrained to contain
# *ONLY* integers. Yuppers. It's true. @beartype actually type-checks this now.
@beartype
def tastes_like_lard(yum: SoDumbSoDelicious[int]) -> int:
return next(iter(yum))
# Define delectable instances of this generic. Prove this isn't just crazy-talk!
love_me_some_lard = SoDumbSoDelicious((0xCAFE, 0xBABE))
gods_no_more_lard = SoDumbSoDelicious(('Cafe', 'Babe!'))
# Assert that calling this function with a valid parameter returns the
# expected value. You are the cafe. You are the lard. You knew you shouldn't
# have read that, but you kept on doing it. The punchline so wasn't worth it.
assert tastes_like_lard(love_me_some_lard) == 0xCAFE # <-- oh not that cafe
# Call this function with an invalid parameter, which then raises a
# type-checking violation. May the Gods strike me down if lard is bad for you!
tastes_like_lard(gods_no_more_lard) # <-- ...you will eat lard and like it
...which raises the expected type-checking violation:
Traceback (most recent call last):
File "/home/leycec/tmp/mopy.py", line 35, in <module>
tastes_like_lard(gods_no_more_lard) # <-- ...you will eat lard and like it
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
File "<@beartype(__main__.tastes_like_lard) at 0x7fce8b0828e0>", line 89, in tastes_like_lard
beartype.roar.BeartypeCallHintParamViolation: Function
__main__.tastes_like_lard() parameter yum=<__main__.SoDumbSoDelicious object at
0x7fce8b35d6d0> violates type hint __main__.SoDumbSoDelicious[int], as generic
superclass collections.abc.Iterable[T] of <class "__main__.SoDumbSoDelicious">
index 0 item str 'Cafe' not instance of int.
In other words, @beartype now "knows" that any object typed as SoDumbSoDelicious[int] should be an iterable container whose items are all integers. Under the furry hood, @beartype "knows" this by recursively replacing each instance of the type variable T with the child hint int within the generic type hierarchy of SoDumbSoDelicious .
In other words, @beartype now correctly propagates mappings from type variables parametrizing generic declarations (e.g., the T in class SoDumbSoDelicious[T](Iterable[T], Container[T]):) to the child hints subscripting usage of those generics (e.g., the int in SoDumbSoDelicious[int]). @beartype does this recursively for arbitrarily complex generic type hierarchies.
This broke my brain and took two full months of mostly unpaid volunteerism. That's why I still beg for money on GitHub Sponsors like that hobo chugging Duck wine out of a cardboard box on your commute to work every day. Like the hobo, I had fun. Like the hobo, I had the urge to sleep in the gutter. This feature was so hard I had to refactor the entire @beartype codebase to support it – including @beartype's totally-not-fragile dynamic type-checking code generator that I avoid touching at all costs. That's how totally-not-fragile it is. It's the sort of poorly documented 10,074-line code dump you see prefixed with stultifying ASCII art banners like:
######### LUCK DRAGON EATS YOUR LUNCH, THEN HICCUPS?!? #########
# Here thar be dragons, code matey. Arr. *hiccup* #
# ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣤⣤⣤⣤⡼⠀⢀⡀⣀⢱⡄⡀⠀⠀⠀⢲⣤⣤⣤⣤⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ #
# ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⣾⣿⣿⣿⣿⣿⡿⠛⠋⠁⣤⣿⣿⣿⣧⣷⠀⠀⠘⠉⠛⢻⣷⣿⣽⣿⣿⣷⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀ #
# ⠀⠀⠀⠀⠀⠀⢀⣴⣞⣽⣿⣿⣿⣿⣿⣿⣿⠁⠀⠀⠠⣿⣿⡟⢻⣿⣿⣇⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣟⢦⡀⠀⠀⠀⠀⠀⠀ #
# ⠀⠀⠀⠀⠀⣠⣿⡾⣿⣿⣿⣿⣿⠿⣻⣿⣿⡀⠀⠀⠀⢻⣿⣷⡀⠻⣧⣿⠆⠀⠀⠀⠀⣿⣿⣿⡻⣿⣿⣿⣿⣿⠿⣽⣦⡀⠀⠀⠀⠀ #
# ⠀⠀⠀⠀⣼⠟⣩⣾⣿⣿⣿⢟⣵⣾⣿⣿⣿⣧⠀⠀⠀⠈⠿⣿⣿⣷⣈⠁⠀⠀⠀⠀⣰⣿⣿⣿⣿⣮⣟⢯⣿⣿⣷⣬⡻⣷⡄⠀⠀⠀ #
# ⠀⠀⢀⡜⣡⣾⣿⢿⣿⣿⣿⣿⣿⢟⣵⣿⣿⣿⣷⣄⠀⣰⣿⣿⣿⣿⣿⣷⣄⠀⢀⣼⣿⣿⣿⣷⡹⣿⣿⣿⣿⣿⣿⢿⣿⣮⡳⡄⠀⠀ #
# ⠀⢠⢟⣿⡿⠋⣠⣾⢿⣿⣿⠟⢃⣾⢟⣿⢿⣿⣿⣿⣾⡿⠟⠻⣿⣻⣿⣏⠻⣿⣾⣿⣿⣿⣿⡛⣿⡌⠻⣿⣿⡿⣿⣦⡙⢿⣿⡝⣆⠀ #
# ⠀⢯⣿⠏⣠⠞⠋⠀⣠⡿⠋⢀⣿⠁⢸⡏⣿⠿⣿⣿⠃⢠⣴⣾⣿⣿⣿⡟⠀⠘⢹⣿⠟⣿⣾⣷⠈⣿⡄⠘⢿⣦⠀⠈⠻⣆⠙⣿⣜⠆ #
# ⢀⣿⠃⡴⠃⢀⡠⠞⠋⠀⠀⠼⠋⠀⠸⡇⠻⠀⠈⠃⠀⣧⢋⣼⣿⣿⣿⣷⣆⠀⠈⠁⠀⠟⠁⡟⠀⠈⠻⠀⠀⠉⠳⢦⡀⠈⢣⠈⢿⡄ #
# ⣸⠇⢠⣷⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠻⠿⠿⠋⠀⢻⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⢾⣆⠈⣷ #
# ⡟⠀⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣶⣤⡀⢸⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⡄⢹ #
# ⡇⠀⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠈⣿⣼⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠃⢸ #
# ⢡⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⠶⣶⡟⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡼ #
# ⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡾⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁ #
# ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡁⢠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ #
# ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣼⣀⣠⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ #
# Dragons ahoy, code matey. Swab the Steam Deck. Arr. #
######### ASCII ART MEANS SWEAT, TEARS, AND CI FAILURE #########
It's likely there are unresolved edge cases I'm blissfully unaware of. If you hit one of these golden landmines, please submit an issue. I'll promptly resolve everything or collapse on my keyboard trying. :cold_sweat:
‼ Plum users may now dispatch on subscripted generics. This is the profit of depending on @beartype. You wait years for @leycec to do something. Finally, @leycec does a thing. Exhaustedly, you pump your hand in the sign of victory. You are vindicated... yet you feel empty.
<sup>awkward life moments (brought to you by @beartype)</sup>
@beartype now propagates child hints up generic type aliases, too. This is genuinely cool. Like, "cool kids" cool. Generic type aliases are kinda like functions or a full-blown templating engine – except for type hints. If you've ever found yourself copy-pasting one stupidly long type hint into another, you've realized you're violating the Don't Repeat Yourself (DRY) principle and are now off the deep end of QA. Thus:
# Instead of copy-pasting hellish type hints surely spawned in the Ninth Circle
# like this...
PygmentsStringToken = list[typing.Union[
tuple[str | collections.abc.Callable[typing.Concatenate[object, object, ...], object], ...],
tuple[str | pygments.token._TokenType[str], ...],
typing.Annotated[collections.abc.Collection[str], beartype.vale.IsInstance[pygments.lexer.include]],
]]
PygmentsBytesToken = list[typing.Union[
tuple[bytes | collections.abc.Callable[typing.Concatenate[object, object, ...], object], ...],
tuple[bytes | pygments.token._TokenType[bytes], ...],
typing.Annotated[collections.abc.Collection[bytes], beartype.vale.IsInstance[pygments.lexer.include]],
]]
# ...just define a single generic type alias like this...
type PygmentsToken[T] = list[typing.Union[
tuple[T | collections.abc.Callable[typing.Concatenate[object, object, ...], object], ...],
tuple[T | pygments.token._TokenType[T], ...],
typing.Annotated[collections.abc.Collection[str], beartype.vale.IsInstance[pygments.lexer.include]],
]]
# ...then subscript that alias with child hints. Look, code metrics! No DRY.
PygmentsStringToken = PygmentsToken[str]
PygmentsBytesToken = PygmentsToken[bytes]
In this example, PygmentsToken may not look like a function – but it pretty much is. It's a "function" that generates a new type hint from a standardized template every time you subscript it with a new child hint.
So what's the catch? We hope you don't mind requiring Python ≥ 3.12 by dropping support for Python ≤ 3.11. Because... that's the catch. If you try to define even a single type alias in a single submodule of your package under Python ≤ 3.11, your entire app unceremoniously blows up with a fatal SyntaxError: invalid syntax exception. Good luck with that.
@beartype 0.20.0rc0: because your users hate Python ≤ 3.11, too.
<sup>barbarian code never got type-checked by @beartype</sup>
I don't even know what "thrumming" is, but I'm pretty sure that's what happens to your sinews when you combine the fearsome power of subscripted generics and subscripted type aliases. The coworker to your right is already cowering. Good. That feeling is good. Soon, they will all kneel! </muhaha— *choking*>
Behold! A subscripted type alias propagating its child hint onto a subscripted generic. Why? Because your QA pipeline wasn't complicated enough and you now need to justify your position to those new bastards in suits nice HR spokespeople:
from beartype import beartype
from collections.abc import Container, Iterable, Iterator, Sequence
# Define the same PEP 585-compliant generic as above. Booooooooring.
@beartype
class SoDumbSoDelicious[T](Iterable[T], Container[T]):
def __init__(self, sequence: Sequence[T]) -> None:
self._sequence = sequence
def __contains__(self, obj: object) -> bool:
return obj in self._sequence
def __iter__(self) -> Iterator[T]:
return iter(self._sequence)
def __len__(self) -> int:
return len(self._sequence)
# Define a PEP 695-compliant type alias unifying this generic with other stuff.
# Look... *I* don't know. You're the genius here. You'll defly get a raise at
# work if you keep pushing out quality code like this.
type MaybeSoDumbSoDelicious[T] = T | MaybeSoDumbSoDelicious[T] | None
# Define a function accepting either an integer, an instance of this generic
# constrained to contain *ONLY* integers, or "None". @beartype type-checks this,
# because @beartype has no say in the matter. @beartype has to do what you say.
# This is why @beartype feels great sadness in the Autumn.
@beartype
def maybe_tastes_like_lard(yum: MaybeSoDumbSoDelicious[int]) -> int:
return next(iter(yum))
# Define delectable instances of this generic. Prove this isn't just crazy-talk!
love_me_some_lard = SoDumbSoDelicious((0x1331, 0xC001))
gods_no_more_lard = SoDumbSoDelicious(('Leet', 'Cool!'))
# Assert that calling this function with a valid parameter returns the
# expected value. You are the cafe. You are the lard. You knew you shouldn't
# have read that, but you kept on doing it. The punchline so wasn't worth it.
assert maybe_tastes_like_lard(love_me_some_lard) == 0x1331 # <-- oh not that cafe
# Call this function with an invalid parameter, which then raises a
# type-checking violation. May the Gods strike me down if lard is bad for you!
maybe_tastes_like_lard(gods_no_more_lard) # <-- ...you will eat lard and like it
...which raises the expected type-checking violation:
Traceback (most recent call last):
File "/home/leycec/tmp/mopy.py", line 42, in <module>
maybe_tastes_like_lard(gods_no_more_lard) # <-- ...you will eat lard and like it
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
File "<@beartype(__main__.maybe_tastes_like_lard) at 0x7f89162fa8e0>", line 94, in maybe_tastes_like_lard
beartype.roar.BeartypeCallHintParamViolation: Function
__main__.maybe_tastes_like_lard() parameter yum=<__main__.SoDumbSoDelicious
object at 0x7f89165c5810> violates type hint MaybeSoDumbSoDelicious[int], as
<class "__main__.SoDumbSoDelicious"> <__main__.SoDumbSoDelicious object at
0x7f89165c5810>:
* Not int or <class "builtins.NoneType">.
* Generic superclass collections.abc.Iterable[T] of <class
"__main__.SoDumbSoDelicious"> index 0 item str 'Leet' not instance of int.
...which only goes to show that @beartype now knows everything. Lo! It is known.
<sup>@beartype is getting bad vibes from trumpy</sup>
One ill-fated morning, you realise with a gnawing horror... You've run out of coffee. The Pop-Tarts® box is empty. There's no reason to get up anymore. You greet the watery Sun with a thousand-yard stare half-baked beyond the void of space. Yet, through crusty eyes, you think to yourself:
Can I compare whether one subscripted generic is a subhint of (i.e., both commensurable with and narrower than) another subscripted generic? If I can, why would I want to do that? Is this what life without coffee and Pop-Tarts® is like for billions around the world even today!?!?
I don't know why, but unspecified people <sup>okay, it's @patrick-kidger and @wesselb</sup> want to compare subscripted generics. I smell academia and a burgeoning career built on the shuddering back of @beartype. You might be one of these people or a person like these people. But, let's be honest... you're probably not. You're wondering why you're still reading this and rapidly realizing there's no good answer to that question. Some questions are bad.
Let's pretend you're one of these people. Rejoice! @beartype now decides the least trivial computational puzzle in the field of type systems, because @leycec couldn't bear to see that unresolved [Bug] status for another year:
>>> from beartype.door import is_subhint # <-- it's baaaaaaack
>>> from collections.abc import Sequence
>>> from typing import Generic, TypeVar
# Define some type variables like it's 2017.
>>> S = TypeVar('S')
>>> T = TypeVar('T')
>>> T_sequence = TypeVar('T_sequence', bound=Sequence)
# Define some parametrized generics. Pretend this means something.
>>> class GenericST(Generic[S, T]): pass
>>> class GenericSInt(Pep484GenericST[S, int]): pass
# Decide whether these generics are related or not, especially when subscripted
# by child hints. You don't know why you want to decide this. You just do.
# You've gotta. You're compelled by inner demons to go beyond the pale
# crenellations of normalcy, beyond even the liminal subspaces of QA. WUT?!?
>>> is_subhint(Pep484GenericSInt, Pep484GenericST)
True # <-- ...okay
>>> is_subhint(Pep484GenericSInt, Pep484GenericST[int, int])
False # <-- sure, buddy
>>> is_subhint(Pep484GenericSInt[int], Pep484GenericST)
True # <-- i'm not your buddy, pal
>>> is_subhint(Pep484GenericSInt[int], Pep484GenericST[S, T_sequence])
False # <-- whatever you say, bear boss
>>> is_subhint(Pep484GenericSInt[list], Pep484GenericST[T_sequence, object])
True # <-- pretty sure i no longer know what's happening
>>> is_subhint(Pep484GenericSInt[list], Pep484GenericST[Sequence, Any])
True # <-- seems legit if i squint at it
>>> is_subhint(Pep484GenericSInt[str], Pep484GenericST[T_sequence, S])
True # <-- it's too good to be true
>>> is_subhint(Pep484GenericSInt[Sequence], Pep484GenericST[list, object])
False # <-- finally, a falsehood exposed
>>> is_subhint(Pep484GenericSInt[T_sequence], Pep484GenericST)
True # <-- yeah, right, @beartype! as if
I'm confident you could profitably author multiple doctoral theses strung out across multiple poorly remunerated grad students who hate themselves almost as much as you do by just addressing, redressing, and endlessly rehashing this single issue. Papermill careers are built on the boneless backs of issues like this.
Originally, I wanted to bludgeon everyone in attendance with a tiresome essay doing just that. In the bitter end, even the mere thought of tiring everyone tires me beyond the event horizon of exhaustion.
Still, boredom is its own reward. I'll say that @beartype has probably invented the optimally efficient algorithm for deciding this problem. Nobody cares, of course. Even I am lacklustre about this whole thing.
But a non-recursive depth-first search (DFS) is a glorious self-flagellation that few ever attempt and even fewer survive. This DFS exhibits:
O(1) constant time complexity. Pump that fist!O(jk) quadratic time complexity, where:
j is the largest number of child type hints transitively subscripting an unerased pseudo-superclass of the first generic passed to is_subhint(). You don't even want to know what "unerased pseudo-superclass" means.k is the total number of transitive pseudo-superclasses of the same generic. Ditto.Because the DFS is non-recursive, it's stupidly fast, unreadable, undebuggable, and unmaintainable. This is why we @beartype:
...so that all the suffering is concentrated in one place.
<sup>left: parametrized generics. right: ...is that @leycec?!?</sup>
Of course, @leycec is lazy. @beartype still lacks general-purpose support for type-checking type variables at call time. This means @beartype still ignores type-checking violations involving mismatching types across type variables like:
def muh_func[T](muh_arg: T) -> list[T]:
return ['this is busted', "but you'll never know", 'cause @beartype dumb.']
# Beartype should raise a type-checking violation here, as the list returned by
# this function is a list of strings rather than a list of integers. Sadly,
# beartype currently thinks this is fine and does nothing, much like our cats.
muh_func(0xDEADCODE)
@beartype 0.20.0rc0: shrugging apathetically while your code burns
<sup>the answer may shock you</sup>
@beartype now tolerates these extremely popular third-party packages that hate @beartype ≤ 0.19.0 (and other runtime type-checkers, too):
urllib3.xarray.Yes, these packages hate @beartype (and other runtime type-checkers, too). The irony is especially rich in Pydantic's case, because Pydantic itself is a runtime type-checker. This must be what happens when you go full-Rust.
These packages all doubled down on the typing.TYPE_CHECKING forward reference antipattern, which @beartype 0.21.0 will have a lot to say about. Until then, this is "Leycec's Abbreviated Notes on the Antipattern That Destroys True Goodness and Heroism":
# This is what PEP 484, PEP 563, and @beartype all want you to do. Forward
# referencing external types defined in other packages is easy, fam:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import some_package
def muh_func(some_arg: 'some_package.some_submodule.SomeType'): ... # <-- good!
# Instead, this is what Pydantic, "urllib3", and "xarray" are all doing. This is
# the "TYPE_CHECKING" forward reference antipattern:
if TYPE_CHECKING:
from some_package.some_submodule import SomeType
def muh_func(some_arg: 'SomeType'): ... # <-- *BAD*! gag me with a spork, Mork
Those two approaches may look identical. From the runtime perspective, those two approaches share nothing in common. They hate one other. TYPE_CHECKING evaluates to False at runtime, so @beartype (and other runtime type-checkers) can't see the imports hidden inside those if conditionals. All @beartype sees is:
'some_package.some_submodule.SomeType' in the former case. @beartype can fully resolve the SomeType type from this. @beartype is pleased and growls contentedly while rubbing its belly for scritches.'SomeType' in the latter case. That's... just not enough information. Like, at all. Throw @beartype a friggin' bone. @beartype can't resolve anything from that! @beartype growls and throws up.@beartype 0.21.0 will explicitly detect, warn about, and repair this antipattern across all beartype.claw import hooks. That's the glorious future. For now, @beartype 0.20.0 contents itself with just silently ignoring these problematic packages in beartype.claw import hooks. We do what we can. Sometimes, it isn't much.
@beartype doesn't hate these packages. We try to be tolerant of everyone's misinformed and bad opinions. In this case, we tolerate these packages by internally blacklisting them. We don't bother trying to subject their modules, types, or callables to runtime type-checking, because we can't. Their modules, types, and callables despise type-checking. What can you do? Nuthin'. We didn't make crazy; we can't control crazy; we just put crazy in a head lock and roll over it multiple times with our adipose-laden bodies until it stops moving.
Blacklisting these packages really improves the real-world usability of the beartype.claw.beartype_all() import hook in particular, which previously choked on imports from these packages. Since @beartype now automatically blacklists these packages, you no longer need to manually blacklist them yourself by listing these packages under the BeartypeConf(claw_skip_package_names=...) configuration option: e.g.,
from beartype.claw import beartype_all
# This default call to beartype_all()...
beartype_all()
# ...is now equivalent to this complicated logic, kinda. Yay!
#from beartype import BeartypeConf
#beartype_all(conf=BeartypeConf(claw_skip_package_names=(
# 'pydantic',
# 'urllib3',
# 'xarray',
#)))
@beartype 0.20.0rc0: make all the bad stuff go away, QA daddy.
<sup>lol</sup>
...to financially feed @leycec and his friendly @beartype through our ancient GitHub Sponsors profile that predates the existence of dinosaur-like AI chatbots. 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. :musical_note: :musical_keyboard: :notes:
...but nobody asked to leave, either. The Bear Beta Fan Club is GitHub's own Hotel California. The "Exit!" sign is poorly labelled. You keep getting roped back in with dubious reassurances that "things will be better this time."
This is that time.
@posita, @wesselb, @iamrecursion, @patrick-kidger, @langfield, @JelleZijlstra, @RobPasMue, @GithubCamouflaged, @kloczek, @uriyasama, @danielgafni, @JWCS, @rbroderi, @AlanCoding, @tvdboom, @crypdick, @jvesely, @komodovaran, @kaparoo, @MaximilienLC, @fleimgruber, @EtaoinWu, @alexoshin, @gabrieldemarmiesse, @James4Ever0, @NLPShenanigans, @rtbs-dev, @yurivict, @st--, @murphyk, @dosisod, @Rogdham, @alisaifee, @denisrosset, @damarro3, @ruancomelli, @jondequinor, @harshita-gupta, @jakebailey, @denballakh, @jaanli, @creatorrr, @msvensson222, @avolchek, @femtomc, @AdrienPensart, @jakelongo, @Artur-Galstyan, @ArneBachmann, @danielward27, @WeepingClown13, @rbnhd, @radomirgr, @rwiegan, @brettc, @spagdoon0411, @helderco, @paulwouters, @jamesbraza, @dcharatan, @kasium, @AdrienPensart, @sunildkumar, @peske, @mentalisttraceur, @awf, @PhilipVinc, @dcharatan, @empyrealapp, @rlkelly, @KyleKing, @skeggse, @RomainBrault, @deepyaman, @mzealey, @adamtheturtle, @Moosems, @minmax, @jedie, @pablovela5620, @thiswillbeyourgithub, @Logan-Pageler, @knyazer, @ilyapoz, @yuzhichang, @Fedezzab, @antonioan, @im-Kitsch, @mthramann, @fbartolic, @rgallardone, @frrad, @jonnyhyman, @jennydaman, @likewei92, @acec2127
Your coding agent can read these notes before it upgrades. Set up the MCP server →