Replies: 7 comments 10 replies
-
Well I love the introduction, but don't like the body so much. I'll discuss with the team next week and get back to you. Either way, you should all come to the typing summit at pycon US, it works be nice to not be the only runtime wonk at the party. You should probably invite the msgspec guy too. He'll probably reply with a msgspec Vs pydantic benchmark, as that seems to be his fetish, but fetishists should be welcome too. |
Beta Was this translation helpful? Give feedback.
-
This doesn't work if |
Beta Was this translation helpful? Give feedback.
-
Blech. Thank you @leycec for this heads-up. Q1: mutating FWIW, in jaxtyping we're very careful never to mutate the fn.__annotations__['foo'] = bar because that would be mutating an object we don't own. We do mutate our own objects that sit inside of class MyJaxtypingClass:
pass
foo = fn.__annotations__['foo']
if isinstance(foo, MyJaxtypingClass):
foo.bar = baz When you talk about mutating Q2: PEP 563 Ech, you're right, this is about to come back with a vengeance. This is going to be an absolute nightmare to resolve -- PEP649 was supposed to finally end the difficulty of handling stringified hints, and instead it's just going to make things worse. Do you think we could raise this with the Python folks directly, and ask for a |
Beta Was this translation helpful? Give feedback.
-
@jcrist: @samuelcolvin and I politely summon you! Because @msgpack accesses the |
Beta Was this translation helpful? Give feedback.
-
I've started a discussion on this in the Python forums here. |
Beta Was this translation helpful? Give feedback.
-
Tagging @pingsutw + @ddl-rliu + @wild-endeavor for Flytekit, as I believe you're doing some nontrivial runtime interaction with Python annotations as well. (I can see what looks like quite a complicated |
Beta Was this translation helpful? Give feedback.
-
Whoa, @leycec, this is fantastically written. :) What you wrote about, however, is much less great. 😭 It seems that all our lives are about to become a lot more painful. Thanks for the heads up regarding Plum's exposure. That's good to know and an action item that's moved high up my priority list.
To anyone reading this, I can confirm that |
Beta Was this translation helpful? Give feedback.
-
Here! Here! The dishonourable @leycec presiding. You may now be thinking: "This is a scam, right?", "Who are you and why are you rooting around in my inbox?", "You annoy me, peasant.", "@beartype is bad and you should feel bad!", "Oh, Gods. That guy.", and "I already dislike you on principle."
These are reasonable thoughts. Nonetheless! Please, monocled gentle-people. There are matters afoot. They affect all members of the runtime type-checking community. This means you. Of course, runtime type-checking is mad-cap, pell-mell, and hazardous to mental health and physical well-being at the best of times. There are only a few of us. We're not legion. Nobody even knows about us. Okay, everybody knows about Pydantic. But the rest of us are like
typing
ninjas. Nobody expects... oh forget it. We're all tired of that line. Sadly, you are about to become even more tired.Only three of us author general-purpose runtime type-checkers. Only a handful more author special-purpose runtime type-checkers. You have been called here tonight, because you are one of these authors.
Please, Gods. Say Something Interesting Already.
Python 3.13 is about to break forward compatibility with your type-checker. Python 3.13 – the next stable release of Python – is slated for release on October 1st of this year. Python 3.13 will unconditionally enable an old "feature" previously only enabled through arcane syntax so annoying that basically nobody used it, except PyTorch. PyTorch gotta walk its own way. That feature is baaaaaack. What's back? More on that in a hot minute.
The more important first question is: "Who is affected by the breakage?" After all, if it's not you, none of this matters. You can just go back to sleep and lapse into the sugar coma you came from. Sadly, it's you.
Unaffected: Most Packages, But Not Yours
Most Python packages are unaffected. Most means 99.999999999%. I just made that number up, but it's probably true. This is the good news. Python 3.13 preserves compatibility for most use cases and users.
Sadly, yours is not one of them. This is the bad news.
Most Python packages only annotate things with type hints. They don't actually consume, introspect, modify, or otherwise handle type hints as actual runtime objects. Thankfully, these use cases and users are fine. These packages should continue doing exactly what they are doing. Whatever they're doing is fine and will continue to be fine across the Python 3.13 threshold.
Consider @felixchenier at kineticstoolkit/kineticstoolkit#198, who wonders what exactly this means for end users. I hesitated in responding before, because I genuinely didn't know. I was woefully dumb. Also, this topic is nuanced and terrifying. But then I read and implemented support for the PEPs we shall introduce shortly. Now, I pretend to know. @felixchenier and other concerned citizens, your answer is:
This is a big nothing-burger for almost everybody. But what about the rest of us? ...heh.
Affected: Hardly Any Packages, But You Wrote One of Them
Affected Python packages include those either directly accessing the
__annotations__
dunder dictionary or directly calling the standardinspect.get_annotations()
ortyping.get_type_hints()
functions. Because I have summoned you, your package is one of these. Specifically:__annotations__
across 2 files intypeguard
and another 3 references totyping.get_type_hints()
across 1 file.pandera
– but you've committed a lot recently. So, you're it. Congrats. I see some 8 references to__annotations__
across 4 files inpandera
and another 9 references totyping.get_type_hints()
across 4 files.pandera
is really exposed to this issue.__annotations__
across 1 file injaxtyping
and 2 references totyping.get_type_hints()
across 1 file. Excellent showing!__annotations__
across 17 files in @pydantic alone and another 4 references to__annotations__
across 3 files in `pydantic-core. @pydantic is extremely exposed to this issue.__annotations__
across 1 file in Plum and another 2 references totyping.get_type_hints()
across 1 file. Excellent showing!You definitely did the best out of everybody, @patrick-kidger and @wesselb. Your pain is minimal. Still, there is pain.
I mostly didn't bother grepping for
inspect.get_annotations()
calls. Almost nobody accesses annotations indirectly that way. Why bother, right? Python 3.13 says: "Hi! You should've bothered. I'm about to break you."Uhh.... I Still Don't Get It. What Was This About Again?
Right. Right. Getting there. So, we're agreed that we're all about to suffer. Everyone is in this together, which certainly makes me feel fuzzy and/or warm. Communal suffering. Nothing quite like it.
Python 3.13 enables a new annotation-specific feature called "bare forward references." "What's that? Sounds horrid," you're thinking. A "bare forward reference" is an unquoted reference to a type that has yet to be declared. The most common forward reference is a self-reference to the class currently being declared. Examples include:
Both references to
MuhClass
above are bare forward references. In both contexts, theMuhClass
class has yet to be declared in its lexical scope. Of course, in the latter context, theMuhClass
class is technically available to@your_runtime_checker
– but that doesn't particularly matter, because@your_runtime_checker
has no say in the matter. "Wut!? The heck it don't!", you are now thinking. Bear with me a moment, please. When resolving annotations, lexical scope is the only thing that matters. Sadly, your decorator is irrelevant there.Allow me to now explicate. Python 3.13's support for bare forward references requires fundamentally new technology unavailable in prior Python versions. To support bare forward references, your runtime type-checker must be manually refactored by you to add this support. This support does not come for free. If you ignore this issue and do nothing, your runtime type-checker will fail to support bare forward references. "Who cares? So what? I'm a tough dude who wears shades at night," you are now thinking.
Sadly, everybody wants bare forward references. Everybody hates PEP 484-style quoted forward references, PEP 585-style
typing.ForwardRef()
-encapsulated forward references, and the PEP 673-styletyping.Self
singleton. Everybody just wants to reference classes in type hints regardless of whether those classes have been declared yet. This is the grimdark reality we now find ourselves swimming in.Let's up the grimdark. If you do nothing, your runtime type-checker will raise one unreadable exception for each type hint containing one or more bare forward references. The unreadability of the exception depends on whether the problematic type hints in question occur at global or local scope. Notably:
For each global type hint containing one or more bare forward references, your runtime type-checker will raise an unreadable
NameError
exception resembling:For each local type hint containing one or more bare forward references, your runtime type-checker will raise an unreadable
UnboundLocalError
exception resembling:In all three cases, note that the exception message fails to emit the name of the module, callable, or class annotated by that problematic type hint. Obviously, the traceback contains that context – but consider the intersection of logging, web and mobile apps, and your runtime type-checker. That context may be stripped, truncated, or otherwise missing by the time your devoted userbase finally heaves a collective sigh and submits an issue to your tracker.
In other words, you absolutely must do something. If you ignore these prophetic words I am intoning in a deep baritone, destruction and mayhem shall be thy constant companions. Woe! Woe unto thy codebase.
I'm Becoming Concerned By What I'm Hearing. Also, You're Creeping Me Out.
...right? Thankfully, all is not lost. The codebase you save from Python 3.13 might be your very own.
You have three choices. To quote somebody who was famous but is now dead:
Choice 1: Do It Yourself, Because Ain't Nobody Else Gonna
Choice 1 is the easy way. The easy way preserves independence. You don't need to add any additional optional or mandatory dependencies to your package. Instead, you "just" need to:
@callable_cached
decorator. As for as I know,@callable_cached
is the fastest possible general-purpose memoization decorator in pure Python. For efficiency, it prohibits keyword arguments. On the other hand, it even memoizes exceptions – which does cost a bit, but is basically mandatory for sane memoization in the general case.@functools.cache
decorator under Python ≥ 3.12 and@functools.lru_cache(maxsize=None)
decorator under older Python versions. Both are considerably slower than@callable_cached
and fail to memoize exceptions – both of which sorta defeat the point of memoization. But... hey. You do you. Both of these decorators are certainly fine for casual use in a first-draft implementation. It's like when your significant other casually says "...fine!" and then sighs loudly. 😮💨gimme_dose_hints()
. Yes. You do need to define a new private utility function in your package for reasons that will become clear shortly. No, it doesn't need to be calledgimme_dose_hints()
. It probably shouldn't be, in fact.__annotations__
dunder dictionary or direct call to the standardinspect.get_annotations()
ortyping.get_type_hints()
functions into a call to yourgimme_dose_hints()
function.The implementation vaguely resembles (and I am wildly waving my hands here):
"I hate Choice 1," you were almost about to think. But you stopped when you realized that Choices 2 and 3 were probably a lot worse. They are. So, what is going on here? Why is memoization required?
What is going on here is PEP 649. PEP 649 deprecates PEP 563 (i.e.,
from __future__ import annotations
) by globally and unconditionally enabling a completely different internal mechanism called "annotation scopes" that semantically achieves a similar purpose. For unknown reasons that likely reduce to "cause it was easy," the implementors of PEP 649 chose to implement the portion of PEP 649 handling bare forward references in pure Python.Even that wouldn't have been so bad – except that this pure-Python implementation is outrageously, incredibly, and excrutiatingly slow. Why? Because this pure-Python implementation is doing insane things you do not want to think about:
Your face should now be contorting into a rictus grin. Note that the "exotic runtime environment" mentioned above is a dynamically synthesized data structure with non-trivial space and time complexity that the
inspect.get_annotations(obj, format=FORWARDREF)
function internally creates and then discards each time you call it. This worst-case complexity happens each time you pass an object containing one or more bare forward references to yourgimme_dose_hints()
function.If I had to estimate the worst-case time complexity of your unmemoized
gimme_dose_hints()
function under Python ≥ 3.13, I'd ballpark it atO(∞!∞)
. That is, infinity factorial infinity. Kinda big number, huh? Don't be that guy. Memoize the bad PEP away.I wouldn't bother with an LRU cache, either. Just go unbounded. The amount of space consumed by type hints and the total number of type hints across an app is very small by compare to the remainder of the app – especially for those of us in data science and web apps.
Choice 2: How Could Things Get Any Worse
To introduce Choice 2, I now need to discuss the failings of Choice 1. Actually, there's only one failing – but you're not gonna like it. You're really not gonna like it. Let's back up to our initial example of bare forward references. Remember this? @leycec remembers this:
If you did something, that now works under Python ≥ 3.13. Congrats. I salute you before passing out.
But... "What about Python ≤ 3.12"? I can hear your quavering voice already. It sounds like my own. The answer, of course, is that the above code raises
SyntaxError
exceptions in Python ≤ 3.12. But users want that code to work reliably across all Python versions! So... "What do users do?"You already know the answer to this, don't you? Somewhere deep inside, you know. The truth is squirming. It's creeping unbidden across your brain like that intestinal worm you got after visiting the Dominican Republic last summer that just won't go away.
The answer is PEP 563. Ironically, although PEP 649 deprecates PEP 563, PEP 649 also makes PEP 563 much more popular by legitimizing what PEP 563 was trying but failed to do a decade ago. This means that everybody is about to add the following to every single module in their codebase:
Do you see what just happened there? Everybody just enabled PEP 563 via
from __future__ import annotations
. In other words, not only is PEP 563 not going away for the foreseeable future, but PEP 563 just got a huge testosterone-enhancing shot in the arm and is back for one more swing with your codebase. PEP 563 is now legitimate. It's here to stay. It's going to crop up everywhere.You are now thinking: "...fine. This is fine. I... I can deal with this. I can cope. I'm hugging a teddy bear right now. I'll just tell everybody not to do that. Hah! Dumb users lose again."
Yeah. That's not gonna happen. Well, that might happen. Let's be honest. But those dumb users might just decide that your package is even dumber. They might just decide to switch to a different runtime type-checker that actually does support PEP 563 rather than abandon bare forward references – which, as we all know by now, everybody wants. PEP 563 can no longer be ignored, because PEP 563 is effectively being globally enabled.
Can you do something about that? You can. But you're not going to like it. You're going to hate it. And then you're going to try to punch me repeatedly in my GitHub avatar.
Remember that
gimme_dose_hints()
function we defined above? What if we augmented that function to transparently support PEP 563 under Python ≤ 3.12? You should now be thinking: "Impossible, bro. Tried that. Couldn't do it. Nobody can. Can't be done. Gave up. Went home. Did something productive with our lives... like you should be doing right now."Behold! The impossible made possible:
That's right. @beartype fully supports PEP 563... and has for a year or two. That's pretty much all I did for a year, honestly. I want that year back. Alas, all I have is PEP 563 support.
You can now profit from my loss by "piggybacking" your own runtime type-checker onto @beartype. But you do not want to require @beartype, of course. You only want to parasitically feed off @beartype's salty tears.
That's where our public
beartype.peps.resolve_pep563()
API comes in. @wesselb's Plum has supported PEP 563 for just as long as @beartype itself by calling that function. It works. It's battle-hardened. It does everything everybody wants.You are now thinking: "I hate you. Bait-and-switch, much? I ain't calling nuffin'. I'm gonna copy-paste that right out of @beartype into {insert-my-better-package-here}." Yeah. That's not gonna work. I wish it would, honestly. Then you'd be more likely to do this. But PEP 563 support is extraordinarily non-trivial. I'm pretty sure it took in upwards of 100,000 lines of pure-Python to get that to work. Copy-pasting is probably infeasible. Just sayin'.
You are now thinking: "...I am going to live on a mountain, curl up into a little ball, and pretend that none of this ever happened."
Excellent decision! That's basically what I did. Just substitute "mountain" for "smelly wetlands in Canada." It's less picturesque, but more full of edible food. Assuming your definition of edible food includes insects and things that go <squelch-squelch>.
Choice 3: The Omega of Bad Choices
Actually... there is no Choice 3. Not yet, anyway. If Choice 3 did exist, it would look something like this:
gimme_dose_hints()
function, you'd just call @beartype'sbeartype.gimme_dose_hints()
. That function does not exist. But it might. Does anybody besides @wesselb want something like that? Since @wesselb be does, I'll be doing this anyway – but I might not necessarily publicly advertise it unless somebody else expresses interest.Some of You Are Doing Something PEP 649 Hates
Some of you go a step further than simply accessing (i.e., getting, reading) annotation dictionaries. You're also modifying (i.e., setting, writing) annotation dictionaries. Unfortunately, PEP 649 really hates that.
Technically, you can still attempt to modify annotation dictionaries – but not directly. Python ≥ 3.13 will silently ignore any attempts to modify the
__annotations__
dunder dictionary. Sure, you can do it. But you just made life non-deterministic for your userbase, because the__annotations__
dunder dictionary is now just a volatile cache that can be overwritten by any number of standard things – including the new__annotate__()
dunder method, theinspect.get_annotations()
function, and thetyping.get_type_hints()
function.In other words, you probably want to think twice. Do you really need to modify the
__annotations__
dunder dictionary? If not, you should stop doing that. If you really do need to modify that dictionary, your life just became even harder and more exhausting. Why? Because you now need to dynamically redefine the__annotate__()
dunder method on all passed objects. Your redefined__annotate__()
method must then perform the desired modifications, isolated to that method.Now consider what this means if someone else (e.g., some other runtime type-checker) has already redefined the
__annotate__()
before you got there. What then? What about function composition? Good luck. That's what.In short, it sucks. It really sucks. Consider finding another way – like, any other way – to do what you want. Because you really do not want to try modifying the
__annotations__
dunder dictionary. It might even be the case that you can no longer reliably and portably do so in a manner consistent with function composition. I'm not sure, honestly. I wouldn't try it. But... if you're still convinced you want to fling yourself body-and-soul down that murder hole, that's certainly a "choice."I Am So Tired. Please. Stop.
...yeah. We're all tired. I'm tired of Python repeatedly punching both itself and the runtime type-checking community in the face. But what can you do?
Sometimes, a man just gotta lie down.
Beta Was this translation helpful? Give feedback.
All reactions