Comments by leycec
All comments ranked by humor rating
Oh. My. Guido. Somebody's spitting PEP references at me! This... this is phenomenal. This may very well be the greatest day of 2025. Even our Maine Coon cats are weeping. 😹
Because you have referenced Liskov Substitution, I am now forced to respect you. I'd initially hoped that my furious hand-waving was convincing. Implementing
is_typeddict_subhint()__orig_bases__is_typeddict_subhint()__annotations__On the one hand, PEPs 563, 649, and 749 all complicate access of
__annotations____annotations__is_typeddict_subhint()__annotations__O(1)def is_typeddict_subhint(td_1: TypedDict, td_2: TypedDict) -> bool: return td_1.__annotations__ == td_1.__annotations__
Seem trivial, right? It is...
Until PEPs 563, 649, and 749 Stroll into the Chat
These PEPs hinder equality comparisons between
__annotations____annotations____annotations__.__eq__()__annotations__Consider the horrors thereupon:
# In module "foo.py": from __future__ import annotations from typing import TypedDict class Foo(TypedDict): fighters: list[beartype.FrozenDict] # <-- unquoted forward reference: OHNOES
# In module "bar.py": from typing import TypedDict class Bar(TypedDict): fighters: list[beartype.FrozenDict] # <-- unquoted forward reference: OHNOES
Lot of madness goin' on here. Let us assume the active Python interpreter is Python ≥ 3.14 for sanity, because otherwise module
bar- Module above enables PEP 563 explicitly via
foo.from __future__ import annotations - Module above enables PEP 649 and 749 implicitly under Python ≥ 3.14.
bar
Here's what their
__annotations__annotationlib>>> from annotationlib import Format, get_annotations # <-- pretend this makes sense # Show me some foo fighters. Do it. Do it now. >>> from foo import Foo >>> get_annotations(Foo, format=Format.FORWARDREF) {'fighters': ForwardRef('list[beartype.FrozenDict]', module='__main__')} # Well... that was sure weird, huh? We're lookin' at some weird "ForwardRef" stuff # that doesn't make sense to normal humans with normal brains. # # What about bar fighters? How bad could they be? >>> from bar import Bar >>> get_annotations(Bar, format=Format.FORWARDREF) {'fighters': list[ForwardRef('beartype.FrozenDict', is_class=True, owner=<class '__main__.Bar'>)]} # WTFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF–
We now have conclusively demonstrated that...
What You Want To Do Might Be Infeasible in the General Case
Sucks, huh? Semantically,
FooBarTypedDictIn fact, the values of those keys aren't simply totally different. It is unknown how to dynamically resolve the values of those keys at runtime into semantically equivalent objects. By "unknown," I mean just that. Nobody's ever done that before. In the same sense that electrical impedance tomography (EIT) or the traveling salesman problem are unsolved problems, comparing two type hints for semantic equality under Python ≥ 3.14 is an unsolved problem. It's that hard.
FooFoo.__annotations__['fighters']annotationlib.ForwardRefevaluate()The issue is
BarBar.__annotations__['fighters']annotationlib.ForwardRefevaluate()Bar.__annotations__['fighters'].__args__[0]annotationlib.ForwardRefSo @beartype now needs to recursively traverse over the deeply nested contents of all type hints just to resolve whatever
annotationlib.ForwardRefDeciding the semantic equality of even a single pair of type hints is now sufficiently non-trivial that it may be no longer be reasonably feasible under Python ≥ 3.14. That said, there might be one finally heroic way to resolve this...
The beartype.door.TypeHint
API
beartype.door.TypeHintThe
beartype.door.TypeHintbeartype.doorIf the
is_typeddict_subhint()TypedDictbeartype.door.TypeHintIn theory, that works. In practice, that's extremely costly in all possible respects.
beartype.door.TypeHintMoreover, there's a very real and very serious maintenance cost here.
beartype.door.TypeHintbeartype.door.TypeHintThe even larger issue now, of course, is...
THAT. ALL. OF. THIS. IS. MADNESS.
Right? Like, I just typed a thirty thousand-page doctoral thesis on the nature of
TypedDictWill I ever implement this feature in the way that it should be implemented? That's a hard: "nope. Nope. NOPE. NOOOOOOOOOOOOOPE." I'd love to. I just don't have the mental bandwidth, intestinal fortitude, load of brain parasites, or hoard of gold bullion required to make the proper approach work against all possible edge cases... and if I can't make the proper approach work against all possible edge cases, there's no point to doing it. It will have bugs and be bad.
Whatever approach I adopt absolutely needs to be robust against edge cases. It cannot raise exceptions. This isn't simply about
is_subhint()typing.TypeForm[...]is_subhint()@beartypetyping.TypeForm[TypedDict]So. This is the moment of truth. The moment of horror. The moment where we all admit...
You're Probably Just Going to Implement It the Bad Way, Aren't You?
Yes! Yes. That is exactly it. The bad way may suck, but at least it's robust against edge cases. It actually works for certain definitions of "works". It never raises exceptions. That's far more important than the theoretically pure approach that theoretically works perfectly, but actually just raises exceptions in edge cases.
In other words, we're straight back to:
In the worst case,
has to iteratively crawl up theis_typeddict_subhint()of each__orig_bases__subclass (starting atTypedDictas above) until it either:td_2.__orig_bases__
- Finds
as above, in which casetd_2 in set(td_current.__orig_bases__)returnsis_typeddict_subhint().True- Finds a
subclass whoseTypedDictis the trivial 1-tuple__orig_bases__, in which case(<function TypedDict at 0x7f4fb545ca40>,)returnsis_typeddict_subhint().False
This violates the Liskov Substitution Principle. Sure. That is true. Your theoretical concerns are valid, relevant, and heard. But... it actually works. Apparently, runtime type-checking that actually works is a miracle in 2025.
Truly, Guido wept.
<sup>guido, you lookin' surprisingly good</sup>
WOAH. Lovin' it! Thanks so much for pounding this into the ground on a grim Friday night on the cusp of winter. The leaves are rattling. The trees are swaying. Snow is foreboding on the horizon line. "Let us ski dangerously this year!", I pray. :pray: :snowboarder: :pray:
I'm so sorry I can't immediately review your heartfelt dedication to the spirit of "Never say die. Not even once." The growing pytest-beartype
pytest-beartype 0.3.0In the meanwhile, I have an almost religious faith in the all-seeing eye of @Glinte. :joy:
<sup>@Glinte unlocks his final form. I'm not sure what that is either. But I trust it implicitly.</sup>
Internals? Oh, Gods...
We're talking about PEP 563 (i.e.,
from __future__ import annotationsIn other words, I know nothing. If you're curious about dark magic, you can try to reverse engineer what @beartype does from the private
beartype._check.forward.fwdresolve# ..................{ LOCALS }.................. # Decorated callable and metadata associated with that callable, localized # to improve both readability and negligible efficiency when accessed below. func = decor_meta.func_wrappee_wrappee # If the frozen set of the unqualified names of all parent callables # lexically containing this decorated callable has yet to be decided... if decor_meta.func_wrappee_scope_nested_names is None: # Decide this frozen set as either... decor_meta.func_wrappee_scope_nested_names = ( # If the decorated callable is nested, the non-empty frozen set of # the unqualified names of all parent callables lexically containing # this nested decorated callable (including this nested decorated # callable itself); frozenset(func.__qualname__.rsplit(sep='.')) if decor_meta.func_wrappee_is_nested else # Else, the decorated callable is a global function. In this case, # the empty frozen set. FROZENSET_EMPTY ) # Else, this frozen set has already been decided. # # In either case, this frozen set is now decided. I choose you! # If this hint is the unqualified name of a parent callable or class of the # decorated callable, then this hint is a relative forward reference to a # parent callable or class of the decorated callable that is currently being # defined but has yet to be defined in full. If PEP 563 postponed this type # hint under "from __future__ import annotations", this hint *MUST* have # been a locally or globally scoped attribute of the decorated callable # before being postponed by PEP 563 into a relative forward reference to # that attribute: e.g., # from __future__ import annotations # # # If this is a PEP 563-postponed type hint... # class MuhClass: # @beartype # def muh_method(self) -> 'MuhClass': ... # # # ...then the original type hints prior to being postponed *MUST* # # have annotated this pre-PEP 563 method signature. # class MuhClass: # @beartype # def muh_method(self) -> MuhClass: ... # # In this case, avoid attempting to resolve this forward reference. Why? # Disambiguity. Although the "MuhClass" class has yet to be defined at the # time @beartype decorates the muh_method() method, an attribute of the same # name may already have been defined at that time: e.g., # # While bad form, PEP 563 postpones this valid logic... # MuhClass = "Just kidding! Had you going there, didn't I?" # class MuhClass: # @beartype # def muh_method(self) -> MuhClass: ... # # # ...into this relative forward reference. # MuhClass = "Just kidding! Had you going there, didn't I?" # class MuhClass: # @beartype # def muh_method(self) -> 'MuhClass': ... # # Naively resolving this forward reference would erroneously replace this # hint with the previously declared attribute rather than the class # currently being declared: e.g., # # Naive PEP 563 resolution would replace the above by this! # MuhClass = "Just kidding! Had you going there, didn't I?" # class MuhClass: # @beartype # def muh_method(self) -> ( # "Just kidding! Had you going there, didn't I?"): ... # # This isn't just an edge-case disambiguity, however. This situation # commonly arises when reloading modules containing @beartype-decorated # callables annotated with self-references (e.g., by passing those modules # to the standard importlib.reload() function). Why? Because module # reloading is ill-defined and mostly broken under Python. Since the # importlib.reload() function fails to delete any of the attributes of the # module to be reloaded before reloading that module, the parent callable or # class referred to by this hint will be briefly defined for the duration of # @beartype's decoration of the decorated callable as the prior version of # that parent callable or class! # # Resolving this hint would thus superficially succeed, while actually # erroneously replacing this hint with the prior rather than current version # of that parent callable or class. @beartype would then wrap the decorated # callable with a wrapper expecting the prior rather than current version of # that parent callable or class. All subsequent calls to that wrapper would # then fail. Since this actually happened, we ensure it never does again. # # Lastly, note that this edge case *ONLY* supports top-level relative # forward references (i.e., syntactically valid Python identifier names # subscripting *NO* parent type hints). Child relative forward references # will continue to raise exceptions. As resolving PEP 563-postponed type # hints effectively reduces to a single "all or nothing" call of the # low-level eval() builtin accepting *NO* meaningful configuration, there # exists *NO* means of only partially resolving parent type hints while # preserving relative forward references subscripting those hints. The # solution in those cases is for end users to either: # # * Decorate classes rather than methods: e.g., # # Users should replace this method decoration, which will fail at # # runtime... # class MuhClass: # @beartype # def muh_method(self) -> list[MuhClass]: ... # # # ...with this class decoration, which will work. # @beartype # class MuhClass: # def muh_method(self) -> list[MuhClass]: ... # * Replace implicit with explicit forward references: e.g., # # Users should replace this implicit forward reference, which will # # fail at runtime... # class MuhClass: # @beartype # def muh_method(self) -> list[MuhClass]: ... # # # ...with this explicit forward reference, which will work. # class MuhClass: # @beartype # def muh_method(self) -> list['MuhClass']: ... # # Indeed, the *ONLY* reasons we support this common edge case are: # * This edge case is indeed common. # * This edge case is both trivial and efficient to support. # # tl;dr: Preserve this hint for disambiguity by reducing to a noop. if hint in decor_meta.func_wrappee_scope_nested_names: # type: ignore[operator] # print(f'Preserving string hint {repr(hint)}...') return hint # pyright: ignore # Else, this hint is *NOT* the unqualified name of a parent callable or # class of the decorated callable. In this case, this hint *COULD* require # dynamic evaluation under the eval() builtin. Why? Because this hint could # simply be the stringified name of a PEP 563-postponed unsubscripted # "typing" non-class attribute imported at module scope. While valid as a # type hint, this attribute is *NOT* a class. Returning this stringified # hint as is would erroneously instruct our code generation algorithm to # treat this stringified hint as a relative forward reference to a class. # Instead, evaluate this stringified hint into its referent below: e.g., # from __future__ import annotations # from typing import Hashable # # # PEP 563 postpones this into: # # def muh_func() -> 'Hashable': # def muh_func() -> Hashable: # return 'This is hashable, yo.'
And that's only a small fraction of the total madness on offer here. I genuinely don't know what any of that means anymore. Truly, it's a dissertation in forward reference resolution – which would have been wonderful if anyone cared, but mostly nobody cared. Now, I myself shudder when I look at that code.
If I had to hazard a guess, I'd guess that it's probably infeasible for other third-party projects to fully support stringified forward references this late in the Python game. My honest advice would be to either:
- Wait for Python 3.14. You can even start playing around with the pre-releases right now. At least see if Python 3.14 satisfies your use case. If it does, I'd just run with that and mandate Python ≥ 3.14. Save yourself the thankless nightmare that is forward reference resolution.
- Call @beartype stuff. Just make @beartype an optional dependency. If importable, import and call @beartype stuff to munge forward references.
But... yeah. Nobody should be trying to manually munge forward references in 2025. It's beyond non-trivial. I wasted most of my volunteer development hours on that wicked decision problem. Now, I kinda wish I had done something more useful with my scarce life force.
tl;dr: Life's too short.
Your dreams and ideals are too valuable. Just let somebody else – whether CPython devs via Python 3.14 or me via @beartype – squander their precious sweat and tears. Let someone else suffer. Use other people's forward reference machinery. They suffered that you didn't have to.
Don't reinvent the forward reference wheel. It's a sucky wheel that never really works right. If I could go back, I'd just ignore forward references entirely and wait for Python 3.14 to be eventually released. My face is expressionless with regret. 😑
This is a fun one. Let's scope out what exactly a prospective
beartyping.ListI spent the entire night on this instead of releasing @beartype
0.21.0rc0Behold, A Monstrosity Is Born
Firstly:
# In the "beartyping.__init__" submodule: from typing import TYPE_CHECKING if TYPE_CHECKING: List = list else: from beartype.claw import beartype_this_package beartype_this_package() from beartyping._hintfactory import List __all__ = [ 'List', ]
...and then:
# In the "beartyping._hintfactory" submodule: from beartype.door import is_bearable from beartype._data.hint.datahintpep import Hint from beartype._util.cache.utilcachecall import callable_cached from beartype._util.cls.utilclsmake import make_type class _ListSubscriptedMeta(type): hint: Hint hint_child: Hint _hash: int __args__: tuple[Hint, ...] __origin__: type def __init__(self, *args) -> None: super().__init__(*args) self.hint = None # type: ignore self.hint_child = None # type: ignore self._hash = id(_ListSubscriptedMeta) self.__args__ = None # type: ignore self.__origin__ = None # type: ignore def __eq__(cls, other: object) -> bool: return ( isinstance(other, _ListSubscripted) and cls.hint_child == other.hint_child ) def __hash__(cls) -> int: return cls._hash def __repr__(cls) -> str: return f'beartyping.List[{repr(cls.hint_child)}]' def __instancecheck__(cls, obj: object) -> bool: return is_bearable(obj, cls.hint) class _ListSubscripted(metaclass=_ListSubscriptedMeta): __module__ = 'beartyping' __name__ = 'List' _count = -1 class List(object): __module__ = 'beartyping' @classmethod @callable_cached def __class_getitem__(cls, hint_child: Hint) -> type[_ListSubscripted]: global _count _count += 1 list_subscripted = make_type( type_name=f'_ListSubscriptedSubclass{_count}', type_module_name='beartyping._hintfactory', type_bases=(_ListSubscripted,), ) #FIXME: Refactor into a dunder method above, please. *sigh* list_subscripted.hint_child = hint_child list_subscripted.hint = list.__class_getitem__(hint_child) list_subscripted._hash = hash((list_subscripted.hint,)) list_subscripted.__args__ = (hint_child,) list_subscripted.__origin__ = _ListSubscripted return list_subscripted
It's best not to question how any of that works. Why? Because I have no idea myself. I can't answer your valid questions. Let us simply accept on faith that black magic is best left unquestioned.
The proof is in the vile pudding, however:
# Prove that "beartyping.List" produces sane repr() strings. # This was *SHOCKINGLY* hard, mostly because the PEP 604-compliant # type union operator "|" does *NOT* behave like anyone thinks it does. >>> from beartyping import List >>> List[str] beartyping.List[<class 'str'>] # <-- good enough for now >>> List[str] | int beartyping.List[<class 'str'>] | int # <-- i'll take it # Prove that "beartyping.List' is intrinsically type-checkable at # runtime via the standard isinstance() builtin. >>> isinstance(["This", "is", "a", "list", "of", "strings"], List[str]) True # <-- \o/ >>> isinstance([b"This", b"ain't",], List[str]) False # <-- \o/ intensifies
That's a fairly representative example. That generically applies to most type hint factories and could, indeed, be extended to cover most of the existing functionality of the standard
typingWill anyone actually make this lurid dream a beautiful reality? Maybe... not. But surely even a bearded yet bald man in the woods can dream. 🧔 -> 👨🦲
@JWCS: If you have a spare lifetime and would like to tackle that as an official
beartyping- Make a new hollow empty package.
beartyping - Add you as a @beartype contributor.
- Let you run havoc with the dogs of QA.
Alternately, we can also just wait until I reincarnate into a second clone body, whereupon I'll surely take up the heavy mantle of my original body and do this as a testament to the folly of cloning. 🥳
大成功! This means "great success," apparently! It's like the modern "Eureka!", only unreadable! I don't even know anymore. I just do what Claude Opus tells me. 😅
I cogitated about this all weekend. I cogitated in the bathtub. I cogitated on the bicycle. I cogitated with food in my mouth and sleep in my eyes. And then it hit me. The stupidest, cleverest idea I've had all year! It's both stupid and clever. Bad ideas are often like that. The idea is thusly:
PEP 649 defines this stupid
format. It's only supposed to be for documentation purposes. But what if we forcefully abuse that format without any CPython devs knowing? What Guido doesn't know can't hurt him too much. What if we misuseFormat.STRINGto solve non-trivial decision problems at runtime? What if this is the miracle we've all been searching for?Format.STRING
The Plan: Planning for Darkness Intensifies
Specifically, what if we:
- First, try comparing the dictionaries of
__annotations__subclasses in the normal way. This works if bothTypedDictsubclasses to be compared contain no unquoted forward references under Python ≥ 3.14. That covers 99% of most use cases, honestly.TypedDict - Lasty, fallback to as a backup strategy for
Format.STRINGsubclasses annotated by one or more unquoted forward references under Python ≥ 3.14. This should work. I've tested this and can confirm thatTypedDictenables comparison between semantically comparableFormat.STRINGsubclasses even when one of those subclasses is "stringified" under PEP 563 (i.e.,TypedDict).from __future__ import annotations
The future is so bright it burns my eyes a lot. 😎 🌞
The Code: Code, Code Against the Dying of the Light
The code is surprisingly trivial. It's a bit hard to grok, because I commented nothing. But I've confirmed that this solution behaves as expected under all possible edge cases – including:
- Python < 3.14.
- Python ≥ 3.14.
- PEP 563.
- PEP 649.
Behold! A new darkness emerges:
def is_hintable_subset(hintable_1, hintable_2) -> bool: try: return ( hintable_1.__annotations__.items() <= hintable_2.__annotations__.items() ) except NameError: pass from annotationlib import ( # type: ignore[import-not-found] Format, get_annotations, ) hintable_1_hints = get_annotations(hintable_1, format=Format.STRING) hintable_2_hints = get_annotations(hintable_2u, format=Format.STRING) return hintable_1_hints.items() <= hintable_2_hints.items()
Aaaaaaaand... that's it. That's the stupidest-cleverest hybrid solution of all time.
Let's prove this hype train is still on-rails. First, we define the familiar
TypedDict# In module "bar.py": from typing import TypedDict class Bar(TypedDict): fighters: list[beartype.FrozenDict] # <-- unquoted forward reference: OHNOES brawl: 0xFEEDFACE
# In module "foo.py": from __future__ import annotations from typing import TypedDict class Foo(TypedDict): fighters: list[beartype.FrozenDict] # <-- unquoted forward reference: OHNOES
Last, we define a trivial script testing both the above
TypedDictAwful*TypedDictTypedDict# In module "test_all_this_badness.py": from foo import Foo from bar import Bar from typing import TypedDict def is_hintable_subset(hintable_1, hintable_2) -> bool: try: return ( hintable_1.__annotations__.items() <= hintable_2.__annotations__.items() ) except NameError: pass from annotationlib import ( # type: ignore[import-not-found] Format, get_annotations, ) hintable_1_hints = get_annotations(hintable_1, format=Format.STRING) hintable_2_hints = get_annotations(hintable_2, format=Format.STRING) return hintable_1_hints.items() <= hintable_2_hints.items() class Awful(TypedDict): horrible: int class Awfuller(Awful): horribler: list[int] class Awfullest(TypedDict): horrible: int horribler: list[int] print(f'Foo <= Bar? {is_hintable_subset(Foo, Bar)}') print(f'Bar <= Foo? {is_hintable_subset(Bar, Foo)}') print(f'Awful <= Awful? {is_hintable_subset(Awful, Awful)}') print(f'Awful <= Awfuller ? {is_hintable_subset(Awful, Awfuller)}') print(f'Awfuller <= Awfullest? {is_hintable_subset(Awfuller, Awfullest)}')
Running
test_all_this_badness.pyFoo <= Bar? True Bar <= Foo? False Awful <= Awful? True Awful <= Awfuller ? True Awfuller <= Awfullest? True
This is why we @beartype. @leycec makes all the bad things go away. The motto of the day is:
If Hell is a place in Python... ...then @beartype is a place in Heaven. -- @leycec, 2025, somewhere deep in the boiling Canadian wilderness that is constantly on fire
@beartype
0.22.4pipenvThanks so much for your patience, everybody. Have a great Saturday evening! There's still time...
<sup>Depicted: @leycec's Saturday evening.</sup>
Yay! Money! Someone wants to feed @beartype money! We're the project that does anything for money. We're like famed JRPG lead Yoko Taro – only without the fame, the unsettling mask, or the robot chicks. Like Yoko Taro, we'll do anything for money.
Sadly, I see no pile of unearned money laying about on the floor here. Only a pile of more work. You lured me in with the temptation of financial sustenance, @bitranox. My dreams of bathing in bills have been dashed. I'm walking away without even a T-shirt here. I was at least hoping for @beartype memorabilia that I could sell on the cheap. I'm only asking for text that reads:
"I spent 9 months slaving for GraalPy support and all I got was this Hawaiian T-shirt that blinds both of our eyes."
GraalPy, Though: Let's Do This
Sometime! Unfortunately, I have no time and I must sleep. In a better world, porting @beartype to both PyPy and GraalPy would be my immediate priorities. This is not that world. I'm currently obsessed with pushing out
pytest-beartype 0.3.0Let's pretend
pytest-beartype 0.3.0O(n)Thanks for suggesting this, though. Maybe somebody will do this for @beartype, someday. Sweet dreams are made of these.
<sup>that person on the bed concerns me. who goes to bed looking like that?</sup>
@JWCS! We've missed you so much! It's been so quiet on the issue tracker this summer without you... WAITAHOTMINUTE. It almost sounds like I want more issues to be submitted. I think I now have a codependent relationship with @beartype bugs. 😂
Uhm. I mean: "Thanks so much for chiming in!" I'd totally forgotten about #522 and #534 in the madcap dash for Python 3.14 compliance, which has been living QA Hell. Both feature requests are definitely tangentially related to this issue. Unfortunately:
- #522 is a bit bigglier than merely sane behaviour. In fact, it's so biggly that nothing will ever be done. Let's leave that open for decades until I finally close it in exhaustion on my deathbed.
TypedDict - #534 still requires an explicit wrapper, which... is probably non-trivial. That's another one for the deathbed. That said, resolving this issue here is a hard prerequisite for resolving that feature request. At least that much is super-nice.
beartype.door.TypedDict
I hope you've had an awesome summer, too. It's been beastly hot here in Canada – until two days ago, when the temperature precipitously plummeted to late November vibes. Apparently, it's winter now. I am dressed in a ski jacket. I expect snow and frostbite in minutes. Gods below! What is happening with the weather...
And once again, in Freudian tradition, I listen to Claude’s elaborations while cutting my toenails...
:foot: -> :nail_care: -> :woozy_face:
...and randomly throw in a “really? that long?”
:joy:
and sometimes "its only a cigar !"
:sob:
Who is going to read all this code? Anyone out there? Anyone? Dopamine is rushing in, seeing all that green checkmarks. Who knows the BS behind them, besides Cecil ? No one ! No one !!!
@Glinte! Please! Gods! Help us! You know the BS behind all works of man and machine. Where there is BS, there too you shall find @Glinte... unearthing that BS and hitting it with a shovel like that one French shovel guy wearing no pants.
So. Let Me Tell You What Now.
This is a phenomenal work that transforms @beartype-PyPy into @beartype+PyPy. I'd all but given up on ancient issue #324: "Resuscitate PyPy support." Not for want of desire, but want of manpower. @bitranox + Claude is that man. Err, machine. Machine-power. It's power, anyway. I can smell your concentrated power all the way from Canada, where the arctic snow begins to blow hard on the borderline.
This is also the largest changeset @beartype has ever received. 2,000 lines added and 200 lines deleted is no small thing. It's a big thing. It's bigly. I've been in open-source long enough to know how this sort of thing usually goes. Are bigly PRs usually merged? They are not. To my personal disappointment, I've seen open-source project after project quietly fail to merge large changesets. Bit rot and merge conflicts soon set in. Large-scale volunteer contributions remain unmerged, effectively dead in GitHub's brackish backwaters. Volunteers become dispirited. Useful improvements falter. Hell is a place on GitHub.
I'm determined never to let that happen here or anywhere else I govern. Under my watch, a working PR will always be merged. It may take time. It may take a lifetime. It may take several lifetimes. But it will be merged. If I have to reincarnate five times as a pasty white Canadian guy to make this happen, I will. I'll take one for the team.
So here's the 5-Year Plan to Resuscitate PyPy. It's not a great plan. It's a bald middle-aged man plan. But it's the only plan:
- Beg @Glinte to incrementally review changes to each module. @Glinte is a battle-hardened @beartype survivor. If @Glinte says it's "Okay, I guess," it's "Okay, I guess." @Glinte is GitHub's best sanity check against land mines, pitfalls, and personal sanity loss. What @Glinte says, @Glinte knows. I'm sure typing @Glinte a lot.
- I will manually merge reviewed changes to each module. Sure. I know. Technically, I could just wait until all changes have been reviewed by somebody. Pragmatically, I can't see that working. For large PRs with non-trivial changesets (like this), merge conflicts always crop up faster than unpaid malcontents (like us) can reasonably resolve. Instead, I'll manually merge each module's changeset as it passes peer review. By which I mean @Glinte. I'm typing @Glinte again! When I merge a module's changeset, I will attribute you and as the authors, @bitranox. To @bitranox, all the glory.
claude
This is the 5-Year Plan to Resuscitate PyPy.
<sup>Five months later.</sup>
Resolved by 9b943ff7f0ab05f. Interestingly, there's an ever easier way to induce recursion without directly defining a PEP 695-compliant recursive
type- Define a normal unbound type variable.
- Define a normal PEP 484- or 585-compliant generic parametrized by that variable.
- Subscript that generic by itself.
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. 😅 💦
Does Anyone Even Care About Recursive Data Structures?
No idea. I care in the abstract sense of the word "care." As is always for me, 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 actually touch the real world in any useful way.
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.
Onward to @beartype
0.21.0rc0Actually... this is ludicrous. Bike-shedding has no place here! This faded issue tracker is bursting apart at the seams. Let's quietly close this, do nothing about this, and pretend this never happened. 😹
...heh. Fortunately for my scarce time with which I'd rather be bashing goblin mobs in some nameless trashy JRPG, @beartype isn't wrong. Whereas pure static type-checkers like
mypypyrightBehold! @beartype is right about everything:
$ python3.13 >>> print(isinstance(list[int], type)) False
See? Score 1 for @beartype. Score 0 for
mypypyrightlist[int]mypypyrightfrom beartype.door import is_bearable from beartype import beartype from typing import ( Any, TypeForm, # <-- Python 3.14 magic, yo! ) @beartype def checked_cast[T](typ: TypeForm[T], val: Any) -> T: # <-- Truly magic! Truly joy! ...
Interestingly, your exact use case is covered by this section of PEP 747:
There is currently no way to indicate to a type checker that a function accepts type form objects and knows how to work with them.
addresses this limitation. For example, here is a function that checks whether a value is assignable to a specified type and returnsTypeFormif it is not:None
def trycast[T](typx: TypeForm[T], value: object) -> T | None: ...
Pretty much your exact use case, right? I know. PEP 747 is baller.
Let's pivot this issue into a feature request for PEP 747 support in @beartype. @beartype basically already internally supports PEP 747 via our private
beartype._util.hint.is_hint()Sadly, we're currently blocked on even getting started on this. Why? Because neither
typing.TypeFormtyping_extensions.TypeFormtyping_extensionsTypeFormUntil then, I'll be bashing goblin mobs in some nameless trashy JRPG if you need me. 👺 ⚔ ☠
...heh.
typing_extensions-
@beartype implements what is fun. If it ain't fun, @beartype prolly ain't gonna do it. Implementing support for unofficial third-party
attributes that (A) nobody in the @beartype userbase particularly cares about (as evidenced by the lack of vociferous users threatening to burn down this issue tracker while wielding flame torches precariously perched on pitchforks) and (B) play poorly with their officialtyping_extensionsequivalents at runtime... doesn't sound very fun. That's pretty much the diametric opposite of fun. If dead-tree dictionaries still existed, the entry for "anti-fun" would surely read:typingAnti-fun, adjective: Implementing support for unofficial third-party
attributes.typing_extensions -
@beartype implements what is practical. Usually, what is fun is what is practical. If it's impractical, it's often also opposed to fun. This issue only proves the equivalence. Over 80 real issues threatening to burn down this issue tracker means I have to triage, sadly. Impractical issues inevitably get thrown under the Bus of Time™. Superficially, this seems like such an issue. Is this issue actually impractical? No idea. If no one else complains, it's impractical; else, it's practical. Thankfully, the definition of "practicality" sure also reads:
Practicality, noun: When multiple living beings that are not AI bots all agree @beartype is doing something bad, there is a practical problem.
CurseBless the humans! 😯 -
has been in Development Hell™ for nearly a year. Their last stable release was nearly a year ago, people. Please let that sink in.
typing_extensionsis not a project committed to runtime sanity, stability, or usability.typing_extensions -
fails to conform to the runtime API of the standard
typing_extensionsmodule. Seriously. How annoying is that? Really annoying, guys. Supportingtypingitself across all actively maintained Python versions is infeasible enough. Now try addingtyping, which fails to export a conformant API, into that mix. Anti-fun impracticality intensifies.typing_extensions
Ultimately, what we have here is a philosophical difference in bad opinions. Because supporting
typing_extensionstyping_extensions"Let laziness prevail!" — the @beartype motto, probably
Let's talk specifics. Because... why not? What else could possible go wrong!? 😫
is different fromtyping.Anytyping_extensions.Any
lolbro.
typing_extensionstyping_extensionsThere is no justifiable reason for
typing.Anytyping_extensions.Anytypingtyping_extensionstypingtyping_extensions- Immediately dropping support for all unmaintained Python versions. Python 3.8 has already hit its End-of-Life (EOL) and thus constitutes a security risk. Supporting unmaintained Python versions just increases everyone's attack surface, which is already large enough. So, should drop support for Python ≤ 3.8 (if it hasn't already). Obviously. That's obvious to everyone. Right?
typing_extensions - Trivially importing all attributes guaranteed to exist in Python 3.9 into
typing. This includestyping_extensionsand a whole boatload of other attributes. Right? This is sensible, obvious, and just mandatory to preserve sanity across third-party runtime introspection of type hints.typing.Any
Likewise...
should probably drop the Python 3.12 conditional, as theHintPep695TypeAliasbackport is perfectly usable in earlier Python versions.typing_extensions.TypeAliasType
@beartype has no mandatory dependencies. This includes
typing_extensionstypingmypypyright.typingOh, wells. You type-check with the API you have – not the API you wish you had.
Which leads us to...
However, the next
release will include a different version, meaningtyping_extensionsistyping.TypeAliasType is not typing_extensions.TypeAliasType!True
Guh! Awful intensifies. The body blows just keep coming.
Honestly, I don't even see the point of trying to backport PEP 695. You literally can't do that, because PEP 695 requires low-level changes to CPython's Parser Expression Grammar (PEG)-based parser. If you take away those changes, you take away the entirety of PEP 695 and are left with... nuthin'.
The PEP 695-compliant
type- Recursive type hints, which requires lazy statement evaluation.
type - Fake poor man's unquoted forward references, which also requires lazy statement evaluation.
type - Implicit instantiation of ,
TypeVar, andTypeVarTupleobjects. Syntactic sugar finally makes type variables usable by a wide audience. Rejoice! 🥂ParamSpec
Take all those things away and what are you actually left with? Not much. I mean, is anything left? Does the PEP 695 backport do anything at all? Even if the PEP 695 backport did one small thing, do @beartype users actually care about that one small thing? Wouldn't @beartype users who actually care about PEP 695 (...which is an absolutely huge time sink just to read, grok, and integrate into their workflows) just require Python ≥ 3.12 as a basic sanity check and use the
typeI dunno, bro. All this kinda seems like massive bike-shedding and bureaucratic make-work for little to no tangible, practical, real-world gain. But... maybe that's just my crabby cabin fever talking. It's a snow blizzard outside and I don't get to ski again until tomorrow morning. <sup>...how can a man live like this!?</sup>
Until then, my surly crankiness surely can't be held against me.
<sup>average winter's day in the @leycec household</sup>
<sup>even @beartype doesn't know what's going on here</sup>
Resolved by 91f2a83cb222d. Thanks so much for the detailed writeup, @rg936672. Unsurprisingly, you were right about everything. Although this technically isn't a regression of #512, it practically sure feels like one. To ensure this never happens again, I've thoroughly audited the @beartype codebase for anything even remotely smelling like this issue. And... wouldn't you know it!?!
Sure enough. @beartype was making the same erroneous assumption (i.e., that third-party containers define sane
__bool__()ripgrepI'm releasing these resolutions as @beartype
0.20.2Let me know if your awesome team of crack specialists stumbles into any other explosive @beartype blockers. Until then, may the Creepy Fractal Toroid of Graph Theoretic Enlightenment be with us all.
<sup>the creepy fractal toroid of graph theoretic enlightenment is most pleased with this issue resolution</sup>
Fascinating question! I was sure we had an open feature request regarding this topic, but... nope. Therefore, let's do this.
You have a couple options here. When you want to tell static type-checkers (like
mypypyrighttyping.TYPE_CHECKING
: Lying to Everybody Since 2014
typing.TYPE_CHECKINGThe standard
typing.TYPE_CHECKINGBehold! Lies, lies, and more lies:
from beartype import beartype from typing import TYPE_CHECKING, TypeVar import ray from ray._private.worker import RemoteFunction0 # <-- this is the 🦆 T = TypeVar("T") R = TypeVar("R) # If this module is currently being statically type-checked, # define type hints suitable for static type-checking. Yowza. if TYPE_CHECKING: RemoteFunction0RT = RemoteFunction0[R, T] # Else, this module is currently being runtime type-checked. # Define type hints suitable for runtime type-checking! Yikes. else: # Ignore this type hint at runtime, because the third-party # "ray._private.worker.RemoteFunction0" type hint factory # fails to support runtime type-checking. YIIIIIIIIIIIKES. RemoteFunction0RT = object # <-- lolbro @beartype def map_records( fn: RemoteFunction0RT, records: Sequence[T] ) -> R: ... @ray.remote def mapper(item: int) -> str: return str(item) # this type checks because @ray.remote is typed to return RemoteFunction0 # but fails at runtime because `beartype.infer_hint(mapper)` is actually just Callable # (or for other similar reasons) string_items = map_records(mapper, [0, 1, 2, 3])
In theory, that should work. If either
mypypyrightRemoteFunction0RTtyping.TypeAliasif TYPE_CHECKING: from typing import TypeAlias RemoteFunction0RT: TypeAlias = RemoteFunction0[R, T]
But what if you hate that? What if you really, really hate that? You still have options. For example...
beartype.BeartypeHintOverrides
: Still Awful After All These Years
beartype.BeartypeHintOverridesSadly, I never got around to properly documenting @beartype hint overrides. Time never permitted; code kept breaking; issues kept exploding. These are the days of our coding lives.
Nonetheless, @beartype hint overrides work perfectly well (probably) for just this sort of icky situation. The tl;dr here is that you can tell @beartype to internally treat certain type hints like certain other type hints. Specifically, tell @beartype to treat the
RemoteFunction0[R, T]objectfrom beartype import beartype, BeartypeConf, BeartypeHintOverrides from typing import TypeVar import ray from ray._private.worker import RemoteFunction0 # <-- this is the 🦆 T = TypeVar("T") R = TypeVar("R) # Ignore this type hint at runtime, because the third-party # "ray._private.worker.RemoteFunction0" type hint factory # fails to support runtime type-checking. YIIIIIIIIIIIKES. @beartype(conf=BeartypeConf(hint_overrides=BeartypeHintOverrides({ RemoteFunction0[R, T]: object}))) # <-- lolbro def map_records( fn: RemoteFunction0[R, T], records: Sequence[T] ) -> R: ... @ray.remote def mapper(item: int) -> str: return str(item) # this type checks because @ray.remote is typed to return RemoteFunction0 # but fails at runtime because `beartype.infer_hint(mapper)` is actually just Callable # (or for other similar reasons) string_items = map_records(mapper, [0, 1, 2, 3])
Flex it, @beartype. Flex those burly QA muscles. 💪 🐻
But... Which Is Better!?!
Both are awful. In both cases, you're lying to somebody about something. That's not great. Nobody should lie – especially to the type-checkers that are trying to crush your bugs.
typing.TYPE_CHECKINGBeartypeHintOverridesBut... Can't @beartype Just Support This Feature Directly?
Sure! Let's keep this issue open until @beartype supports this feature directly. Ideally, @beartype would allow you to explicitly specify the name of a callable parameter to be ignored via a new
BeartypeConf(ignore_arg_names: Collection[str] = ())from beartype import beartype, BeartypeConf from typing import TypeVar import ray from ray._private.worker import RemoteFunction0 # <-- this is the 🦆 T = TypeVar("T") R = TypeVar("R) # Ignore this type hint at runtime, because the third-party # "ray._private.worker.RemoteFunction0" type hint factory # fails to support runtime type-checking. YIIIIIIIIIIIKES. @beartype(conf=BeartypeConf(ignore_arg_names=('fn',)) # <-- lolbro def map_records( fn: RemoteFunction0[R, T], records: Sequence[T] ) -> R: ... @ray.remote def mapper(item: int) -> str: return str(item) # this type checks because @ray.remote is typed to return RemoteFunction0 # but fails at runtime because `beartype.infer_hint(mapper)` is actually just Callable # (or for other similar reasons) string_items = map_records(mapper, [0, 1, 2, 3])
Clearly, that's the simplest approach. Just as clearly, that currently doesn't exist. I'm afraid it's either
typing.TYPE_CHECKINGbeartype.BeartypeHintOverrides
<sup>this is qa duck. the heart breaks just looking at qa duck.</sup>
Yes! So much "Yes!" I too share that dream of a future where Python just works. Because Python in the absence of types just does not work. Interestingly,
mypypyright- Override a dunder method. Like, any dunder method. The minute you're overriding dunder methods is the minute you've broken the static type-checker.
- Call the or
eval()builtins. When you're dynamically evaluating and executing code, you're no longer best friends with static type-checkers. In fact, they're now licking knives while wearing sunglasses whenever they see your codebase.exec() - Call the builtin. Dynamically define a class? Great. Now static type-checkers hate you and all your children.
type() - Use metaclass programming. Got a metaclass? Great. Metaclasses taste better than milk. Sadly, static type-checkers disagree. Of course, static type-checkers disagree on everything.
- Use tensors. Seriously. Static type-checkers just do not know what to do with tensors. y u h8 AI so much?
None of this is that dark... especially tensors. That's just normal.
I usually throw out the full-blown static type-checker in favour of a simpler linter like
pylintmypypyrightThanks again for being such an amazing @beartype user. I'm delighted that I've eased your burden during live demos. That is soooooo cool. And stressful. Sounds pretty stressful. Just thinking about live demoing anything makes me reach for a Japanese role-playing game while the sweat beads my forehead.
<sup>live demos: truly, i now have a new nightmare</sup>
Ho, ho... ho. @beartype maintainer @leycec has been summoned via #129463. As a born contrarian living in a cabin in the Canadian woods with too much time and too little sense, I have a lot of tiresome things to say about this and every other topic. Thankfully, nobody wants to hear it. I'll just pontificate about type hint smells instead.
So Even Type Hints Smell Now, Huh?
A type hint smell is the
typingtyping.ForwardRefnumpy.typing.NDArray[...]It's simple. Thus, it's maximally supported:
from typing import TypeForm def is_hints_smelly(hint_a: TypeForm, hint_b: TypeForm) -> bool: ''' :data:`True` only if the passed type hints **smell** (i.e., are poorly designed in a manner suggesting catastrophic failure by end users). ''' return not ( repr(hint_a) == repr(hint_b) and hint_a == hint_b )
There should thus exist a one-to-one correspondence between:
- The string representation (i.e., ) of a type hint.
repr() - The identity (i.e., ) or equality (i.e.,
is) of that type hint.==
Two type hints that share the same string representation should thus either literally be the same type hint or compare equal to one another. Makes sense, right?
In fact, this maxim makes so much sense that we can pretty much extend it to most object-oriented APIs. An API that violates this smell test isn't necessarily bad per say, but it is suggestive of badness.
Nobody Cares About Your Suggestions, Though
In the case of type hints, this isn't quite a "suggestion."
@beartype really wants this smell test to hold across all type hints. @beartype aggressively memoizes all internal calls on the basis of type hints and their string representations. Since all existing type hints satisfy the above smell test, they also memoize well out-of-the-box with respect to @beartype.
The proof is in the tedious pudding. Pretend this matters:
# Prove that the "list[int]" type hint isn't smelly. >>> is_hints_smelly(list[int], list[int]) False # <-- good! # Prove that the "Literal['smells', 'good']" type hint isn't smelly. >>> is_hints_smelly(Literal['smells', 'good'], Literal['smells', 'good']) False # <-- good! # Prove that the "ForwardRef('muh_package.MuhType')" type hint isn't smelly. >>> is_hints_smelly(ForwardRef('muh_package.MuhType'), ForwardRef('muh_package.MuhType')) False # <-- good!
So. We're agreed. Existing type hints don't smell. That's good... isn't it? :thinking: :thought_balloon: :boom:
Would You Please Stop Talking Already
No. I've warned you that I would pontificate! I intend to do just that.
The proposed refactoring does simplify the existing implementation of
typing.ForwardRefThe proposed refactoring also violates the above smell test. After merging this PR,
typing.ForwardRefSo. You Admit You Are Willing to Do Something Then?
No. I'm super-lazy! I just pontificate without writing code. Mostly.
Actually, I lie. I casually inspected the
typing.ForwardRefSure. CPython could diminish the real-world utility of forward references by gutting the
__eq__()__hash__()I have a trivial proposal. It is trivial, because I just did it. If I can do it, literally anyone can do it. I live in a cabin in the woods, people. Let's just resolve the issue in the
__hash__()__eq__()# This is what I'm a-sayin', folks. def __hash__(self): return hash( (self.__forward_arg__, self.__forward_value__) if self.__forward_evaluated__ else (self.__forward_arg__, self.__forward_module__) )
Literally just two more lines of code. That's it. Of course...
Python 3.14: It Is Hell
I acknowledge that Python 3.14 is coming. Indeed, Python 3.14 is almost here.
typing.ForwardRefPreserving the fragrant non-smelliness of
typing.ForwardReftyping.ForwardRefSomebody? Anybody? Hellllllllllo? :face_exhaling:
tl;dr: Let Us Fix a Thing Rather than Break a Thing
Thus spake @leycec. He don't know much, but he know a thing. This might be that thing.
...lol. That's brutal. Thanks so much for the detailed writeup, @Glinte. You're absolutely right. Python 3.12 has made a monster and I'm here for it.
Under Python ≥ 3.12, PEP 695 introduced a new class and callable pseudo-scope implicitly instantiating type variables. This new pseudo-scope interacts with PEP 563 (i.e.,
from __future__ import annotationsGreat. That's just great.
2025: another year in the QA trenches with PEP standards that hate each other. 😭
WOAH! Django horror show continues and, in fact, intensifies. I now apologise for straining your Django app so far past the breaking point of readable tracebacks. Even I fear to tread where your app is going. Usually, I love code that breaks minds. But this is a bit much.
Still, I can probably explain a bit of the special darkness you're seeing here. Let's see...
I found another bug...
Pretty sure you mean "feature." Right, @rudimichal? Feature. No? You're pretty sure? It's another bug, huh? Gods preserve our spare time. 😭
Based on the eldritch darkness useful traceback you've so helpfully copy-pasted, it looks like @beartype:
- Is decorating some or
@staticmethoddirectly defined on each of your Django@classmethodsubclasses. That's fine.Model - Then unwraps that or
@staticmethodto obtain the underlying pure-Python function you've defined. That's fine.@classmethod - Dynamically generates code type-checking the value returned by that pure-Python function. That would be fine, except...
- The type hint annotating that return is a relative forward reference (i.e., a string referring to a currently undefined type like ). That would be fine, except...
"MyOtherModel"
...then will be called with
owner=<forwardref MyOtherModel(__name_beartype__=None, __scope_name_beartype__=None)>
So friggin' weird! Totally a bug, too. No idea how or why that's happening, either. It shouldn't. Nobody else has ever hit this.
__name_beartypeNoneAre you enabling PEP 563 with the
from __future__ import annotationsYou tried valiantly to reproduce this with a minimal working example – but failed through no fault of your own, because Django is balls crazy. I feel great despair for your codebase. Let's reopen this issue until Django stops beating @beartype with a stick.
Django: A beautiful but cursed thing.
I need an automated way of doing it...
Doesn't exist, sadly. Let us commiserate together in shared misery.
typingDo you know when the typing types are going away?
Superb question! Alas, the answer is: "I am dumb and no nothing." The best-case answer is 2023. CPython devs want this logic gone now, but they also don't want to enrage everyone still stuck with legacy type hints. The worst-case answer is later this year:
The deprecated functionality may eventually be removed from the
module. Removal will occur no sooner than Python 3.9’s end of life, scheduled for October 2025.typing
And if the subscription sizes are going to be added to the official types?
...heh. Not a snowball's chance in Hell. CPython devs hate runtime typing. Hell, CPython devs hate typing in general. There are still no type hints in CPython's standard library. No way they'd ever add a public API for introspecting type hints to anything outside the
typingtypingExcellence! beartype/beartype@6b3aadfff7f9e4ef1ccde is the first step on this tumultuous voyage into the unknown. Your questions are, of course, apropos and almost certainly highlight deficiencies in my worldview. To wit:
Is naming the method
going to be robust to future changes in Python?__instancecheck_str__()
...heh. Let's pretend it is. Actually, the hope is that this will eventually metastasize into an actual PEP standard. In the meanwhile, I hope that somebody who is not me will market this to agronholm himself at the
typeguardInjecting the suspiciously @beartype-specific substring
__beartype____typeguard_instancecheck_str__()I'm finding the current beartype import hooks a bit... intense.
(╯°□°)╯︵ ┻━┻
5 different options
you're not wrong
andbeartype_packagebasically overlap;beartype_packages
you're not wrong
is probably too dangerous to ever be used;beartype_all
you're not wrong
isn't actually documented besides the one example;beartyping
you're not wrong
Wait... I'm beginning to detect a deterministically repeatable pattern here. Allow me to now try but fail to explain:
- Personally, I just wanted and
beartype_this_package(). But everybody else who piled onto the pre-release beta test of thebeartype_packages()subpackage wanted all of those other things. Things got out of hand quickly. This is what you happens when you do what other people want. Now, @beartype can't back out of any of those decisions without breaking backward compatibility across the Python ecosystem. Thankfully, it's mostly fine. These functions have yet to actually break or do anything bad. So, there's no maintenance burden or technical debt here. They just kinda hang out, doing their own thing. I salute these functions I do not need and never really wanted.beartype.claw - I meant to document . But nobody complained, I got tired, and then video games happened.
beartyping - just contextually applies to everything in the body of its
beartypingblock. I didn't realize that relative imports might break that. I never even considered relative imports anywhere. I always do absolute imports everywhere, because I am obsessive-compulsive and have trust issues. Do relative imports breakwith beartyping(...):? I... don't know, actually. I should probably go and test that. But... I probably won't until somebody complains. I'm still tired and video games are still happening.beartyping
Assuming
beartyping# foo/__init__.py with jaxtyping.install_import_hook("foo"), beartype.beartyping(): from . import bar from . import baz
This definitely should work, too:
# foo/__init__.py jaxtyping.jaxtype_this_package() # <-- dis iz h0t beartype.beartype_this_package() from . import bar from . import baz
Likewise it'd be pretty neat if beartype had a pytest hook...
Yes! So much, "Yes!" I actually implemented a pytestpytest-beartype
Long story short: "Nobody did nuffin'." :face_exhaling:
...what's the status of
checking in beartype?O(n)
Given that @beartype still fails to deeply type-check most standard container types like
dictsetO(1)O(n)That said, does this actually intersect with
jaxtypingjaxtypingUhm... Err...
Oh. Wait. I never actually implemented support for
jaxtypingjaxtypingPreviously, I'd assumed that
jaxtyping__instancecheck__()jaxtypingisinstance()I'm not even necessarily clear what "trace time" is, frankly. My wife and I are currently sloooowly migrating our data science pipeline from Ye Ol' Mostly Single-threaded NumPy and SciPy World to Speedy Gonzalez JAX World. I must confess that I am dumb, in short.
Resolved by ef26913b4ac58d71f. Thanks for your patience, forbearance, and... wait. Was that an unexpected (but altogether welcome) bear pun in my @beartype feature request? You're not wrong. Where's there's puns, there's fire. This is your final API for raising human-readable violations:
from beartype import beartype class MetaclassOfMuhClass(type): def __instancecheck_str__(cls, obj: object) -> str: return f'{repr(obj)} has disappointed {repr(cls)}... for the last time.' class MuhClass(object, metaclass=MetaclassOfMuhClass): pass @beartype def muh_func(muh_obj: MuhClass) -> None: pass muh_func("Some strings, you just can't reach.")
...which now raises the "human"-readable violation message:
beartype.roar.BeartypeCallHintParamViolation: Function __main__.muh_func() parameter muh_obj="Some strings, you just can't reach." violates type hint <class '__main__.MuhClass'>, as "Some strings, you just can't reach." has disappointed <class '__main__.MuhClass'>... for the last time.
Note that the string you return from your
__instancecheck_str__()- Should be a sentence fragment whose first character is typically not capitalized.
- May optionally be suffixed by a trailing period. In either case, @beartype intelligently does the right thing and ensures that the entire violation message is always suffixed by only a single trailing period.
Bear muscles are glistening with sweat. Flex it, @beartype! :muscle: :bear:
...heh. The age-old PEP 526-compliant Annotated Variable Assignment Runtime Type-checking Problem, huh? That one just never gets old. So, thanks for pinging me on! I love this sort of hacky Python wrangling.
Sadly, you already know everything. This is like when Luke in Empire Strikes Back finally realizes that the little wizened green lizard blob thing is actually the most powerful surviving Jedi in the entire universe. You are that blob thing. Wait. That... no longer sounds complimentary. Let's awkwardly start over.
</ahem>Two Roads: One That Sucks and One That Sucks a Bit Less
As you astutely surmise, two sucky roads lie before you:
- Do what does. That is, supercharge
typeguardto instrument ASTs.@jaxtypedcode can be a little arduous to reverse-engineer, owing to a general lack of internal commentary in that codebase. From the little to nothing that I've gleaned, thetypeguarddecorator attempts to do this. I italicize attempts, because I remain unconvinced that that genuinely works in the general case. In-memory classes and callables will be the test failures of us all. @beartype briefly considered doing this and then quickly thought better of it. A bridge too far is a bridge that will explode, plummeting all of us to our shrieking dooms below. <sup>also, i have no idea how to do this</sup>@typeguard.typechecked - Do what @beartype does. That is, supercharge with a new
install_import_hookmethod. This is the least magical and thus best possible approach.visit_AnnAssign()
Relatedly, you almost certainly want to steal everything not nailed down be inspired by @beartype's own visit_AnnAssign()
Let's Make a Deal: A Match Made in GitHub
Tangentially, would you like @beartype to automate your import hooks for you?
@beartype would be delighted to silently piggyback
jaxtypingbeartype_this_package()beartype_packages()beartyping()jaxtypingjaxtyping.install_import_hook()jaxtyping._import_hook.JaxtypingTransformerbeartype.claw._ast.clawastmain.BeartypeNodeTransformerSpecifically, @beartype would automate away everything by:
- Automatically detecting when is importable. Trivial.
jaxtyping - When is importable:
jaxtyping- Automatically applying the subclass immediately after or before (...not sure which) applying its own
jaxtyping._import_hook.JaxtypingTransformersubclass. Trivial. If I'm reading your code correctly, @beartype can just do something like:beartype.claw._ast.clawastmain.BeartypeNodeTransformer
- Automatically applying the
# In our private "beartype.claw._importlib._clawimpload` submodule: ... # Attempt to... try: # Defer optional dependency imports. from jaxtyping._import_hook import JaxtypingTransformer # AST transformer decorating typed callables and classes by "jaxtyping". # # Note that we intentionally pass *NO* third-party "typechecker" to avoid # redundantly applying the @beartype.beartype decorator to the same callables # and classes twice. ast_jaxtyper = JaxtypingTransformer(typechecker=None) # Abstract syntax tree (AST) modified by this transformer. module_ast = ast_jaxtyper.visit(module_ast) # If "jaxtyping" is currently unimportable, silently pretend everything is well. except ImportError: pass # AST transformer decorating typed callables and classes by @beartype. ast_beartyper = BeartypeNodeTransformer( conf_beartype=self._module_conf_beartype) # Abstract syntax tree (AST) modified by this transformer. module_ast_beartyped = ast_beartyper.visit(module_ast)
Of course, you don't need to actually depend upon or require @beartype in any way. No changes on needed on your end. Actually, one little change would improve sanity: if you wouldn't mind publicizing
JaxtypingTransformerThat's it. Super-easy, honestly. Everyone currently getting @beartype would then get
jaxtypingEquinox: You Are Now Good to Go
Oh – and @beartype now officially supports Equinox. 2024: dis goin' be gud.
WOAH. Austria is drop-dead gorgeous. You're the blond bombshell of the human world. Where can Canada get some of this millennia-old epic architecture of the Gods? That hulking capitol monolith in the first pic is just waiting to crush the unsuspecting tourists as it collapses into its own gravitational field like a dying white dwarf star. Meanwhile, we're over here still cobbling together uninsulated cottages with wattle, sweat, and candy-cane wrappers. Also:
Germans call us "Schluchtenscheisser" (those who shit in the valley).
Germans, man. Germans. The only people with toilets intentionally designed for close cross-examination of fecal matter. And... that's all anyone needs to know about Germans. :joy:
So sorry! I desperately want to say, "Yes! Absolutely! All things will be done, including the hardest things!" Sadly, I am fat, slovenly, and like video games too much. Hard things got done for @beartype 0.20.0 – just not the hardest.
O(n)O(1)O(n)list[...]set[...]tuple[...]That said, I'm kinda weighing options. There's a ton of stuff left to do for deep
O(1)- and
collections.abc.Callable[...]type hints. Super-hard, but super-critical. Can't believe @beartype still doesn't do this. Here we are. My head is bandaged! 🤕typing.Callable[...] - Pydantic-style fields on assignment. Currently, @beartype only type-checks
@dataclasses.dataclassfields on initialization. We currently avoid type-checking fields on assignment, because I'm lazy. Sadly, everyone is tired of that excuse. It's a good excuse, though! My laziness is incorrigible.@dataclasses.dataclass - Generator and coroutine parameters, returns, and s. This is non-trivial, because the only feasible way of type-checking generators and coroutines is in abstract syntax tree (AST) transformations run by
yieldimport hooks. Still, this is technically feasible. This should be done by somebody... someday. We all yearn for that day.beartype.claw
Those are the Big Three™, I think.
@JWCS: You're basically the Beartype Community Manager at this point. Congratulations! I will now weigh you down with awful responsibilities. Do you know of any simple way to make a poll in a GitHub Discussions thread? If it's too hard, let's not bother. But perhaps it's easy. If it is, redirecting this question to @beartype users makes more sense than @leycec just randomly hacking on whatever he wants. What do actual users want? What should I prioritize? No idea. A poll could disclose the truth. If I had my way, I'd just hack on features in the above order. Thus,
O(n)O(n)...lol. Thanks so much for digging into that! Unsurprisingly, untested code authored by a bald guy who had no idea what he was doing did nothing. No one has any idea what a "sybil" is – except that it's something no one cares about. 🥲
Let's just merge your original snippet. That's awesome and actually works. If no one submits a PR sooner, I'll try (but inevitably fail) to do this by Canadian Thanksgiving. Ping me if I do nothing. Sadly, that's a solid guarantee.
Calling
on typed dictionaries doesn't make sense.isinstance
...debatable. I'd argue that a sane
typingtypingisinstance()It's not that runtime type-checking was "too hard" for CPython devs per say. I'm just an unfunded autist in an isolated Canadian cabin with no insulation, a fake water system that freezes up in the winter, and slightly more mosquitos than video games per square inch. If I can do it, CPython devs could have done it.
It's that runtime type-checking conflicted with the pro-static type-checking ideology of CPython devs. It was purely ideological. It still is.
Anyways. That's really tangential to the meat of this discussion, which is...
Calling isinstance()
on Generic Typed Dictionaries Totally Makes Sense
isinstance()It's generic typed dictionaries that should be passable to
isinstance()isinstance()So why can't you pass generic typed dictionaries to
isinstance()typing.TypedDict__instancecheck__()__subclasscheck__()Of course, doing that would also required
typing.TypedDictdictBut the cruel reality is that...
None of That's Ever Gonna Happen, Huh?
Exactly. None of that's ever gonna happen. In 2025,
typing.TypedDictKinda sucks. My guess is that somebody will eventually come up with a much cleaner alternative syntax to
typing.TypedDictdef muh_func(**muh_kwargs: Unpack[int_key: int, str_key: str]) -> None: ...
That's valid syntax. That's way better than
typing.TypedDictSomebody, please make a miracle happen. 🙏
And yes, please do document your findings!
Wondrousness. I shall do so when I can lift my head again without collapsing! Take me away, blessed slumberland... 😴
It is easy to underestimate the FastMCP users...
Clearly, I underestimate them at my peril. 😆
...but extreme performance is in their blood...
Humans and LLMs after my own pulse-pounding heart! Truly, FastMCP and @beartype are BFFLs. Thankfully, the Bear has good news. Because this issue has now been summarily...
Resolved by e114bd107cb233191aaa. To ensure this never regresses again, I've also concocted a complimentary integration test that puts this madness through its paces. I don't even want to know how late it is. I'll be pushing out @beartype
0.22.5Thanks again for being so awesome. FastMCP for life. Random bear emoji surrounded by hearts! 💛 🐻 💙
Guh! Thanks so much for the detailed writeup, @thetianshuhuang. Sadly...
Currently (0.20.2) it seems that generic
andTypedDictaren't supported.NamedTuple
ohnoes. You're the first @beartype user to combine both
typing.TypedDicttyping.NamedTupletyping.Generic[T]I was on the cusp of publishing our upcoming @beartype
0.21.0</gulp>Ugh! Conda-forge hates organization feedstocks, huh? Very well. I impotently shake my skinny fists like antennae at the heavens Redmond, Washington. You leave us little choice, conda-forge! Let's quietly close this and pretend that Monday night was productive.
It's almost checkmate, conda-forge. Your move: :chess_pawn:
@beartype 0.22.4
In @beartype's defense, everything was Poetry's fault.
requires-python = ">=3.10,!=3.14rc1,!=3.14rc2"pyproject.tomlIn Poetry's defense, everybody loves Poetry. That's not much of a defense, but it's hard to argue with literally everybody. I take breakage like this extremely seriously. I fixed this even faster than I fixed our PyTorch breakage. I'm trustworthy! @beartype is trustworthy! I'm typing too many exclamation marks, aren't I? 😓
Thanks for being so understanding, everybody. FastMCP is awesome. Oh – and last but not least...
I Forgot to Tell Everybody that @beartype Supports FastMCP
The @beartype
0.22.x0.22.x# In a user's "{some_package}.__init__" submodule: from beartype.claw import beartype_this_package beartype_this_package()
# In the same user's "{some_package}.muh_fastmcp" submodule: from fastmcp import Client, FastMCP fastmcp_server = FastMCP('Super Cool FastMCP Server Yo') fastmcp_client = Client(fastmcp_server) # @beartype used to raise exceptions here. Now, it doesn't. We give thanks. @fastmcp_server.tool def muh_fastmcp_tool(some_rando_string: str) -> int: return len(some_rando_string) async def muh_fastmcp_app() -> None: async with fastmcp_client: await fastmcp_client.ping() TOOL_INPUT = 'You take this input and take it good' tool_output = await fastmcp_client.call_tool( 'muh_fastmcp_tool', { 'some_rando_string': TOOL_INPUT, } ) assert tool_output.data == len(TOOL_INPUT)
All works. All type-checks.
Incidentally, if anyone on the FastMCP side would like to investigate an even closer relationship between FastMCP and @beartype, it'd be fun to see if we could get @beartype to officially type-check FastMCP tools (and other stuff, possibly) without requiring users to
beartype_this_package()Or... is that already happening? Maybe that's what "adding beartype" meant. Uh oh. I suddenly feel like my ignorance is showing. Let's wrap this up quick. Thanks for flying the friendly @beartype skies! 🐻 🛫
Ha, ha! Everything mysteriously works yet again with the magical combo of @beartype
0.22.0The River Lethe got nuthin' on @beartype. 😂
Resolved by 9b7cd6a45e6d4d2bfac. Our upcoming @beartype 0.21.0 release now fully supports recursive type hints. We give praise to the long winter months here in Canada, which forced me to actually code something for once. Hallelujah! 👼
What's the Catch?
What? Catch? Surely you jest! There's no...
...oh, who am I kidding!?!?!? There are huge catches associated with this resolution. @beartype intentionally does not support older PEP-noncompliant variants of recursive type hints that used stringified forward references, like those used in @alisaifee's original example. @beartype probably could, but there's not much point in supporting non-standard functionality when standardized alternatives exist. Everyone now sees where this is going.
@beartype now fully supports PEP 695-compliant recursive type
mypyRecursive
typeOh, Gods! Here It Comes!
That's right. You love to hate it. PEP 695 is unusable under Python ≤ 3.11. Attempting to define any
type"SyntaxError: invalid syntax"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. Users who hate recursion are users you love to hate. 😆
Is That 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 this resolution. @beartype currently does not deeply type-check recursive data structures to a countably infinite depth of nested recursion. Instead, @beartype:
- Type-checks recursive data structures to only a single level of nested recursion.
- Silently accepts all deeper levels of nested recursion in recursive data structures.
We'll see exactly what that looks like below. For now, let's just accept this is happening. But why is this happening? Coupla reasons, fam:
- Constant-time time complexity. Deeply type-checking a recursive data structure with recursive height
O(1)would necessitate linear-timektime complexity in @beartype – violating @beartype's fundamental efficiency guarantee.O(k) - Space safety. Recursion in super-unsafe in Python. Python lacks tail recursion (which sucks) and allocates recursive call frames on the stack rather than the heap (which also sucks). This means that @beartype cannot safely type-check arbitrarily large recursive data structures through recursion. Thankfully, we are all awesome. Therefore, we all know that all recursive algorithms can be implemented iteratively rather than recursively. In theory, this means @beartype could recursively type-check arbitrarily large recursive data structures through iteration rather than recursion. In practice, I am already so tired I can barely see the backs of my eyelids. There may never exist enough lifetimes in the known Universe for me to do that.
- Time safety. Even an iterative approach to recursive type-checking rapidly bumps up against unpleasant real-world edge cases like infinitely recursive containers (i.e., containers that contain themselves as items like ). Of course, an iterative approach could be protected against these edge cases by dynamically generating type-checking code that maintains:
bad_list = []; bad_list.append(bad_list)- For each recursive 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
typealias for each function call. Space and time efficiency rapidly spirals into the gutter and then clutches its aching head like in a depressing Leaving Los Vegas scene.type
- For each recursive
Only Python ≥ 3.12!? Only one layer of recursion!?
Oh, Gods! There It Is! We're Screwed!
I know! I know! There are so many exclamation points happening here!
Therefore, allow me to exhibit how utterly awesome @beartype's support for PEP 695-compliant recursive
typetypefrom typing import Union from typing_extensions import TypeGuard import beartype # PEP 695 type aliases. They just work under Python ≥ 3.12. Let's not talk about what happens # under Python ≤ 3.11, because my face is already numb enough. type Primitive = Union[int, bool, float, str, bytes] # Note that PEP 695 explicitly supports unquoted forward references. Kindly do *NOT* quote # recursive references to the same type alias. Just name this alias without using strings. type Response = Union[ Primitive, list[Response], # <-- recursion erupts! set[Primitive], dict[Primitive, Response] # <-- more recursion erupts! it's recursion everywhere! my eyes! ] def valid_keys( dct: Union[ dict[Primitive, Response], dict[ Response, Response ], # needed as this is what the typeguard is filtering out. ] ) -> TypeGuard[dict[Primitive, Response]]: return all(isinstance(k, (int, bool, float, str, bytes)) for k in dct.keys()) @beartype.beartype def whynot(x: Response) -> dict[Primitive, Response]: resp = {} if isinstance(x, list): it = iter(x) resp = dict(zip(it, it)) elif isinstance(x, dict): resp = x else: raise ValueError() if valid_keys(resp): return resp raise ValueError() # These are all valid calls! @beartype happily accepts these, much like @beartype accepts you. print(whynot({"bar": "baz"})) # {"bar": "baz"} print(whynot(["bar", "baz"])) # {"bar": "baz"} print(whynot({"bar": 1, b"baz": [2, 3]})) # {"bar": 1, b"baz": [2,3]} print(whynot(["bar", 1, b"baz", [2, 3]])) # {"bar": 1, b"baz": [2,3]} print(whynot({"bar": [1, {2, 3}]})) # {"bar": [1, {2,3}]} print(whynot(["bar", [1, {2, 3}]])) # {"bar": [1, {2,3}]} print(whynot(["bar", [1, {2, 3}]])) # {"bar": [1, {2,3}]} # This is an invalid call! Boooooooo. @beartype rejects this, much like @beartype rejects your # coworker's bugs. print( whynot({frozenset({"bar"}): [1, {2, 3}]}) )
...which prints the expected output and raises the expected exception:
{'bar': 'baz'} {'bar': 'baz'} {'bar': 1, b'baz': [2, 3]} {'bar': 1, b'baz': [2, 3]} {'bar': [1, {2, 3}]} {'bar': [1, {2, 3}]} {'bar': [1, {2, 3}]} Traceback (most recent call last): File "/home/leycec/tmp/mopy.py", line 49, in <module> whynot({frozenset({"bar"}): [1, {2, 3}]}) ~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<@beartype(__main__.whynot) at 0x7aeef3c94720>", line 122, in whynot beartype.roar.BeartypeCallHintParamViolation: Function __main__.whynot() parameter x={frozenset({'bar'}): [1, {2, 3}]} violates type hint Response, as dict {frozenset({'bar'}): [1, {2, 3}]}: * Not list or set. * Not bytes, float, str, bool, or int. * dict key frozenset "frozenset({'bar'})" not bytes, float, str, bool, or int.
Praise be to Bear Cat. This is Bear Cat:
<sup>hi! dis iz bear cat. bear cat iz doing' just fine. just clawin' yer bugz. tnx fer askin', bub.</sup>
Resolved by 7603dc0. Such a shambolic code nightmare CPython devs have wrought! Yet, the long nightmare is finally over. We can all now rest easing, content in the certain knowledge that @beartype now type-checks one more thing somebody cares about.
Weather Gods, all I ask for this good deed is a warm September. Where did the summer go!? 😭
Quax looks so cool! Congrats, @patrick-kidger. You've made yet another awesome runtime QA thing. Jeez. What gruesome problems won't that guy solve next? 🤔
And now allow me say...
OMG. JAX Really Screwed the QA Pooch on This One
@davidmarttila, your brilliant minimal-length example...
from typing import Union import jax.numpy as jnp, numpy as np print(Union[type[jnp.float32], type[np.float32]]) # type[jax.numpy.float32] print(Union[type[np.float32], type[jnp.float32]]) # type[jax.numpy.float32]
...highlights exactly why JAX is busted. If someone would like to submit that to the JAX issue tracker, that would go a long way to resolving this issue to everyone's satisfaction, for everyone's sanity. That said...
There are over 1.6K open JAX issues. That's even more horrifying than @beartype's issue tracker. An issue might not be enough is what I'm saying. Someone might genuinely need to submit a working PR resolving this. 😞
Ideally I would have wanted to define a
encompassing the surprisingly large amount of ways you can express a real-valued floating point number dtype to JAX:DTypeLikeFloat
...lol. You're incredibly brave, brazen, and bold. I applaud. Reading that wall of text was such a fun dive off the deep end of tensor dtypes. I especially love this arcane madness:
Float dtypes are also subtypes of
, BUT ONLY if the type exists in numpy as well, EVEN THOUGH extended jax-only dtypes are still instances of types that are instances ofnp.dtypes._FloatAbstractDTypenp._DTypeMeta
That's trash. 😆
I thought would be nice and clean to have that handled through type annotations and dispatchers, but that was... slightly misjudged.
I'm so sorry to have disappointed. I feel bad on behalf of the entire Python QA community now. You earnestly wanted to do the right thing. You were straight-up punished for good intentions with repeated body-blows to your codebase, your time, and your sanity.
I'm so sorry. Typing's not supposed to be like this. If it's any consolation, JAX is absolutely at fault here. JAX is amazing. Don't get me wrong. We all love some JAX in our heady GPU-fueled workloads. But they sure did botch that fake NumPy dtype API. Yikes.
I guess it would be nice if the jax dtypes at least replicated the numpy MRO hierarchy? I.e. inherit from
/np.floatingwhere appropiate?np.dtypes._FloatAbstractDType
Indeed. In 2025, my bar is even lower. I'd just settle for not irresponsibly duplicating hashes. Like, come on, JAX. Work with us here. Please don't break the entire
typingThere are some edge cases though:
...heh. Subscripted generics, huh? Thankfully, that is something @beartype now does. My Gods! @beartype supports something! I am shocked, too. 😲
PEP 728 will add extra complexity...
...heh. You're trying to kill me. Thankfully, I have a mantra against PEPs like this: "I didn't see that. If no one complains about that, that means it doesn't exist and goes away."
and one important thing is to always perform such validation, even if no remaining arguments are found. In that case, an empty dict should be used to validate against the typed dict
Right. That makes sense. Gotta reject calls failing to pass all requisite keyword arguments.
After a cursory once-over, I note a metric ton of edge cases. Some of which @beartype can conveniently just ignore for an initial draft implementation; others of which, not so much:
-
from typing import TypedDict, Unpack class Movie(TypedDict): name: str year: int def foo(name, **kwargs: Unpack[Movie]) -> None: ... # WRONG! "name" will # always bind to the # first parameter. -
PEP 655-compliant
andtyping.Required[...]type hints:typing.NotRequired[...]from typing import Required, NotRequired, TypeDict class Movie(TypedDict): title: Required[str] year: NotRequired[int]
So yeah depending on how arguments are validated, this can be implemented more or less easily.
So what you're saying is: "It's gonna hurt. It's gonna hurt real bad, bro." 😆
Rejoice! This is next up on my list.
@beartypeOMGGGGGGGGGGGGGG– Seriously. I cannot believe how utterly intense the
TensorDictdir()# Uhh... wat? Tensorclasses don't even have a distinguishable class? # You've gotta be friggin' kidding me here. What is this ugly madness? >>> print(type(MyData)) type # <-- ...you don't say # My eyes and your eyes are now both bleeding. >>> print(dir(MyData)) ['__abs__', '__add__', '__and__', '__annotations__', '__bool__', '__class__', '__dataclass_fields__', '__dataclass_params__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__expected_keys__', '__firstlineno__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__getitems__', '__getstate__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__invert__', '__ipow__', '__isub__', '__itruediv__', '__le__', '__len__', '__lt__', '__match_args__', '__module__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pow__', '__radd__', '__rand__', '__reduce__', '__reduce_ex__', '__replace__', '__repr__', '__rmul__', '__ror__', '__rpow__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__setitem__', '__setstate__', '__sizeof__', '__static_attributes__', '__str__', '__sub__', '__subclasshook__', '__torch_function__', '__truediv__', '__weakref__', '__xor__', '_add_batch_dim', '_apply_nest', '_autocast', '_check_batch_size', '_check_device', '_check_dim_name', '_check_unlock', '_clone', '_clone_recurse', '_data', '_default_get', '_erase_names', '_exclude', '_fast_apply', '_flatten_keys_inplace', '_flatten_keys_outplace', '_from_dict_validated', '_from_module', '_from_tensordict', '_frozen', '_get_at_str', '_get_at_tuple', '_get_names_idx', '_get_str', '_get_sub_tensordict', '_get_tuple', '_get_tuple_maybe_non_tensor', '_grad', '_has_names', '_is_non_tensor', '_is_tensorclass', '_items_list', '_load_memmap', '_map', '_maybe_names', '_maybe_remove_batch_dim', '_memmap_', '_multithread_apply_flat', '_multithread_apply_nest', '_multithread_rebuild', '_new_unsafe', '_nocast', '_permute', '_propagate_lock', '_propagate_unlock', '_reduce_get_metadata', '_remove_batch_dim', '_repeat', '_select', '_set_at_str', '_set_at_tuple', '_set_str', '_set_tuple', '_shadow', '_to_module', '_type_hints', '_unbind', '_values_list', 'abs', 'abs_', 'acos', 'acos_', 'add', 'add_', 'addcdiv', 'addcdiv_', 'addcmul', 'addcmul_', 'all', 'amax', 'amin', 'any', 'apply', 'apply_', 'as_tensor', 'asin', 'asin_', 'atan', 'atan_', 'auto_batch_size_', 'auto_device_', 'batch_dims', 'batch_size', 'bfloat16', 'bitwise_and', 'bool', 'bytes', 'cat', 'cat_from_tensordict', 'cat_tensors', 'ceil', 'ceil_', 'chunk', 'clamp', 'clamp_max', 'clamp_max_', 'clamp_min', 'clamp_min_', 'clear', 'clear_device_', 'clear_refs_for_compile_', 'clone', 'complex128', 'complex32', 'complex64', 'consolidate', 'contiguous', 'copy', 'copy_', 'copy_at_', 'cos', 'cos_', 'cosh', 'cosh_', 'cpu', 'create_nested', 'cuda', 'cummax', 'cummin', 'data', 'data_ptr', 'del_', 'densify', 'depth', 'detach', 'detach_', 'device', 'dim', 'div', 'div_', 'double', 'dtype', 'dumps', 'empty', 'entry_class', 'erf', 'erf_', 'erfc', 'erfc_', 'exclude', 'exp', 'exp_', 'expand', 'expand_as', 'expm1', 'expm1_', 'fields', 'fill_', 'filter_empty_', 'filter_non_tensor_data', 'flatten', 'flatten_keys', 'float', 'float16', 'float32', 'float64', 'floor', 'floor_', 'frac', 'frac_', 'from_any', 'from_consolidated', 'from_dataclass', 'from_dict', 'from_dict_instance', 'from_h5', 'from_module', 'from_modules', 'from_namedtuple', 'from_pytree', 'from_struct_array', 'from_tensordict', 'from_tuple', 'fromkeys', 'gather', 'gather_and_stack', 'get', 'get_at', 'get_item_shape', 'get_non_tensor', 'grad', 'half', 'int', 'int16', 'int32', 'int64', 'int8', 'irecv', 'is_consolidated', 'is_contiguous', 'is_cpu', 'is_cuda', 'is_empty', 'is_floating_point', 'is_locked', 'is_memmap', 'is_meta', 'is_shared', 'isend', 'isfinite', 'isnan', 'isneginf', 'isposinf', 'isreal', 'items', 'keys', 'lazy_stack', 'lerp', 'lerp_', 'lgamma', 'lgamma_', 'load', 'load_', 'load_memmap', 'load_memmap_', 'load_state_dict', 'lock_', 'log', 'log10', 'log10_', 'log1p', 'log1p_', 'log2', 'log2_', 'log_', 'logical_and', 'logsumexp', 'make_memmap', 'make_memmap_from_storage', 'make_memmap_from_tensor', 'map', 'map_iter', 'masked_fill', 'masked_fill_', 'masked_select', 'max', 'maximum', 'maximum_', 'maybe_dense_stack', 'mean', 'memmap', 'memmap_', 'memmap_like', 'memmap_refresh_', 'min', 'minimum', 'minimum_', 'mul', 'mul_', 'named_apply', 'names', 'nanmean', 'nansum', 'ndim', 'ndimension', 'neg', 'neg_', 'new_empty', 'new_full', 'new_ones', 'new_tensor', 'new_zeros', 'non_tensor_items', 'norm', 'numel', 'numpy', 'param_count', 'permute', 'pin_memory', 'pin_memory_', 'pop', 'popitem', 'pow', 'pow_', 'prod', 'qint32', 'qint8', 'quint4x2', 'quint8', 'reciprocal', 'reciprocal_', 'record_stream', 'recv', 'reduce', 'refine_names', 'rename', 'rename_', 'rename_key_', 'repeat', 'repeat_interleave', 'replace', 'requires_grad', 'requires_grad_', 'reshape', 'round', 'round_', 'save', 'saved_path', 'select', 'send', 'separates', 'set', 'set_', 'set_at_', 'set_non_tensor', 'setdefault', 'shape', 'share_memory_', 'sigmoid', 'sigmoid_', 'sign', 'sign_', 'sin', 'sin_', 'sinh', 'sinh_', 'size', 'softmax', 'sorted_keys', 'split', 'split_keys', 'sqrt', 'sqrt_', 'squeeze', 'stack', 'stack_from_tensordict', 'stack_tensors', 'state_dict', 'std', 'sub', 'sub_', 'sum', 'tan', 'tan_', 'tanh', 'tanh_', 'to', 'to_dict', 'to_h5', 'to_module', 'to_namedtuple', 'to_padded_tensor', 'to_pytree', 'to_struct_array', 'to_tensordict', 'transpose', 'trunc', 'trunc_', 'type', 'uint16', 'uint32', 'uint64', 'uint8', 'unbind', 'unflatten', 'unflatten_keys', 'unlock_', 'unsqueeze', 'update', 'update_', 'update_at_', 'values', 'var', 'view', 'where', 'zero_', 'zero_grad']
WTTTTTTTTTTTTTTTTF
I mean... just... wtf!? There's even an attribute called
cumminI genuinely have no idea where to start with this beastly nightmare. My new life goal for 2025 is to just figure out how to even detect
@tensorclass@tensorclasstypetype@tensorclassWho designed this API horror show? They should feel bad, because they're bad. First I sigh. Then I facepalm. 🤦
Thanks for your magnanimous patience, all. @beartype
0.20.2Oh, salubrious hierophant of all that is profane and yet concurrently sacred in the hallowed halls of ML! Truly, your wizened name is recorded in the Akashic Data Science Records, @davidfstr. Even @beartype genuflucts before the QA prowess of an elder Python statesman. Actually... wait. My balding hair and wrinkled brow that looks like a blobfish suggests that I'm the elder Python statesman now. Hear my plaintive lament, GitHub:
"Oh, how did this happen to me..."
Let's see what we have here:
Specifically, the TypeForm-enhanced MyPy is now rightfully throwing shade at beartype’s usage of TypeForm.
A pox on mypy's house! You do not throw shade onto @beartype's filthy porch that hasn't been cleaned in seven years, mypy. @beartype throws shade onto your probably very cleanly porch, mypy!
Oh, very well. You can't argue with mypy. Well, you can. I do all the time while clenching my skinny fists tightly in the perfidious blackness of a Friday all-nighter. To no avail, though. Mypy ignores @beartype's baleful pleas in the night and furtive hand-wringing. This must be what it feels like to be on the bottom of a power-law distribution. 😅
Let's Get Pragmatic 'Cause All This Poetry Be Exhaustin' Me
My rural redneck can only take so much pithy Shakespearean musings on the Furies of QA. Please, allow me to speak like a normal person for the first time in my life.
Thanks so much for the detailed shell script! Indeed, it worked perfectly. I can confirm that @beartype is throwing spunky chunks all over mypy's newfound
TypeFormIndeed, let us see if @leycec actually does anything. Who is this @leycec guy, anyway!?
Resolved by 3f18100213f9a8e... uhh, maybe. It's best to assume nothing with this issue, which shall hereafter be referred to as "The Neverending Issue." I'm tired, I'm cranky, and I'm all of out luck dragons.
Would you mind giving this a final try? Your Django use case is the ultimate torture test. And I do emphasize "torture." Pretty sure we both now need to seek psychotherapy on a couch:
pip install git+https://github.com/beartype/beartype.git@3f18100213f9a8e396d2ae7d204461df7ecea610
Thanks again for your phenomenal efforts with this @beartype beast, @rudimichal. You've gone above and beyond the call of Pythonic QA. Truly, a magical miracle happened here tonight.
<sup>would you trust a face that looks like this? @leycec would</sup>
Guh! This is, indeed, an explosion in badness. Looks like Sphinx is doing insane, inhumane, and profane things in
conf.py- Forcing PEP 563 by injecting into the top of all
from __future__ import annotationsscripts, regardless of whether users actually want PEP 563 or understand the harmful ramifications of enabling PEP 563.conf.py - Executing scripts by:
conf.py- Reading the entire contents of those files into a local in-memory string variable.
- Dynamically evaluating those strings as executable Python code by calling the builtin.
exec()
As @beartype's exception message implies, all of this is madness. Although none of this is @beartype's fault, it's easy to blame the bear. After all, code that works without
@beartype@beartypeIt's hard to work with crazy. Sphinx is very much crazy and "ackin' cray-cray." The only reason @beartype itself uses Sphinx for documentation is that I didn't even know about MkDocs at the time. In hindsight, a little more research there would have been helpful. Because of this and similar issues, I'm now considering refactoring all of @beartype's documentation from Sphinx to MkDocs. Ain't nobody got time for Sphinx and its "special-needs" doco stack.
But What About This Awful Issue!?!?
Hmmm. No idea, honestly. @beartype should clearly do something that it's not currently doing. But... what? One obvious improvement would be to improve the resiliency of @beartype's PEP 563 resolver.
Clearly, resolving the string
"None"Nonefunc.__module__you seemed to know them outside of github
I only wish. Oh, QA Gods above, how I wish.
We'd have so much fun IRL. We'd Naruto-run; we'd pull all-nighters; we'd drink thirteen litres of God's Green Crack Ass Coffee; we'd take turns reading the first 15,000 pages of Brandon Sanderson's Stormlight Archive to each other at 3:27AM each night and I'd do all the voices of the bad guys; <sup>...geh heh heh</sup> we'd hard-core board game and video game through all the modern geek classics until the Kobe beef cows come home and the crows caw the dawn alight. Race for the Galaxy! Final Fantasty XIV! Underwater Cities! Metaphor: ReFantazio! Twilight Imperium! Tekken 7! Through the Ages! Dynasty Warrior Origins! We'd finish up with a five-day Mystery Science Theatre 3000 marathon until we can no longer feel our hands. Oh, what fun we'd have.
Back to reality. For better or worse, I live in a cabin in the Canadian wilderness. Usually, that is for the better. But it's times like this that make me momentarily miss the dense urban geek-filled landscape of Leh Big City™. Where mah homies at? 🏙
Fully agreed. You are correct about everything. I actually just finished up the changelog writeup a hot minute ago. It's impossible to believe, but I'm releasing @beartype
0.20.0rc0"Release me from my shackles! Gods! Releaaaaaaaaase me!" — zombie @leycec, probably
lolbro. Baymax intensifies and then feels deflated. <sup>...get it, "deflated"? you see, that was a pun because... oh, nmind.</sup>
typingtyping>>> from typing import List, Union >>> list[Union] list[Union] # <-- guess that's okay, i guess? thanks, pep 585! >>> List[Union] ... TypeError: Plain typing.Union is not valid as type argument # ^-- thanks fer nuthin', pep 484.
PEP 585 and 484 disagree about everything. PEP 585 takes precedence, because PEP 585 destroyed PEP 484. I don't know. Neither does anyone else. It's probably best to assume PEP 585 has the right of it – especially because that assumption simplifies everything and makes everything orthogonal.
Basically:
- Any type hint factory can be unsubscripted.
- When unsubscripted, a type hint factory defaults to subscription by the singleton.
typing.Any
Sadly, this was never actually formalized anywhere. PEP 484 strongly hints <sup>...get it? i'll stop now</sup> that this is the case, but never comes right out and says it. Because it's easier to be permissive, @beartype assumes that this is the case.
Thus ends the tale of the 5th QA battle. PEP 585: You win the day! :flags:
Ho, ho... ho. Alas, you just hit @beartype's last well-known bug. :sob:
I last attempted to type-check defaults with the ancient @beartype 0.18.0 release. I thought I'd tested all possible edge cases. I was wrong... dead wrong. I was so wrong that I temporarily broke PyTorch, OpenAI, and Microsoft for a day. I hit the panic button, immediately reverted type-checking of defaults, released @beartype 0.18.2 without that, <sup>...don't even ask what happened to @beartype 0.18.1; just don't</sup> and then permanently pulled @beartype 0.18.0 and 0.18.1 from PyPI after everyone begged me to.
I am now deathly afraid of type-checking defaults and run screaming whenever anyone suggests @beartype start doing that. Allow me to show you how brutal type-checking defaults can get with a common edge case:
from beartype import beartype @beartype def what_could_possibly(go_wrong: 'ThisIsAwful' | None = None): pass class ThisIsAwful(object): pass
See what's awful there? The
go_wrong'ThisIsAwful' | NoneThisIsAwful@beartypewhat_could_possibly()ThisIsAwful'ThisIsAwful' | NoneHowever, type-checking the default for that parameter requires that the type hint
'ThisIsAwful' | NoneThere are various ways around this, but they're all non-trivial and require varying degrees of cleverness. I am not a clever man, @ilyapoz. I am a dumb man who just bangs on keyboards a lot.
I acknowledge that this is a super-annoying deficit in @beartype. Please bug me about this if I still haven't resolved #154 in a year or two. Until then, I'd prefer not to break PyTorch, OpenAI, or Microsoft again. Someone would probably punch me if I did that again.
Guh! Justifiable begging intensifies. You're all painfully right. I should have rolled out a patch release months ago with all of these minor issue resolutions. Sadly, tests are currently failing, CI is exploding, and QA is out the window.
Please bear with @leycec as he desperately plugs the widening holes in the bursting dam.
<sup>@beartype leakier than a squirrel whose only fashion accessory is an acorn hat</sup>
Totally. The badness is appalling. A dramatic increase in space and time complexity proportional to the number of forward references used throughout a codebase is probably the least of the badness. Exactly as you say, forward reference proxies preventing garbage collection by erroneously maintaining a death grip on stale objects scares the digested food out of me. It's important that nobody else notices this. Wait. Are these discussions public!? ohnoooooooooo—
I'd better tackle this before anybody else notices this. Let us hang our heads in exhaustion. 😞
...lol.
importlib_metadatabeartype.roar.BeartypeCallHintParamViolation: Function __main__.main() parameter package_path=[PackagePath('_beartype.pth'), PackagePath('beartype-0.20.1.dist-info/INSTALLER'), Package...')] violates type hint list[importlib.metadata.PackagePath], as list index 1 item <class "importlib_metadata.PackagePath"> "PackagePath('beartype-0.20.1.dist-info/INSTALLER')" not instance of <class "importlib.metadata.PackagePath">.
Their paper-thin justifications at python/importlib_metadata#300 for monkey-patching CPython's standard library are equally banal and nonsensical:
...several users have indicated that maintaining compatibility for this interface would be important to them even if there are no users affected by it.
If there are no users affected by it, then why is this important to users? Make madness make sense. Nobody should maintain a blatantly broken and ill-advised monkey-patch that breaks the standard library and thus valid use cases like runtime introspection, which is way more important than any of their contrived hand-waving. Also:
See miurahr/aqtinstall#221 for an affected user. Although that use-case is no longer affected.
A user that is no longer affected is, by definition, not an affected user. My feelings about all of this:
I throw my hands up in the air! 🙌 And then I throw up everywhere. 🤮
Against my better judgement, you have piqued my reluctant curiosity:
I "solved" my case by identifying under which python version
was imported. Then usingimportlib_metadataandcastI was able to get bothsys.version_infoandpyrightworking.beartype
Would you mind sharing what you did? From the @beartype perspective, we can probably transparently "resolve" this for everybody by:
- Detecting whether has been imported (i.e., is in
importlib_metadata).sys.modules - If so, internally expanding all references to the class across all type hints to either:
importlib.metadata.PackagePath- The monkey-patched class.
importlib_metadata.PackagePath - The union comprising both classes, which is probably a bit safer.
importlib_metadata.PackagePath | importlib.metadata.PackagePath
- The monkey-patched
<sup>@beartype wishes it could unsee this issue</sup>
Resolved by a4924c3. @beartype now officially supports the
@celery.Celery.task0.22.0Thanks so much to @jonathanberthias for that amazing minimal-reproducible example. Celery is a bear of an API, as anyone who has spent five minutes smashing the keyboard in futility could attest. I definitely wouldn't have been able to resolve this issue without your wickedly cool contribution, Swiss Celery guru. @beartype profusely high-fives you until your palm is red and you beg @beartype to stop.
<sup>but @beartype is never gonna stop doing this</sup>
tl;dr: Typer type hints that look like
typing.Annotated[str, typer.Option(help="Last name of person to greet.")]typer.Option(help="Last name of person to greet.")Boring Manologue: It May Cost You Your Hair
...lol. I really wanted to add official support for the
@typer.Typer.commandFortunately, it's unclear whether this issue is even relevant in 2025. Like so many things (e.g., my luxurious hair, my toned physique), Typer has moved on. Why? How? Because Typer now officially supports PEP 593-compliant typing.Annotated[...]
import typer from typing import Annotated def main( name: str, lastname: Annotated[str, typer.Option(help="Last name of person to greet.")] = "", formal: Annotated[bool, typer.Option(help="Say hi formally.")] = False, ): """ Say hi to NAME, optionally with a --lastname. If --formal is used, say hi very formally. """ if formal: print(f"Good day Ms. {name} {lastname}.") else: print(f"Hello {name} {lastname}") if __name__ == "__main__": typer.run(main)
@beartype was correct to complain about Typer's prior use of PEP-noncompliant type hints – and still is. In theory, this means that @beartype and Typer are now BFFLs again. We used to hate each other. Now we quietly tolerate each other. Friendship in the digital age is like that. 😂
Since Typer now plays ball with PEP standards, @beartype now plays ball with Typer. That means this issue no longer has a reason to live, though. If anyone objects, please re-open this and I'll happily take a deeper, harder, longer look at Typer. Until then, thanks so much for playing along with @beartype.
All your CLI belong to Typer. 🫥
A hacky way of achieving this could be writing a script, or a setup wizard that does the actual installation and that the lutris installer runs.
Oh... Oh, Gods. You are attempting to birth a new shambolic horror into the world again, aren't you? Angra Mainyu, the Demon Star symbolizing All the World's Evil arises! Aaaaaaaaaaaaaaaaaaagh– 🪓 🩸
Let's pretend that didn't happen and that you didn't suggest that. You are too clever for everyone's own good. 😂
...give us permission to upload a copy of the patch files (with the necessary credit given to them) needed to run the game on github or a similar platform.
A pertinent question: "How big are these patch files... roughly speaking?" I'm not sure what the current GitHub limits are, but they're probably well below the gigabytes of questionable H scenes that might be required here.
I suppose somebody should now contact upstream. I don't present well and thus dub @TDCMC this repo's Official Beast's Lair Intercessor. Unlike me, you appear straight-laced and reasonable. Congrats, bro. You're the best among us.
Aaaaaaaaaand... let's quietly close this in favour of ancient duplicate issue #60. Apparently, it's been four years already. Four years of everyone wanting this and me not doing this. 😮💨
Canadian Gods of Ice and Snow, hear my plea:
Grant me the time to do this, the wisdom to care, and the money to stuff tuna into my cats' constantly hungry mouths with.
I live in a monorepo with bazel + nix :)
😮
You have gone beyond where eagles dare. The statement "I live in a monorepo" now holds a cherished place in my black heart. We live in a society where bold men, women, and even cats dare to live in monorepos. I... I never thought I'd see the day.
Also, what is the Bazel thing you speak of...
Bazel is a free and open-source software tool used for the automation of building and testing software.
Good. That is good.
Developer(s): Google License: Apache License 2.0 Stable release: 8.3.1 / 30 June 2025; 28 days ago
Good. That is good.
Programming language: Java
I am now squinting at you, @ilyapoz.
Rules and macros are created in the Starlark language, a dialect of Python.
What is this madness come amongst us!?!? 🫢
Congrats! I was really rooting for you all these days! You are a champ!
You're so nice! Thanks so much for your amazing support and papaha hat. You live in a monorepo. More importantly, you've kindly exposed more glaring issues in the @beartype codebase than every other @beartype user combined. You've done so much to help improve @beartype. I'm crying.
Also, what is this Karakul sheep thing that the papaha hat is made of...
Karakul or Qaraqul is a breed of domestic fat-tailed sheep...
What!? Even sheep have fat tails now? I want one. They'd go wonderfully with our fat-bodied Maine Coon cats. No one would be able to tell which is which. 😹
bumping beartype's issues number by one for the sake of documenting everything
You break my heart, Glinte. @beartype's issue number is perilously approaching the dread number 100: the number of issues at which point everyone quietly admits that a project has a problem. Guido wept. 😭
Aaaaaaaaaaaand... disabled. Thus ends the world's fastest speed-run of what not to do with generative AI. What a shame! I hope to re-enable all of this at some point, of course. I live fast, dangerous, and foolishly. @beartype really could use the
gemini-cliLet's re-assess this in a year or so after the dust settles. Some other high-profile open-source projects will need to get publicly burned first. @beartype isn't brave enough to be that sacrificial goat. 🐐
@adamtheturtle: When you find a spare moment with vws-pythonpytest_collection_modifyitems()
I suspect there might be pernicious interactions there with
@pytest.mark@pytest.mark.skipif@pytest.mark.parametrizepytestpytest...lol. PEP 484 never formalized
__parameters____parameters__typingWe can look back at that precious time and cry softly to ourselves in the dark. 2014: "We were innocent, once."
<sup>darn you, pep 484. darn you to heck.</sup>
Duplicate of #60. My Gods. Has it already been four years since #60 was opened!? The time disappeared and all I have to show for it is my bald head. Uhm. Allow me to try again.
</ahem>Thanks so much for the fun issue, @DeflateAwning! Sadly, @beartype is working as intended – just not as expected. You may now be thinking: "If this is working as intended, then @beartype sucks!"
Allow me to now explicate. @beartype guarantees
O(1)Thing.as_dict()Usually, this behaviour is a Good Thing™. Type-checking an arbitrary number of container items in
O(n)In general, the sort of linear type-checking you're expecting isn't necessarily a great fit for runtime type-checkers like @beartype. That said, I acknowledge that everybody hates this! I know. Everybody really wants @beartype to start supporting linear type-checking... even if doing so is guaranteed to destroy their customer base at some point. @beartype is here to support you in your wanton lifestyle. 😸
I'm so sorry that @beartype doesn't currently do this, @DeflateAwning. Truly, 2025 is that kind of year.
You... you so unbelievably nice! I voluminously thank you for your outpouring of kind and favourable words. Indeed, even just shallowly type-checking PEP 646 tuple hints was a bit <sup>okay, a lot</sup> more volunteerism than I initially thought — but that's why we're all here. More volunteerism means more fun. I hope to have more fun just as soon as I pass out and sleep for twelve days straight.
Oh, and...
Now I need to update beartype in our ecosystem to reap the benefits asap.
Yeah... about that, @ilyapoz. I conveniently forgot to mention that this feature resolution is scheduled for release with our upcoming @beartype
0.22.0UPDATE: I Just Realized I Botched an Edge Case!
ohnoes. Let's briefly reopen this. Everything else here works except treating
tuple[str, *Ts]tuple[str, ...]tuple[str, *Ts]tupleGreat success! @beartype now type-checks frozen dataclasses. We're incrementally getting there, bear bros:
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(frozen=True) # <-- i'm freezing my cheeks off over here class MuhFrozenDataclass(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 = MuhFrozenDataclass(muh_int=42) # <-- this works # *BAD.* bad_data = MuhFrozenDataclass(muh_int=42) # <-- still works bad_data.muh_int = '0xCAFEBABE' # <------ this fails with a type-checking violation! *YAY*!
...which raises the expected type-checking violation:
beartype.roar.BeartypeCallHintPep557FieldViolation: Dataclass MuhFrozenDataclass(muh_int=42) attribute 'muh_int' new value '0xCAFEBABE' violates type hint <class 'int'>, as str '0xCAFEBABE' not instance of int.
This lands (hopefully) tomorrow or surely no later than the day after with... @beartype 0.20.0
<sup>@beartype
0.20.0rc0In the spirit of fraternity and making @beartype look better, let us quietly close this. Technically, this is all TensorDict's fault. TensorDict devs hate type-checking. There's just not much you can do when ideology and computer science collide. 😩
<sup>shocked cat is as shocked as @leycec and @pablovela5620 are right now</sup>
Partially resolved by 19d2a7de980de. @beartype now officially supports the problematic
@fastmcp.FastMCP.toolBRILLIANT! Thank you soooooooooooooo much for spending your extremely valuable weekend on the intersection of
mypyThis
typingANY: Hint = Any
I... never would have thought of that. Because I am a Canadian redneck, I probably would've just blindly chucked
type: ignore[cause-i-sed-so]Let us merge this immediately for the good of everything that is holy and Pythonic. :partying_face: :snake: :face_holding_back_tears:
this is one of the best library I have come accross and use it everyday!
Thanks so much for your kind and heartfelt words! That means so much. Let us now hug. 🤗
I have created a custom behaved class...
...uh oh. I got a bad feelin' about this one, Boss.
I suppose that if I just decorate my init method with beartype I will therefore get an error.
I... have no idea, actually. Your use case is super-interesting, though. Would you mind trying to just brute-force decorate everything with the
@beartypeTracebackIf @beartype does raise an exception, you have more than a few horrible kludges that destroy sanity valid options available. I suspect that @beartype probably won't raise exceptions, though. Why? Because @beartype currently raises no exceptions for
dataclasses.fieldHow did you do to bypass this bottleneck in dataclasses?
I didn't. It was glorious. I'm lazy and prefer to play video games. So, I didn't do anything. Specifically, @beartype itself actually doesn't do anything to officially support
@dataclasses.dataclassUnlike static type-checkers like
mypypyright@dataclasses.dataclassCan I then directly decorate TransitionClass's init with beartype?
Sure! Why not? Let's give that a try. When everything blows up, I'll be here to cry myself to sleep and then eventually do something about it.
@beartype currently ignores the PEP 681-compliant
@typing.dataclass_transform()@typing.dataclass_transform()Let's rub our square chins thoughtfully while doing nothing.
<sup>man with bat helmet teaches @beartype how to do it</sup>
YAAAAAAAAAAY! Thanks so much for deciphering our unabashed failures, @rudimichal. It... was totally our fault after all. @beartype has fallen down and cannot get up.
Now that you've done all the heavy and painful lifting that cost you your very will to live, this should be trivial. I'll stumble onwards and upwards from here by:
- Swapping out that clearly awful implementation with @beartype's own empty
DICT_EMPTYimplementation: e.g.,FrozenDict
# Everywhere we do something awful like this... type_scope = DICT_EMPTY # ...we need to instead do something glorious like this! from beartype._data.kind.datakinddict import DICT_EMPTY type_scope = FROZEN_DICT_EMPTY
- Probably removing entirely from the codebase. It's just too risky to have xenomorph face-hugger like that hanging around.
DICT_EMPTY
Looks like there's gonna be a @beartype
0.20.0rc2Probably resolved by @beartype 0.20.0rc00.20.0rc0
pip install --upgrade --pre beartype # <-- bugger the bugs
We salute you who are about to dispatch iterables, Plum users. 💪
beartype.typing.Protocol works!
...lolbro. Super-weird, but I'm delighted. I barely understand how any of this insane
ProtocolWe may be on the worst timeline, but at least we'll always have Zelda. :smile:
...heh. Standardization intensifies.
Is that actually allowed?
By @beartype? Sure! Absolutely. Of course. @beartype just makes up everything as it goes, anyway. @beartype has supported absolute stringified references for... Jesus. Has it really been a decade now? It really has. It's been a decade. What have I been doing with my life?
Havin' fun. That's what! :sunglasses:
...but PEP 484 and the current version in the spec don't mention it as a possibility.
It's PEP 563, actually. Consider this example from PEP 563:
from __future__ import annotations import typing if typing.TYPE_CHECKING: import expensive_mod def a_func(arg: expensive_mod.SomeClass) -> None: a_var: expensive_mod.SomeClass = arg ...
typing.TYPE_CHECKINGFalsefrom __future__ import annotationsexpensive_mod.SomeClass"expensive_mod.SomeClass"import typing def a_func(arg: 'expensive_mod.SomeClass') -> None: a_var: expensive_mod.SomeClass = arg ...
This then implies that
'expensive_mod.SomeClass'SomeClassexpensive_modFrom @beartype's perspective,
typing.TYPE_CHECKINGfrom __future__ import annotationsFrom @beartype's perspective, there's no need to explicitly import anything. Whether users import things in
if typing.TYPE_CHECKING:Given all of the above, why would there be any need to explicitly import anything in an
if typing.TYPE_CHECKING:It's all kinda silly, honestly. Boilerplate is bad. Python isn't Java. We're supposed to be the agile, anti-bureaucratic guys who wear flower-print Hawaiin t-shirts in the room. Nobody wears suits here.
I think mypy might be smart enough to notice that the submodule is not necessarily imported and complain...
I consider that stupidity rather than smart. Static type-checkers just waste everyone's time with this sort of pablum. There isn't really any error or issue with absolute stringified references. Static type-checkers just made up a mythical problem that doesn't actually exist.
Oh, well. @beartype continues shrugging.
Another solution is to keep the
import, but then define aTYPE_CHECKINGmodule hook to do the import, for runtime checkers to use.__getattr__()
Clever, but also awful. Nobody would ever do that. That's the problem, right? If we consider this sort of kludge deeply and earnestly, we come to the unmistakable conclusion that nobody should ever do that.
Seriously. That's maximum bureaucracy, boilerplate, and DRY violations. Can you imagine literally every single codebase that uses the
typing.TYPE_CHECKING__getattr__()The Ultimate Solution Emerges, Ultimately
Ultimately, the solution is for @beartype to automate all of this on behalf of users. When a user activates a
beartype.claw- Detect the reference antipattern.
typing.TYPE_CHECKING - Issue one non-fatal warning for each block containing one or more hidden imports.
if typing.TYPE_CHECKING: - Automatically "repair" the antipattern by injecting one global-scoped @beartype forward reference per hidden import into the AST of the current module.
That is, @beartype will transform:
if typing.TYPE_CHECKING: from A.B import C, D from beartype._check.forward.reference.fwdref import BeartypeForwardReference C = BeartypeForwardReference('A.B', 'C') D = BeartypeForwardReference('A.B', 'D')
In the above example,
CDisinstance()issubclass()Fun stuff. I vomit. :vomiting_face:
Ho, ho, ho. Merry Christmas to all and to all a good QA! It's wonderful that something finally broke in @beartype for somebody. This repository has been so depressingly quiet lately. I miss Them Good Ol' Days™ when @beartype used to break every minute and I got to talk to everybody for issues on end. :sob:
perfect_software_that_actually_works = lonely_software_devbro
I at least want to have a lil chat with @leycec :))
This is the nicest thing anyone's ever said to me. :smile:
Wait. There was an actual issue here, wasn't there? Let's just take a looksie here. Aaaaaaand... @TeamSpen210's right about everything, like he always is. :laughing:
Second Way Really Sucks
In particular:
@lru_cache @staticmethod def _heavy_func(x):
Right. Python hates that, unfortunately. As @TeamSpen210 suggests,
@staticmethodFirst Way Kinda Sucks, Too
@staticmethod @lru_cache def _heavy_func(x):
@beartype hates that, huh? Technically, @beartype and @TeamSpen210 aren't wrong.
@lru_cache__call__()@staticmethodTechnicalities Suck, Too (So, Everything Sucks)
That said, technicalities suck. It'd be nice if this use case "just worked out of the box" even if it doesn't particularly make sense when you squint at the low-level details. Nobody really cares about the low-level details, anyway. They just want this madness called "Python" to just work for once.
This means this isn't quite a bug, really. More of a feature request, right? Sadly, this might take me a bit of time to hack on. I'm currently hip-deep in finally implementing deep type-checking support for
Iterable[...]Hacks for Great Glory of Turkey
@TeamSpen210's erudite hack of just disabling type-checking for
_heavy_func()from beartype import beartype, BeartypeConf, BeartypeStrategy class A: def some_func(self, x): return self._heavy_func(x) # Should work. Does it? No idea, bro. May Santa bless this code. @beartype(conf=BeartypeConf(strategy=BeartypeStrategy.O0)) @staticmethod @lru_cache def _heavy_func(x): # do some work return 42
That works – sorta. But as you astutely recognize, @ilyapoz (aka his majesty, aka unkulunkulu, aka The Man with the Magnificent Hat), disabling type-checking sucks.
Can you get what you want without disabling type-checking? Sure! No problem. You just have to do what @leycec does. Ignore
@lru_cache@lru_cacheBehold: A Caching Decorator So Fast It Could Blind You
from collections.abc import Callable from functools import wraps SENTINEL = object() ''' Sentinel object of arbitrary value. This object is internally leveraged by various utility functions to identify erroneous and edge-case input (e.g., iterables of insufficient length). ''' def callable_cached[T](func: Callable[T]) -> Callable[T]: ''' **Memoize** (i.e., efficiently re-raise all exceptions previously raised by the decorated callable when passed the same parameters (i.e., parameters that evaluate as equals) as a prior call to that callable if any *or* return all values previously returned by that callable otherwise rather than inefficiently recalling that callable) the passed callable. Specifically, this decorator (in order): #. Creates: * A local dictionary mapping parameters passed to this callable with the values returned by this callable when passed those parameters. * A local dictionary mapping parameters passed to this callable with the exceptions raised by this callable when passed those parameters. #. Creates and returns a closure transparently wrapping this callable with memoization. Specifically, this wrapper (in order): #. Tests whether this callable has already been called at least once with the passed parameters by lookup of those parameters in these dictionaries. #. If this callable previously raised an exception when passed these parameters, this wrapper re-raises the same exception. #. Else if this callable returned a value when passed these parameters, this wrapper re-returns the same value. #. Else, this wrapper: #. Calls that callable with those parameters. #. If that call raised an exception: #. Caches that exception with those parameters in that dictionary. #. Raises that exception. #. Else: #. Caches the value returned by that call with those parameters in that dictionary. #. Returns that value. Caveats ------- **The decorated callable must accept no keyword parameters.** While this decorator previously memoized keyword parameters, doing so incurred significant performance penalties defeating the purpose of caching. This decorator now intentionally memoizes *only* positional parameters. **The decorated callable must accept no variadic positional parameters.** While memoizing variadic parameters would of course be feasible, this decorator has yet to implement support for doing so. **The decorated callable should not be a property method** (i.e., either a property getter, setter, or deleter subsequently decorated by the :class:`property` decorator). Technically, this decorator *can* be used to memoize property methods; pragmatically, doing so would be sufficiently inefficient as to defeat the intention of memoizing in the first place. Efficiency ---------- For efficiency, consider calling the decorated callable with only: * **Hashable** (i.e., immutable) arguments. While technically supported, every call to the decorated callable passed one or more unhashable arguments (e.g., mutable containers like lists and dictionaries) will silently *not* be memoized. Equivalently, only calls passed only hashable arguments will be memoized. This flexibility enables decorated callables to accept unhashable PEP-compliant type hints. Although *all* PEP-noncompliant and *most* PEP-compliant type hints are hashable, some sadly are not. These include: * :pep:`585`-compliant type hints subscripted by one or more unhashable objects (e.g., ``collections.abc.Callable[[], str]``, the `PEP 585`_-compliant type hint annotating piths accepting callables accepting no parameters and returning strings). * :pep:`586`-compliant type hints subscripted by an unhashable object (e.g., ``typing.Literal[[]]``, a literal empty list). * :pep:`593`-compliant type hints subscripted by one or more unhashable objects (e.g., ``typing.Annotated[typing.Any, []]``, the :attr:`typing.Any` singleton annotated by an empty list). **This decorator is intentionally not implemented in terms of the stdlib** :func:`functools.lru_cache` **decorator,** as that decorator is inefficient in the special case of unbounded caching with ``maxsize=None``. Why? Because that decorator insists on unconditionally recording irrelevant statistics like cache misses and hits. While bounding the number of cached values is advisable in the general case (e.g., to avoid exhausting memory merely for optional caching), parameters and returns cached by this package are sufficiently small in size to render such bounding irrelevant. Consider the :func:`beartype._util.hint.pep.utilpeptest.is_hint_pep_type_typing` function, for example. Each call to that function only accepts a single class and returns a boolean. Under conservative assumptions of 4 bytes of storage per class reference and 4 byte of storage per boolean reference, each call to that function requires caching at most 8 bytes of storage. Again, under conservative assumptions of at most 1024 unique type annotations for the average downstream consumer, memoizing that function in full requires at most 1024 * 8 == 8096 bytes or ~8Kb of storage. Clearly, 8Kb of overhead is sufficiently negligible to obviate any space concerns that would warrant an LRU cache in the first place. Parameters ---------- func : Callable[T] Callable to be memoized. Returns ------- Callable[T] Closure wrapping this callable with memoization. Raises ------ ValueError If this callable accepts a variadic positional parameter (e.g., ``*args``). ''' assert callable(func), f'{repr(func)} not callable.' # Dictionary mapping a tuple of all flattened parameters passed to each # prior call of the decorated callable with the value returned by that call # if any (i.e., if that call did *NOT* raise an exception). args_flat_to_return_value: dict[tuple, object] = {} # get() method of this dictionary, localized for efficiency. args_flat_to_return_value_get = args_flat_to_return_value.get # Dictionary mapping a tuple of all flattened parameters passed to each # prior call of the decorated callable with the exception raised by that # call if any (i.e., if that call raised an exception). args_flat_to_exception: dict[tuple, Exception] = {} # get() method of this dictionary, localized for efficiency. args_flat_to_exception_get = args_flat_to_exception.get @wraps(func) def _callable_cached(*args): f''' Memoized variant of the {func.__name__}() callable. See Also -------- :func:`.callable_cached` Further details. ''' # Object representing all passed positional arguments to be used as the # key of various memoized dictionaries, defined as either... args_flat = ( # If passed only one positional argument, minimize space consumption # by flattening this tuple of only that argument into that argument. # Since tuple items are necessarily hashable, this argument is # necessarily hashable and thus permissible as a dictionary key; args[0] if len(args) == 1 else # Else, one or more positional arguments are passed. In this case, # reuse this tuple as is. args ) # Attempt to... try: # Exception raised by a prior call to the decorated callable when # passed these parameters *OR* the sentinel placeholder otherwise # (i.e., if this callable either has yet to be called with these # parameters *OR* has but failed to raise an exception). # # Note that: # * This statement raises a "TypeError" exception if any item of # this flattened tuple is unhashable. # * A sentinel placeholder (e.g., "SENTINEL") is *NOT* needed here. # The values of the "args_flat_to_exception" dictionary are # guaranteed to *ALL* be exceptions. Since "None" is *NOT* an # exception, disambiguation between "None" and valid dictionary # values is *NOT* needed here. Although a sentinel placeholder # could still be employed, doing so would slightly reduce # efficiency for *NO* real-world gain. exception = args_flat_to_exception_get(args_flat) # If this callable previously raised an exception when called with # these parameters, re-raise the same exception. if exception: raise exception # pyright: ignore # Else, this callable either has yet to be called with these # parameters *OR* has but failed to raise an exception. # Value returned by a prior call to the decorated callable when # passed these parameters *OR* a sentinel placeholder otherwise # (i.e., if this callable has yet to be passed these parameters). return_value = args_flat_to_return_value_get(args_flat, SENTINEL) # If this callable has already been called with these parameters, # return the value returned by that prior call. if return_value is not SENTINEL: return return_value # Else, this callable has yet to be called with these parameters. # Attempt to... try: # Call this parameter with these parameters and cache the value # returned by this call to these parameters. return_value = args_flat_to_return_value[args_flat] = func( *args) # If this call raised an exception... except Exception as exception: # Cache this exception to these parameters. args_flat_to_exception[args_flat] = exception # Re-raise this exception. raise exception # If one or more objects either passed to *OR* returned from this call # are unhashable, perform this call as is *WITHOUT* memoization. While # non-ideal, stability is better than raising a fatal exception. except TypeError: #FIXME: If testing, emit a non-fatal warning or possibly even raise #a fatal exception. In either case, we want our test suite to notify #us about this. return func(*args) # Return this value. return return_value # Return this wrapper. return _callable_cached # type: ignore[return-value]
It's trivial, I swear. The 5,000 lines of documentation makes it look non-trivial. Feel free to:
- Strip out the docstrings and comments.
- Relicense that under any license you like.
Given that, the following should now work for your use case right now. You could wait for the next @beartype release – or you could just do this now:
from beartype import beartype, BeartypeConf, BeartypeStrategy class A: def some_func(self, x): return self._heavy_func(x) # Should work. Does it? No idea, bro. May Santa bless this code. @staticmethod @callable_cached # <-- no @lru_cache, no cry def _heavy_func(x): # do some work return 42
:partying_face: :santa: :christmas_tree:
Woooooooah. Indeed, I see terrifying – yet ultimately justifiable – shenanigans that sadden me. In the absence of a standardized plugin system for runtime-static type-checkers generically supported by both @beartype and
typeguard@beartype and
typeguardjaxtypingjaxtypingjaxtyping.Float[...]My only wish for 2024 is to stop failing
jaxtypingSo inspiring! I... I can't believe the turn-around on FastMCP issues. The @beartype issue tracker is littered with the detritus of ancient cruft that everyone's long since given up on. I'm suddenly inspired to be more like everybody here.
My wife and I adore Claude 4.5 Sonnet (both Code and Chat). 127 IQ is no laughing matter. I stopped laughing when Claude politely suggested I was dumb. Claude was right. In this case, though, Claude seems... not right. I mean, sure. Technically, there is non-trivial magical dunder work needed to smooth the descriptor approach out. But when isn't there? This is Python. It's turtles and non-trivial magical dunder work all the way down the line.
The user workarounds needed to circumvent FastMCP's method decorator caveats are more surprising than any number of magical dunders, I'd say. User experience is primary. Everything else is secondary. Anything that can reduce the cognitive load and maintenance burden of using FastMCP can only be a good thing.
Of course, I'm also a bald middle-aged man at a lakeside cabin in the Canadian wilderness. It's best not to believe people like me.
In the above, my_tool has no binding or relationship to
because it needs to be accessed (literally callingMyClass) in order to trigger the descriptor logic.MyClass.my_tool
I... uhh. Conveniently neglected to mention that part. Okay. I forgot.
There are tons of obscure (meta)class dunder methods (like the little-known
__prepare__()fastmcp.ToolableLet the ABC take care of the nitty-gritty minutiae and most users would be peachy-keen.
I think the other part of the challenge with instance methods is that instantiating more than one instance gives us a conflict problem where tools have the same name.
I never thought that far ahead. Everyone here is already five steps into the future.
But doesn't the current decorator method workaround have the same exact issue? Maybe nobody ever tried to do this before:
from fastmcp import FastMCP mcp = FastMCP() class MyClass: def add(self, x, y): return x + y foo = MyClass() bar = MyClass() mcp.tool(foo.add) mcp.tool(bar.add) # <-- probably not a good thing
How does FastMCP handle that conflict at the moment? Just die and hope for the best, I suspect. That's fine under the existing workflow, but when you start dundering around with magical ABCs,
fastmcp.Toolable- Detect whether an -decorated method has already been
@mcp.tool-ized by a prior instance.Tool - If so, just... what? Oh, right. This could be yet another option in the FastMCP configuration. A few obvious options arise:
- Default to raising an exception. Safety first. 🦺
- Allow users to silently ignore all but the first instantiation. The first object instantiated wins. Every other object's tools are ignored.
- Allow users to silently ignore all but the last instantiation. The last object instantiated wins. Every other object's tools are ignored.
Thanks so much for humouring me, everyone. This pit of doom dunder magic is splendid. Fun is intensifying. I can feel it in my aching arthritic bones. Best of luck with all this fun! I now retreat to the sidelines, where @beartype is calling her siren song. 🧜♀️
I... have no idea what you're talking about. I want to. Yet, I'm clueless. Are you perhaps accidentally submitting to a different GitHub repository than intended? For my own personal sanity amidst an explosion of unresolved issues, let's quietly triage this "Closed."
Apologies if I've misunderstood and there is an actual issue here. Until then, we play video games on a Sunday evening. 🎮
...finally. With the recent release of @beartype 0.21.0
typePython 3.14 + PEP 649 is now up! Let us do this. Thanks so much yet again for this phenomenal pull request, @JelleZijlstra. I sincerely apologize for not merging this immediately. Since a giant pile of non-trivial merge conflicts has erupted, I'll now be merging this manually. I'm rolling up my sleeves. This is gonna get greasy.
Without further ado, Leonardo DiCaprio will now announce what is about to happen.
<sup>PEP 649: pretty sure it's that intense</sup>
Aaaaaaaand... we're done. a4924c3 is it, surprisingly. The
@typer.Typer.commandtyping.Annotated[...]Let us now slump exhaustedly over our keyboards and wait for incoming @beartype 0.22.0rc0
<sup>the @beartype
0.22.0Ho, ho... Tangled.sh! What is this mystic decentralized madness? Looks like great fun. I'm a huge fan of minimalist Craigslist-like UIs. And... I'm getting those vibes here. Big respect. 😄
If I was less antiquated, crusty, or bald, I'd hop onto the Tangled.sh bandwagon like a
gitgitWow. You're... stunning!
I'm working on a type system for a machine learning code base
Yes! Yes! So much "Yes!" ML-AI-LLM type systems is, indeed, the way. Let us tame this horrifying beast of Wild West-style bugginess. Carnegie Mellon getting its money's worth right here.
Thanks so much for that deep dive into @patrick-kidger's hot new
jaxtypingjaxtypingjaxtyping... especially when weird proprietary data gets involved
😄 😆 🤣 😅 😢 😭
Now, if only @beartype would do something and help
jaxtypinganything is an improvement over the status quo (nothing is typed, nothing is checked).
You... are... so... funny. It's all true, of course. That's why it's funny. But it's also sad, which also tends to be the case with funny stuff. You'd think the status quo would have long since moved past Ye Old Duck Typing to something that actually works. Yet here we are.
Type-checking: still just for the outlier devs after all these decades, huh? 😩
They also have a nice property that they can be declared independently and used interchangeably like protocols, which helps when multiple projects want to share interfaces, but don't want to agree on a shared dependency.
That's... absolutely true. Honestly, I never thought of that. That's a compelling argument in favour of the permissive API style that
TypedDictBy popular demand, I've been deprioritizing deep type-checking of
TypedDictNamedTuple@dataclassTypedDictTypedDictI've internally detailed a solid plan to support generic
TypedDictNamedTuple@beartype: if you know, you know. 😉
Adorable. What a cute
uvxbeartype-cliuvxNow... somebody just needs to write
beartype-cliOh, ho... That's a good one. Well, that's a terrible one – but a fascinating one nonetheless. Thankfully, this is a trivial issue for me to resolve unlike some other issues I could mention. <sup>*cough, cough* issue #519 *cough, cough*</sup>
If you're curious what's happening here, @beartype now type-checks type variable bounds and constraints (i.e., the
Anytyping.TypeVar(bound=Any)@beartypetyping.Anytyping.Anytyping.AnyInterestingly,
typing.Anytyping._SpecialFormtyping.Anytyping.AnyAnyways, I Am Babbling
I'll get this all patched up tonight. Sorry about the momentary breakage, @mzealey. Please accept this meme by way of apology.
<sup>now @beartype knows this feeling too</sup>
Oh, ho! The prodigal @beartype 0.19.0 regression is back, huh? Interestingly, @beartype 0.20.0 appears to be right about this. Not quite sure what @beartype 0.19.0 was smoking – but it smells bad and I want none of it. 😆
According to @beartype 0.20.0, you now have two equally valid choices:
-
Type
as accepting afoo()type hint instead. This preserves the existing functionality with a more appropriate type hint and is thus probably your best bet: e.g.,contextlib.AbstractAsyncContextManager[Test]from contextlib import AbstractAsyncContextManager, asynccontextmanager def foo(b: AbstractAsyncContextManager[Test]) -> None: pass -
Drop the
: e.g.,@asynccontextmanagerasync def test_session() -> t.AsyncGenerator[Test, None]: yield Test() -
Invoke
with a standardfoo(), just as you astutely suggested. Actually, I tried that – but it didn't work at all. Theasync withfails for reasons I don't quite understand but probably have something to do with theasync withobject: e.g.,fastapi.Depends()@app.get("/") async def test(a: t.Annotated[Test, Depends(test_session)]) -> None: async with test_session as b: foo(b)
...which raises:
File "/home/leycec/tmp/mopy.py", line 27, in test async with test_session as b: ^^^^^^^^^^^^ TypeError: 'function' object does not support the asynchronous context manager protocol
That's... surprising. Why isn't
test_sessionDepends()Anyyyyyway. This kinda looks like other people's problems, which is @beartype's favourite kind of problem. Let us quietly close this and pretend everything works. 😅
...lol. Apologies for the frivolous goose chase. Indeed, it seems like we're outta spec here. It doesn't help that the "spec" is (as you say) actually five specs distributed across a plethora of different sites. CPython devs: "Clean up your room, please."
requires-python = ">=3.9,!=3.14rc1,!=3.14rc2"Aww. Thanks so much for such wonderful words, @kultura-luke. Positive encouragement when it's pouring cats and dogs outside means a ton. How could it be so cold in August already? What happened to the summer!? Oh, Gods... Canadian weather sure is a thing. 😂
This is a super-fun issue, surprisingly. On the off-hand chance that you're curious why this is non-trivial, it's because there's a long-standing bug in CPython's standard
typingfrom typing import TypedDict, Generic, TypeVar T = TypeVar('T') class MuhTypedDict(TypedDict, Generic[T]): muh_key: T muh_typed_dict = MuhTypedDict(muh_key='So much key. OMG!') isinstance(muh_typed_dict, MuhTypedDict)
In a sane world, that
isinstance()TrueThat
isinstance()Traceback (most recent call last): File "/home/leycec/tmp/mopy.py", line 52, in <module> isinstance(muh_typed_dict, MuhTypedDict) ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^ File "/home/leycec/py/pyenv/versions/3.14.0b3/lib/python3.14/typing.py", line 3199, in __subclasscheck__ raise TypeError('TypedDict does not support instance and class checks') TypeError: TypedDict does not support instance and class checks
Super-weird. I can't think of any other real-world examples of a type that you can instantiate but can't pass to
isinstance()Thanks so much again for the kind words! We'll patch this up in a jiffy. You the bear bestie. 🤗
Part of a startup doing something completely different these days!
That's wonderful! Bay Area startups are so fun. Aside from the death march, perpetual crunch, and tenuous paychecks, I have fond memories. Startups are where the meritocracy and thus all the fun is.
Congrats on parachuting out to some super-hot founder entrepreneur stuff. You will achieve all your dreams.
We actually explicitly need support from the runtime typechecker to correctly fork the state across union calls.
My eyes are bleeding. 😆
Thanks so much for that detailed example. Indeed, I never would've gotten there on my eye. I confess that I am thick in the head and thus still do not understand. I think this line:
state = simple_typechecker(arg, value, state) # <-- `state` only rebound on success, failed calls mean later calls still get the old `state`.
...should have instead been a recursive call to
simple_stateful_typechecker()state = simple_stateful_typechecker(arg, value, state) # <-- `state` only rebound on success, failed calls mean later calls still get the old `state`.
Is that right? Interestingly, nothing like
simple_stateful_typechecker()From Non-stateful isinstance()
to Stateful Type-checking
isinstance()Under the hood, @beartype is a dynamic code generator. For each parameter of a
@beartypeiffrom beartype import beartype, BeartypeConf @beartype(conf=BeartypeConf(is_debug=True)) def kooky_func(kinky_arg: int | set[str] | list[dict[str, bytes]]) -> None: ...
...which prints:
(line 0001) def kooky_func( (line 0002) *args, (line 0003) __beartype_getrandbits=__beartype_getrandbits, # is <built-in method getrandbits of Random object at 0x5615136b7780> (line 0004) __beartype_get_violation=__beartype_get_violation, # is <function get_func_pith_violation at 0x7f5e0f25d850> (line 0005) __beartype_conf=__beartype_conf, # is "BeartypeConf(is_debug=True)" (line 0006) __beartype_object_140041981804928=__beartype_object_140041981804928, # is <class 'NoneType'> (line 0007) __beartype_check_meta=__beartype_check_meta, # is <beartype._check.metadata.metacheck.BeartypeCheckMeta object at 0x7f5e0f26d840> (line 0008) __beartype_func=__beartype_func, # is <function kooky_func at 0x7f5e0f4650c0> (line 0009) **kwargs (line 0010) ): (line 0011) # Generate and localize a sufficiently large pseudo-random integer for (line 0012) # subsequent indexation in type-checking randomly selected container items. (line 0013) __beartype_random_int = __beartype_getrandbits(32) (line 0014) # Localize the number of passed positional arguments for efficiency. (line 0015) __beartype_args_len = len(args) (line 0016) # Localize this positional or keyword parameter if passed *OR* to the (line 0017) # sentinel "__beartype_raise_exception" guaranteed to never be passed. (line 0018) __beartype_pith_0 = ( (line 0019) args[0] if __beartype_args_len > 0 else (line 0020) kwargs.get('kinky_arg', __beartype_get_violation) (line 0021) ) (line 0022) (line 0023) # If this parameter was passed... (line 0024) if __beartype_pith_0 is not __beartype_get_violation: (line 0025) # Type-check this parameter or return against this type hint. (line 0026) if not ( (line 0027) # True only if this pith is of one of these types. (line 0028) isinstance(__beartype_pith_0, int) or (line 0029) ( (line 0030) # True only if this pith is of this container type *AND*... (line 0031) isinstance(__beartype_pith_0, set) and (line 0032) # True only if either this container is empty *OR* this container (line 0033) # is non-empty and the selected item satisfies this hint. (line 0034) (not len(__beartype_pith_0) or isinstance(next(iter(__beartype_pith_0)), str)) (line 0035) ) or (line 0036) ( (line 0037) # True only if this pith is of this container type *AND*... (line 0038) isinstance(__beartype_pith_0, list) and (line 0039) # True only if either this container is empty *OR* this container (line 0040) # is non-empty and the selected item satisfies this hint. (line 0041) (not len(__beartype_pith_0) or ( (line 0042) # True only if this pith is of this mapping type *AND*... (line 0043) isinstance(__beartype_pith_1 := __beartype_pith_0[__beartype_random_int % len(__beartype_pith_0)], dict) and (line 0044) # True only if either this mapping is empty *OR* this mapping (line 0045) # is non-empty and... (line 0046) (not len(__beartype_pith_1) or ( (line 0047) # Localize the first key of this mapping. (line 0048) (__beartype_pith_2 := next(iter(__beartype_pith_1))) is __beartype_pith_2 and (line 0049) # True only if this key satisfies this hint. (line 0050) isinstance(__beartype_pith_2, str) and (line 0051) # True only if this value satisfies this hint. (line 0052) isinstance(__beartype_pith_1[__beartype_pith_2], bytes))) (line 0053) )) (line 0054) ) (line 0055) ): (line 0056) __beartype_violation = __beartype_get_violation( (line 0057) check_meta=__beartype_check_meta, (line 0058) pith_name='kinky_arg', (line 0059) pith_value=__beartype_pith_0, (line 0060) random_int=__beartype_random_int, (line 0061) ) (line 0062) (line 0063) raise __beartype_violation (line 0064) # Call this function with all passed parameters and localize the value (line 0065) # returned from this call. (line 0066) __beartype_pith_0 = __beartype_func(*args, **kwargs) (line 0067) (line 0068) # Noop required to artificially increase indentation level. Note that (line 0069) # CPython implicitly optimizes this conditional away. Isn't that nice? (line 0070) if True: (line 0071) # Type-check this parameter or return against this type hint. (line 0072) if not isinstance(__beartype_pith_0, __beartype_object_140041981804928): (line 0073) __beartype_violation = __beartype_get_violation( (line 0074) check_meta=__beartype_check_meta, (line 0075) pith_name='return', (line 0076) pith_value=__beartype_pith_0, (line 0077) ) (line 0078) (line 0079) raise __beartype_violation (line 0080) return __beartype_pith_0
Crucially, this is the
if(line 0025) # Type-check this parameter or return against this type hint. (line 0026) if not ( (line 0027) # True only if this pith is of one of these types. (line 0028) isinstance(__beartype_pith_0, int) or (line 0029) ( (line 0030) # True only if this pith is of this container type *AND*... (line 0031) isinstance(__beartype_pith_0, set) and (line 0032) # True only if either this container is empty *OR* this container (line 0033) # is non-empty and the selected item satisfies this hint. (line 0034) (not len(__beartype_pith_0) or isinstance(next(iter(__beartype_pith_0)), str)) (line 0035) ) or (line 0036) ( (line 0037) # True only if this pith is of this container type *AND*... (line 0038) isinstance(__beartype_pith_0, list) and (line 0039) # True only if either this container is empty *OR* this container (line 0040) # is non-empty and the selected item satisfies this hint. (line 0041) (not len(__beartype_pith_0) or ( (line 0042) # True only if this pith is of this mapping type *AND*... (line 0043) isinstance(__beartype_pith_1 := __beartype_pith_0[__beartype_random_int % len(__beartype_pith_0)], dict) and (line 0044) # True only if either this mapping is empty *OR* this mapping (line 0045) # is non-empty and... (line 0046) (not len(__beartype_pith_1) or ( (line 0047) # Localize the first key of this mapping. (line 0048) (__beartype_pith_2 := next(iter(__beartype_pith_1))) is __beartype_pith_2 and (line 0049) # True only if this key satisfies this hint. (line 0050) isinstance(__beartype_pith_2, str) and (line 0051) # True only if this value satisfies this hint. (line 0052) isinstance(__beartype_pith_1[__beartype_pith_2], bytes))) (line 0053) )) (line 0054) ) (line 0055) ):
In other words, @beartype mostly just generates
isinstance()isinstance()isinstance()isinstance()bool- if the parameter satisfies its type hint.
True - otherwise.
False
That works for literally everything I've seen, including really strange and weird type hints and objects. Note the lack of recursion anywhere. @beartype explicitly does not perform recursion to type-check anything. That's good, because recursion sucks in Python. Python allocates recursive stack frames on a stack rather than a heap (which means it tends to exhaust the stack a lot faster than in competing languages). Moreover, function calls are extremely costly in Python, which means recursion is extremely costly in Python. Moreover, Python didn't have tail-recursion optimizations until Python 3.14. (Thank you, Python 3.14! Thank you so much!)
Problems: Problems Everywhere!
But...
jaxtypingisinstance():=jaxtyping(success, state := hint.__instancecheck_stateful__(value, state)ifWhat if the
statehint.__instancecheck_stateful__()__bool__()statesuccess# Type-check this parameter or return against this type hint. if not ( # Type-check other stuff in this type hint if need be. Boooooooring. ... # Type-check a "jaxtyping" child type hint subscripting this parent type hint! # Generated code doesn't need to check whether this "jaxtyping" hint defines # the __instancecheck_stateful__() dunder method, because the @beartype # algorithm generating this code already checked that. Which leaves us with... (state := hint.__instancecheck_stateful__(value, state)) and # Type-check other stuff in this type hint if need be. Boooooooring. ... ):
That's still probably not quite right, though.
jaxtypingstateThe worst of it is that @beartype has no idea. Where does
statestatestate@beartypestatePretty rough stuff. Kinda seems like
jaxtypingSuper-duper. Anything you come up with is perfect! Thanks for digging deep into the wacky
pytest...woah. AI slop. I actually love AI slop. The autism in me acknowledges the autism in AI. I'm delighted and deeply honoured that AI even knows that @beartype exists. That's something I never thought I'd see. Thank you, AI. This robot emoji is for you. 🤖
Sadly, AI doesn't quite have the best advice here. Option:
- Moving type-checking to a wrapper function isn't the worst possible thing that you can do (unlike Option 2.), but it's not exactly the best possible thing that you can do either. Most users implicitly type-check everything with . Technically, you can make that work with Option 1. – but to do so you need to manually do even more stuff like importing
beartype.claw.beartype_this_package()and then decorating your inner private@typing.no_type_checkfunction with that. It works (sorta), but it's crude, clumsy, and inefficient. 🤭_map_records_impl() - Replacing your type hints with is the worst possible thing that you can do, because that destroys your type hints. What's the point, right? You'd might as well just delete your type hints entirely and be done with it. So... that's not great. 🤮
typing.Any
Thankfully, the
typing.TYPE_CHECKINGfrom beartype import beartype from typing import TYPE_CHECKING, TypeVar import ray from ray._private.worker import RemoteFunction0 # <-- this is the 🦆 T = TypeVar("T") R = TypeVar("R) # If this module is currently being statically type-checked, # define type hints suitable for static type-checking. Yowza. if TYPE_CHECKING: RemoteFunction0RT = RemoteFunction0[R, T] # Else, this module is currently being runtime type-checked. # Define type hints suitable for runtime type-checking! Yikes. else: # Ignore this type hint at runtime, because the third-party # "ray._private.worker.RemoteFunction0" type hint factory # fails to support runtime type-checking. YIIIIIIIIIIIKES. RemoteFunction0RT = object # <-- lolbro @beartype def map_records( fn: RemoteFunction0RT, records: Sequence[T] ) -> R: ... @ray.remote def mapper(item: int) -> str: return str(item) # this type checks because @ray.remote is typed to return RemoteFunction0 # but fails at runtime because `beartype.infer_hint(mapper)` is actually just Callable # (or for other similar reasons) string_items = map_records(mapper, [0, 1, 2, 3])
No need for inefficient and awkward wrapper functions. No need for type hint destruction via
typing.Any@beartype: 'cause AI don't know it's paw from its paw-paw. 🥝 <sup><-- this is not a paw, AI</sup>
Ho, ho. It's @beartype's favourite computer vision roboticizer! And... he's back with a horrifying new issue I can't do much about. I'm not quite certain what "the injected request/evt parameters get stolen away by the @beartype decorator" means, but I am quite certain that Gradio is behaving suspiciously. Sadly...
is there some sort of "exclude" parameters that I could use with from BeartypeConf?
Not yet. We all feel sadness about this. All there is is a recent feature request from two weeks ago where somebody else also desperately begs for an ignore_arg_names
What the @beartype people want, the @beartype people get. Until the people get what they deserve, the best that you can do is Poor Man's Ignore Parameter Technique™:
from typing import TYPE_CHECKING # If we're statically type-checking under mypy or pyright, always tell the truth. if TYPE_CHECKING: request_hint = r.Request evt_hint = SelectionChange # Else, we're runtime type-checking under @beartype. In this case, LIE, LIE, LIE!!! # Specifically, ignore all problematic parameters. @beartype doesn't see what # @beartype doesn't want to see. else: request_hint = object evt_hint = object def register_keypoint( active_recording_id: str, current_timeline: str, current_time: float, request: request_hint, evt: evt_hint, ): ...
If that still doesn't work, I'm afraid you have no recourse but to enter a dangerous hibernal dream state in cryogenic storage for fifteen years. You will be known globally as Sleeping Pablo Vela Beauty. When you wake up, perhaps things will be better. 😅
Ho, ho... ho.
nptyping@davidfstr: @beartype is now down to only 31 air-quoted "errors" against this PR. Hallelujah! We give praise to Guido. That said, more than a few of these "errors" appear to be false positives in this PR's current implementation of
typing.TypeForm-
Failing to match
as a validtyping.Any.typing.TypeFormhad better be a valid type hint. Right? I will eat my own mustache ifAnyisn't a valid type hint. Everywhere @beartype attempts in vain to returnAnyfrom a function annotated as returningAny, this PR loudly complains that:TypeForm[Any]beartype/_check/convert/_convcoerce.py:191: error: Incompatible return value type (got "<typing special form>", expected "TypeForm[Any]") [return-value]
I'll report back if I find any other stragglers. Altogether and all in all, though, I'm exceptionally impressed with this madness. Thanks so much for all your valiant efforts here! Together, we'll build a better type-checker. And it shall be known as...
mybeartypepy....heh. So, it turns out this is technically a duplicate of feature request #128. Resolving this issue effectively means implementing full-blown support for recursive type hints. That's not a bad thing, though. That's a great thing! We've been meaning to do this for literally years, but never did because this feature request scares me. I am not afraid to admit the truth.
It's time to face my fears. I have no choice. @beartype is now blowing up on recursive type hints. Let's rock this.
<sup>unsure exactly what is happening here... but @leycec likes it</sup>
lol. Truly, IPython is playing with madness:
...the root cause is that
is determined incorrectly, while most other properties of the function object are correct. Incorrectness ofco_firstlinenoactually leads to the wholeco_firstlinenobeing incorrect.__code__
I tracked down the issue to the level of the IPython magic: everything breaks if we attempt to add any decorator to the
transformations list. The most interesting part is that adding even the pointer-copying decorator fails, even if wrapped withastorfunctools.wraps.wrapt
The body blows just keep coming.
<sup>ipython devs: "because we dgaf"</sup>
OMG! Thanks so much for the deep-dive on PEP 695. I truly thought @beartype had that nailed down to the floor at this point. Clearly, @beartype's work is still cut out for it. PEP 695 is like a summer camp slasher fic: it's wearing a hockey mask, it's holding an axe, it's breathing heavily, and it just keeps on coming. 😷
I'll promptly attend to this someday. Tragically, this delays our upcoming @beartype
0.20.0rc10.20.0rc1with maybe a little bit of snow?
...we got nuthin', man! A whole boatload of nuthin'. What is this, Jamaica? Because this feels like Jamaica.
Hey just before doing this release, did you have time to check my comment on #311?
Gah! So sorry about that. Your awesome response somehow dropped right off my radar. Clearly, I need a new radar. The one in our basement appears to be broken. :sob:
I totally see where you're coming from there. Thankfully, Python ≥ 3.12 + @beartype 0.17.0 does actually provide a standardized solution for what you want to do: PEP 695-compliant type
typeTo quote the official article on type
Type aliases are useful for simplifying complex type signatures. For example:
from collections.abc import Sequence type ConnectionOptions = dict[str, str] type Address = tuple[str, int] type Server = tuple[Address, ConnectionOptions] def broadcast_message(message: str, servers: Sequence[Server]) -> None: ... # The static type checker will treat the previous type signature as # being exactly equivalent to this one. def broadcast_message( message: str, servers: Sequence[tuple[tuple[str, int], dict[str, str]]]) -> None: ...
For your use case of the complex
numpy.typing.ArrayLiketypeArrayLikenumpy.typing.ArrayLikefrom numpy.typing import ArrayLike as np_ArrayLike # Simply type alias of a complex type hint: arise, you fiend! Arise to glory! type ArrayLike = np_ArrayLike
What's the Catch, Bub?
Two catches:
- This requires Python ≥ 3.12. When I say "This requires Python ≥ 3.12," I mean really requires. If you even try to define a alias in Python < 3.12, you'll get a non-human-readable
type– even if you try to hide thatSyntaxErroralias from older Python versions withtypeconditionals likeif. Hiding doesn't work. You either need to:if version_info < (3, 12):- Isolate all aliases to a unique submodule imported only under Python ≥ 3.12.
type - Dynamically declare aliases with the
typebuiltin. This is the lazy way. Thus, this is what we do below.exec()
- Isolate all
- This requires @beartype ≥ 0.17.0, which has yet to be officially released. <sup>...heh.</sup>
You are now thinking: "
typeLet's gooooooooooooo:
from numpy.typing import ArrayLike as np_ArrayLike from sys import version_info from typing import TYPE_CHECKING # If we're being statically type-checked, just declare a type alias directly. if TYPE_CHECKING: type ArrayLike = np_ArrayLike # If the current Python interpreter ≥ 3.12, dynamically declare a type alias to # avoid "SyntaxError" complaints from older Python interpreters. elif version_info >= (3, 12): exec('type ArrayLike = np_ArrayLike') # <-- don't ask. don't tell. # Else, the current Python interpreter sucks. In this case, define a stupid # obsolete old-school type alias with deprecated "typing" attributes. Just. Do. It. else: from typing import TypeAlias # <-- deprecated, but who cares ArrayLike: TypeAlias = np_ArrayLike
...heh. What could be simpler, huh? :face_with_spiral_eyes:
Ho, ho, ho. Merry 2025, fellow Nintendude @rbroderi! With the power of friendship, Gannon is going down this time. <sup>...yeah right. gannon'll just be back in five minutes. we all know it. no real point in even defeating that guy, is there? dude has more lives than a Maine Coon cat hopped up on catnip.</sup>
Did you perhaps mean
die_if_unbearable()is_bearable()FalseAlso, there's so much insanity happening in this example that I'm at a genuine loss for words. Actually, that's a lie. I'm still typing hyperbolic verbosity like a Maine Coon cat hopped up on catnip. Still, I kinda have no idea what's even going on in that example.
What's
win32com.client.Dispatch("Word.Application")_WordOh – and
typing.cast()# Pretty sure this is the same thing... just faster and easier. word: _Word = win32com.client.Dispatch("Word.Application")
This also has the benefit of being implicitly type-checked by @beartype – assuming you use
beartype.claw@Glinte: I'm so sorry about that appalling omission. Honestly, I never envisioned that there would ever be a valid use case for
hint_overrideshint_overrideshint_overridesClearly, I lacked vision. I was wrong. I understand and appreciate now that
hint_overridesHo, ho! Please feel no regrets. Like many, I am a code masochist. Unlike many, I blame autism. I love and adore these sorts of obscure QA puzzles. I spent my childhood solving an an endless litany of 80's dimestore paperback logic puzzles with unmarketable names like "ORIGINAL LOGIC PUZZLES". You couldn't pay most people to do those things. But I did those things... for reasons still unclear to anyone.
It's not about the end destination per say. Neither my wife or I need to subscript generics parametrized by type variable tuples in our own personal work. Most humans would therefore consider this wasted time. Thankfully, I am not most humans. One man's intellectual garbage is another man's tastiest gruel.
It's about the journey. The harder the journey, the funner the journey. This particular journey just refuses to submit and die already. Therefore, I keep banging my head against the keyboard. Eventually, something will give. Hopefully, that something isn't my head – or the precious sanity in my head. 😂
See you in a decade ;-)
It's a QA dinner date!
<sup>...on second thought, maybe not</sup>
Wait - so you mean that actually Beartype parses traditional string forward references...
...heh. Of course, silly! <sup>i say "silly" in the cheekiest way possible. no shade intended. @beartype loves its wonderful userbase! this especially includes aspiring, idealistic, young PhD candidates such as yourself who are on the cusp of positively improving this troubled world.</sup>
@beartype fully parses arbitrarily complex stringified type hints, including modern QA madness under Python ≥ 3.12 like:
from __future__ import annotations # <-- enable PEP 563, which implicitly stringifies everything def oh_my_gods[T]( # <-- enable PEP 695, which implicitly instantiates "T" into a "TypeVar" this_is_horrible: T | list[T] | MyUndefinedType) -> ( # <-- PEP 695 intensifies T | set[T] | MyOtherUndefinedType): # <-- more PEP 695, more horror ...
The above is effectively equivalent to this older syntax in older Python versions:
from typing import TypeVar T = TypeVar('T') def oh_my_gods( this_is_horrible: 'T | list[T] | MyUndefinedType') -> ( 'T | set[T] | MyOtherUndefinedType'): ...
@beartype then has to unparse those stringified type hints back into their constituent components. Crucially, this includes replacing the strings
'MyUndefinedType''MyOtherUndefinedType'__instancecheck__()__subclasscheck__()isinstance()issubclass()Basically, @beartype is crazy... in the best way possible. If it's a PEP standard, @beartype almost certainly supports it. Okay, probably supports. Okay, sometimes supports. 😅
Thanks again for playing along with @beartype. I'd love to document all of the eldritch darkness that @beartype is doing these days... but then I'd never get anything else done. I keep hoping somebody else will want to submit a pull request (PR) improving our documentation... but nobody wants to do that, either. Which is understandable. Nuthin' worse than writing docos on a free weekend when you could just be playing video games all night instead.
<sup>a typical saturday night at the @leycec household descends into madness</sup>
Sorry for the shameful delay! I am bad. You are all wonderful. I was busy beating TensorDict into submission, which I have now done to my glib satisfaction. Let's get dirty with dataclasses, everyone.
Interestingly, new polling data just in says everyone wants me to turn @beartype into a general-purpose competitor to Pydantic by improving our type-checking of
@dataclasses.dataclassBut... dataclasses frighten me. At least we have @TeamSpen210:
Dataclasses do have something for introspection - you can call
to get a tuple of field objects, telling you what fields were defined, their types, default values etc. With that you could probably wrapdataclasses.fields(), after it sets the attributes go and do each check.__init__
Yes! So much yes. I did not even know about this
dataclasses.fields()It also looks like @beartype probably wants to type-check fields inside a handrolled
__post_init__()__init__()__post_init__()__post_init__()@dataclasses.dataclassWAIT...
The Dataclass API Sure Is Somethin', Ain't It?
It looks like
__post_init__()@dataclass__init__()__post_init__()@beartype@dataclass__init__()@beartype__post_init__()@beartype__init____post_init__()So... yet again, I'm pretty sure @TeamSpen210 is right about everything. @beartype has no choice but to replace the existing
__init__()__init__()Beartype could potentially provide an implementation there which adds validators to all fields, which will make it check whenever the fields are assigned.
Hmmm... Maybe. Press "F" to doubt, though. Overriding the
__setattr__()Will @beartype ever do dataclasses? Tune in next month as @leycec passes out on a keyboard. 😴
Awwwwwwwww! You're so nice, @Glinte. I love it when my amazing userbase shares in the communal horror. Indeed, I may have suffered for us all and the sins that PEP 563 has committed, but my heart has already been steeled in the icy forge of twenty Canadian winters. ❄ 🌨 ☃ 🧊
PEP 563 can't compare to waking up with a beard full of icicles. If you know, you know.
<sup>in the utopian world I dream of, warm beards are free of icicles</sup>
...lol. Thanks so much for catching this really ugly edge case. You're the ultimate QA turtle. Did I mention that we love turtles where we live? Everybody does here. We live in the cold wetlands of Ontario, Canada – where mosquitoes breed by the billions. We've all got bumper stickers like "I stop for turtles." Your GitHub username warms my heart.
What were we talking about again? Oh. Right. Type mocking. Pretty bizarre stuff, huh? Static type-checkers aren't wrong about this, per say – but they're not necessarily right either. Overriding
__class__@propertymypypyrightYour initial implementation of
@__class__.setter__class__NotImplementedErrorIt's all baseless conjecture and pride-posturing at this point. Probably nobody cares about
@__class__.settermypypyrightLet us just ship your amazing fix. BOOM. It's in. This head exploding proves that you're awesome: :open_mouth: :exploding_head: :boom:
That is an honorable achievement
In a similar way, surviving a moose trampling is an honorable achievement.
We have a meme with my wife. I worked at a big tech company in my country and when I was coming late I was saying that "Yandex is broken and I need to fix it".
...oh, my aching sides. Stop! You're killing' me here! My ribs hurt. Seriously. They hurt.
That's the funniest thing I've heard all week – and I live with three smelly cats and a non-smelly wife in a cramped cabin in the woods. So, you know it's hijinx all day long.
Yandex is a delightful engine. I'm honoured to be supporting a former Yandex warrior. Whenever Google fails me, I roll up my sleeves, stick a fake cigar made of candy into my mouth, and start muttering orthodox catechisms to the search-engine Gods:
"Don't fail me now, Yandex. It's 3:27AM. Find the unfindable, you ugly bastard. You're @beartype's final hopium."
<sup>Yandex: even Power Rangers knows.</sup>