Beartype 0.21.0: Curses, It's Recursion! #528
leycec
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Beartype 0.21.0 consoles your codebase as it shudders under the oppressive tidal wave of bugs. Much like its predecessors, @beartype
0.21.0is here to help. Unlike its predecessors, @beartype0.21.0claims it solves more problems than it creates for once. Is @beartype0.21.0.... lying!? 🫢pip install --upgrade beartype # <-- blast all bugs into the git pitLet 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:
@beartype
0.21.0: this can't be what you've waited months for@beartype
0.21.0is gratefully brought to you by...GitHub Sponsors: When You Befriend the Bear, You've got a Bear for Life
This release comes courtesy these proud GitHub Sponsors, without whom @leycec's cats would currently be eating grasshoppers:
https://sescollc.com
https://dylanmodesitt.com
Thanks so much, masters of fintech.
The Masters of Fintech and Metrology. That's who.
Let's get this pawful party started.
tl;dr: Explosive Recursion Never Felt So Good
@beartype
0.21.0is 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.0is for that somebody. When you need recursion, you need @beartype0.21.0.@beartype
0.21.0also 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.0will 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.@beartype
0.21.0: a familiar face you can trustSynopsis: When You're So Verbose Even Your Synopsis Is a White Paper
Let @beartype assuage, massage, and presage ...wat? it's my release party and i'll rhyme if i wanna those issues away. @beartype
0.21.0promises 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
typealiases. The only catch? This unworldly magic needs Python ≥ 3.12, which is quite the catch indeed. Behold! Unworldly magic:Opt-in dataclass field checking! That's right. Now you too can type-check
@dataclassfields on assignment by enablingis_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 nofrom __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: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:Frozen dictionaries! It's happening, because
beartype.FrozenDictis making it happen: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?"
@beartype
0.21.0: If you don't feel like a wild animal while coding, can it be called coding?Recursion: Still Destroying Lives after All These Years
@beartype
0.21.0now officially supports all possible forms of recursion in type hints. This includes directly recursive PEP 695typealiases, 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
typealiases. They're concise. They're descriptive. They're elegant. They "just work" intuitively in the exact way you expect them to: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:
Let's take this one recursive app destroyer at a time.
@beartype
0.21.0: this is the biggest animated gif i have ever seenDirect Recursion via PEP 695 Type Aliases: Because How Much More Broken Could Your App Get?
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:
...which raises the expected output and exception traceback:
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.
@beartype
0.21.0: "zomg so cuuuuuute oh my brain hurts nooooooooooooooo"What's the Catch?
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
mypyissue tracker:@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.0only supports PEP 695: the only standard for defining recursive type hints. Everything else was just somethingmypymade up. Recursivetypealiases now work wonderfully under Python ≥ 3.12 – but that's the gotcha here.@beartype
0.21.0: rambo with a sword is something that happened only on an alternate timeline... but it still happenedOh, Gods! Here It Comes!
That's right. You love to hate it. PEP 695 is unusable under Python ≤ 3.11. Attempting to define any
typealias 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.
beartype
0.21.0: users who hate recursion are users who make your face contort into a tight rictus of agonyPython ≥ 3.12? Is That Really the Only Catch?
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 heightkwould necessitate linear-timeO(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:typealias 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 recursivetypealias for each function call. Space and time efficiency rapidly spirals into the gutter and then clutches its aching head like in a depressing LeavingLos VegasSilicon Valley scene.Only Python ≥ 3.12!? Only one layer of recursion!?
@beartype
0.21.0: let's get sweaty, togetherLet'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...
Indirect Recursion via Self-subscripting Generics: Because Your App Could Get A Lot More Broke, Apparently
Self-subscripting generics, huh? You may now be thinking:
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
typealiases, 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
typingstandards. 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
typealias, "simply":Behold! This is indirect recursion via self-subscripting generics:
...which prints the expected output and exception traceback:
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. 😭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. 😅 💦
@beartype
0.21.0: these tears i shed for your code are manlyDoes Anyone Even Care About Recursive Data Structures?
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 sustenancewondrous present in a utopian dream-world.May this small piece of the recursive puzzle assist you in your own puzzle-wrangling.
@beartype
0.21.0: because nobody tells you what to do anymoreHint Overrides: Break Type Hint Standards Over Your Knees, Because You Can
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:
...which prints the expected output and exception traceback:
@beartype
0.21.0: when you feel the need to suck on a bottle in the darkness as an afro ninja looks on in shockDataclass Type-checking: Check Fields Like Its 2077
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.0now 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:
...which raises the expected type-checking violation:
As the above example demonstrates, this preliminary functionality supports cafe babes. Uhh... I mean, this supports:
@dataclassdecorator passed no keyword parameters. 👍@dataclass(frozen=True). 🫰@dataclass(slots=True). 🤑typing.ClassVar[...]type hints. 🥰dataclasses.InitVar[...]type hints. 🏩Truly, now you too can get Poor Man's Pydantic© 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 regretteddeeply cherished this character- and morale-building life lesson.who did this to you, @beartype!?! oh, it was just dataclasses.
What's the Catch? What're You not Telling Us!?!?
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 enablingis_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.,Until @beartype fully supports all of the above edge cases,
is_pep557_fieldwill continue defaulting toFalse. Someday, this will surely work for everybody. Until then, let us collectively sob. 😭@beartype
0.21.0: teach a dev to crush bugs for a day and he'll crush bugs for a lifeFrozen Dictionaries: The Core Type Python Denied You, Beartype Gives You
Python needs an official
frozendictimplementation, if only to shut down continual demands for an officialfrozendictimplementation. Thankfully, you use @beartype.@beartype
0.21.0now 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 officialfrozendictimplementation.@beartype
0.21.0: we heard you wanted someFrozenDictwith yourFrozenDictLastly but Beastly (but not Leastly)...
...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. 🎵 🎹 🎶
Bear Club: The First Rule of Bear Club Is You Crush Bugs
@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.
@beartype
0.21.0stands poetically before the burning wreckage of your competitor's codebaseBeta Was this translation helpful? Give feedback.
All reactions