You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Feature Request] beartype.claw import hooks transform runtime-hostile if TYPE_CHECKING: antipattern to runtime-friendly PEP 810 lazy imports = 🥹 #477
Hello! I decided to try beartype recently (read: today). I'm working on a large codebase with a hand-rolled runtime type checking system, but everything is also type-annotated, so why not use beartype and halve the work?
Unfortunately, I ran into an issue quite quickly (may just be user error!), because of forward references with deferred imports. Here's a quick reproduction:
Ho, ho... ho. Yes. A most pertinent question. Indeed, we desperately need to document how to forward reference in a runtime-friendly way. I've been... tied up with other desperate codebase matters, sadly. CI has been on fire for the past several weeks, for example. Just put that one out! Phew. 🔥 🧯
Thankfully, runtime-friendly references are super easy: just use PEP 484-compliant absolute stringified references (i.e., "."-delimited strings spelling out the absolute name of the package or module containing the user-defined class that is being referred to): e.g.,
# In "bears/core.py":fromtypingimportOptionalimportpolarsasplfrombeartypeimportbeartype# Note the absence of the "TYPE_CHECKING" antipattern here. Good!@beartypeclassBear:
def__init__(self, name: str, bear_type: Optional[str] =None) ->None:
frombears.ephemeralimportSpiritBearfrombears.persistentimportPolarsBearself.instance= (
PolarsBear(name, pl.DataFrame())
ifbear_type=="polars"elseSpiritBear(name)
)
# Absolute stringified references to the rescue. Thanks, PEP 484!defmanifest(self) ->"bears.persistent.PolarsBear": # <-- dot-delimited for the winfrombears.persistentimportPolarsBearreturnPolarsBear(self.instance.name, pl.DataFrame())
Guido standardized this with PEP 484 almost a decade ago. When you're in Guido's hands, you know you're in good hands.
Oddly, everybody uses the runtime-hostile TYPE_CHECKING forward reference antipattern instead. This is what the code you've so helpfully supplied above uses. That's the antipattern. It's an antipattern, because it breaks runtime type-checkers like @beartype, @typeguard, and Pydantic. TYPE_CHECKING evaluates to False at runtime. Runtime introspection has no valid means of inspecting imports isolated to TYPE_CHECKING blocks. As far as @beartype is concerned, those imports never happened... because they literally don't at runtime.
Indeed, a future version of @beartype – probably beartype 0.20.1...so the next next stable release – will explicitly detect the TYPE_CHECKING forward reference antipattern and emit fatal warnings (or even exceptions) for each usage of this antipattern.
Thanks so much for the detailed example, @deepyaman. I love that you even know about the Kermode spirit bears! We're so proud of them here in Canada. 🤗 🐻❄️ 🤗
changed the title [-][Question] How can I make forward references with deferred imports work?[/-][+][Feature Request] Detect and warn about the `TYPE_CHECKING` forward reference antipattern in `beartype.claw` import hooks :vomiting_face: [/+]on Jan 8, 2025
You're most welcome! Let me know if you have any other pressing Python QA needs. @beartype aims to please everyone. Together, we'll crush all the bugs. 😄
Is that actually allowed? It probably should be since it's unambiguous and useful, but PEP 484 and the current version in the spec don't mention it as a possibility. Instead stringified annotations must be something that can eval() in the current scope. You'd need to make sure you've imported bears in this scope to let it work. Even then, I think mypy might be smart enough to notice that the submodule is not necessarily imported and complain, and I know Pyright deliberately doesn't allow implicitly referencing submodules like that because it might not have been loaded.
Another solution is to keep the TYPE_CHECKING import, but then define a __getattr__() module hook to do the import, for runtime checkers to use.
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?
It's PEP 563, actually. Consider this example from PEP 563:
from __future__ importannotationsimporttypingiftyping.TYPE_CHECKING:
importexpensive_moddefa_func(arg: expensive_mod.SomeClass) ->None:
a_var: expensive_mod.SomeClass=arg
...
typing.TYPE_CHECKING unconditionally evaluates to False at runtime. Likewise, from __future__ import annotations activates PEP 563 which then stringifies the type hint expensive_mod.SomeClass to "expensive_mod.SomeClass". Altogether, this means that CPython reduces the above to the following at runtime:
This then implies that 'expensive_mod.SomeClass' is a valid forward reference to the type SomeClass defined by the top-level package or module expensive_mod.
From @beartype's perspective, typing.TYPE_CHECKING and from __future__ import annotationsbasically don't exist. All that exists are stringified forward references. Ergo, @beartype transparently supports stringified forward references – because it has to. @beartype has no choice or say in the matter.
From @beartype's perspective, there's no need to explicitly import anything. Whether users import things in if typing.TYPE_CHECKING: blocks is irrelevant to @beartype, because @beartype can't see those things. They're hidden from view. They'd might as well not exist in the first place.
Given all of the above, why would there be any need to explicitly import anything in an if typing.TYPE_CHECKING: block, anyway? Hidden imports are just useless boilerplate. Absolute stringified references already convey all the metadata that @beartype and other runtime type-checkers need to unambiguously resolve forward references to external types.
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.
Another solution is to keep the TYPE_CHECKING import, but then define a __getattr__() module hook to do the import, for runtime checkers to use.
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 reference antipattern having to also now define one __getattr__() function per module just to satisfy... what? @beartype? Nobody cares about @beartype. Most third-party packages that don't actively use @beartype are tired of their userbase complaining about @beartype.
The Ultimate Solution Emerges, Ultimately
Ultimately, the solution is for @beartype to automate all of this away on behalf of users. When a user activates a beartype.claw import hook, an abstract syntax tree (AST) transformation in @beartype will:
Detect the typing.TYPE_CHECKING reference antipattern.
Issue one non-fatal warning for each if typing.TYPE_CHECKING: block containing one or more hidden imports. A warning is necessary, because:
@beartype can't automatically "repair" every instance of this antipattern. Star imports isolated to if typing.TYPE_CHECKING: blocks are unrepairable, for example. (In fact, @beartype may need to raise a fatal exception in this edge case.)
Many users still use @beartype as an explicit @beartype decorator rather than beartype.claw import hooks. This includes PyTorch. This antipattern will continue to break those codebases.
Many users use alternative runtime type-checkers or introspection APIs. Clearly, this antipattern will continue to break those codebases too.
Automatically "repair" the antipattern by injecting one global-scoped @beartype forward reference per hidden import into the AST of the current module.
Thus:
# @beartype will transform this antipattern that doesn't work at runtime...iftyping.TYPE_CHECKING:
fromA.BimportC, D# ...into this comparable logic that actually *DOES* work at runtime.frombeartype._check.forward.reference.fwdrefimportBeartypeForwardReferenceC=BeartypeForwardReference('A.B', 'C')
D=BeartypeForwardReference('A.B', 'D')
In the above example, C and D are now objects that transparently proxy isinstance() and issubclass() checks against those external types defined elsewhere.
As a friendly note to both myself and Claude, hey there, buddy. got tokens? the above transformation isn't quite enough:
# @beartype will transform this antipattern that doesn't work at runtime...iftyping.TYPE_CHECKING:
fromA.BimportC, D# ...into this comparable logic that actually *DOES* work at runtime.frombeartype._check.forward.reference.fwdrefimportBeartypeForwardReferenceC=BeartypeForwardReference('A.B', 'C')
D=BeartypeForwardReference('A.B', 'D')
# Let's try instantiating these objects! It's fun!see_c_run=C()
see_d_die=D()
Why? Because the BeartypeForwardReference() class currently prohibits instantiation. We thought we were doing a good thing. The above logic currently raises unreadable exceptions resembling:
beartype.roar.BeartypeDecorHintForwardRefException: <forwardref
BeartypeForwardRefABC(__name_beartype__=None, __scope_name_beartype__=None)> subclass
<forwardref C(__name_beartype__='C', __scope_name_beartype__='A.B')> not
instantiatable.
In other words, @beartype forward references are not yet suitable proxies for external types. They could be. They should be. But they fail to transparently act like external types in all possible contexts. Maybe that's fine, though? No idea. Surely, anybody importing types in a if TYPE_CHECKING: blocks knows that those types are mostly unusable? No idea.
I suppose we could just ignore the fact that @beartype forward references are imperfect proxies by just gating all of this AST transformation logic behind yet another:
New BeartypeConf(is_proxy_type_checking_imports: bool = True) configuration setting, defaulting to True because almost everybody wants this to just work already.
Sounds good! Let's do something before the Universe implodes! 🪐 💥
New BeartypeConf(is_proxy_type_checking_imports: bool = True) configuration setting, defaulting to True because almost everybody wants this to just work already.
I just want to point out that if this defaults to True, there is a possibility of things mysteriously failing when you stop using beartype. This feature is like useful enough that this may be overall good, but it still feels weird.
...there is a possibility of things mysteriously failing when you stop using beartype.
Indeed. It's even worse than that, though. @beartypeitself stops everyone from using @beartype by silently reducing to a noop under Python optimization (e.g., ${PYTHONOPTIMIZE}, -O). When Python is optimized, @beartype effectively no longer exists. That includes beartype.claw import hooks and thus this feature.
You're absolutely right, @Glinte. Why are you like this!? It's like you're a living QA oracle. The Greeks prophesied your coming. Caution is thus warranted. To preserve compatibility across @beartype logic paths and prevent vendor lock-in to @beartype, beartype.claw import hooks (probably) only want to conditionally perform this transformation when type hints are lazily evaluated. Fortunately, that's increasingly the default state of typing. Hints are lazily evaluated under one of two scenarios:
Under any Python version, when the user explicitly enables PEP 563 for the current submodule via from __future__ import annotations. This is trivial to detect from the AST. In fact, we already do just that! Laziness prevails. \o/
Under Python ≥ 3.14.Always. This is even more trivial to detect, obviously.
We can thus trivially decide at AST transformation time whether we're going to conditionally commit to performing this transform or not. If one of the above two constraints is met, we respect the user-configured conf.is_proxy_type_checking_imports option and do what the user tells us; else, we ignore the user's preference entirely.
Seems reasonable to me, but... video games also seem reasonable to me. They are not.
What's your intuitive read on this sort of QA skullduggery, Master @Glinte? Does lazy evaluation of type hints suffice to render this fully safe... or do we need to dig even deeper? Are edge cases lurking like slathering spiders in the gloom? You can almost hear their mandibles chewing. Is anyone safe anymore? Is nothing sacred? I implore you to answer the questions of the Universe.
Edit: Actually if you disallow instantiation as in the current behaviour it should be fine. This does mean probably some third party library that tries to instantiate stuff inside type hints will fail, and hints like Annotated[int, GreaterThan(3)] will fail if GreaterThan is a BeartypeForwardReference, but this seems reasonable.
Original comment below
Probably not going to suffice for sufficiently bad misuse of if TYPE_CHECKING. I can imagine some newbie doing
ifTYPE_CHECKNG:
fromaimportMyClassx=MyClass()
This is trivially a NameError in runtime and will get caught by everyone, until you use beartype and it becomes functional.
I couldn't be bothered to read the full thread but I think I ran into a similar issue. Tried beartype an ran into some weird
BeartypeDecorHintForwardRefException: Forward reference 'Mapping[annotationlib_name_1, annotationlib_name_2]' not valid Python attribute name
errors that didn't make much sense to me. After fiddling around a bit I found that I have to remove all
ifTYPE_CHECKING:
import ...
from my codebase to make them go away.
If this is intended we should at least have an error message hinting at this. Or at the very least an FAQ entry that's <enter your search engine here>-able.
Totally! In all likelihood, all those super-annoying forward references shenanigans you've hit have already been resolved in the live @beartype codebase. But...
Maybe not, actually. if TYPE_CHECKING: is still runtime-hostile and @beartype still has yet to explicitly do anything about that. What a shame, huh? I totally neglected this critical issue that's been annoying everyone for years.
You're absolutely right, @black-snow. Great avatar name, by the way. This oddness should be (A) publicly documented somewhere and (B) just resolved already. I'm so sorry you've stumbled into all of those roadblocks with @beartype. Certainly, we are imperfect. Someday, this software will do everything everyone wants. Clearly, this is not that day. 😮💨
What follows is my message in a bottle to my older self. No one else may care. Older self, you are finally trying to resolve this issue, huh? Good on ya. I have good news and no bad news, really. The good news is this:
PEP 484-style if TYPE_CHECKING: lazily imported type hints, on the other hand, are not trivially supportable by anybody at runtime. It's unclear how or when @beartype will be capable of that. The conclusion for end users is clear:
Packages willing to require Python ≥ 3.15 as a mandatory dependency should (A) do that and (B) refactor all PEP 484-style if TYPE_CHECKING: lazily imported type hints into equivalent PEP 810-style lazily imported type hints:
# Instead of this...fromtypingimportTYPE_CHECKINGifTYPE_CHECKING:
fromtypingimportLiteral, Self, ThisIsHellOnRuntimeEarth# ...everybody who loves Python 3.15 should instead do this.lazyfromtypingimportLiteral, Self, ThisIsHellOnRuntimeEarth
Packages refusing to require Python ≥ 3.15 as a mandatory dependency should instead refactor all PEP 484-style if TYPE_CHECKING: lazily imported type hints into equivalent PEP 484-style stringified absolute forward references.
# Instead of this...fromtypingimportTYPE_CHECKINGifTYPE_CHECKING:
fromtypingimportLiteral, Self, ThisIsHellOnRuntimeEarth# ...everybody who hates Python 3.15 should instead do this.Literal='typing.Literal'Self='typing.Self'ThisIsHellOnRuntimeEarth='typing.ThisIsHellOnRuntimeEarth'
The Python ≥ 3.15 approach is blatantly superior (both from a syntactic and semantic perspective), but also requires Python ≥ 3.15. Boooooooooo. 😯
I'm fairly certain that there exists a clean one-to-one mapping between PEP 484-style if TYPE_CHECKING: and PEP 810-style lazily imported type hints. That is, given one or more imports isolated to an if TYPE_CHECKING: branch, @beartype can (and should) safely transform those into the equivalent lazy imports via a simple AST transform.
The same cannot be said of PEP 484-style stringified absolute forward references. You can't subscript them or | them, for example. Ergo, beartype.claw import hooks should now do one of two things when it detects imports isolated to an if TYPE_CHECKING: branch at AST transformation time:
If the active Python interpreter targets Python ≥ 3.15, just transparently convert those imports into the equivalent PEP 810-style lazy imports (like above). There's no cause for issuing warnings or raising exceptions here. If @beartype can simplify everyone's life, it should. 💪 🐻
Else, @beartype should issue a non-fatal warning whose message is verbatim the text above starting "The conclusion for end users is clear: ...".
And that's how the if TYPE_CHECKING: was won. By doing nothing for a decade, waiting for CPython devs to fix the mess, and then pointing at a PEP standard. Feels good, fam.
what is even happening on this issue tracker anymore
Activity
leycec commentedon Jan 8, 2025
Ho, ho... ho. Yes. A most pertinent question. Indeed, we desperately need to document how to forward reference in a runtime-friendly way. I've been... tied up with other desperate codebase matters, sadly. CI has been on fire for the past several weeks, for example. Just put that one out! Phew. 🔥 🧯
Thankfully, runtime-friendly references are super easy: just use PEP 484-compliant absolute stringified references (i.e.,
"."-delimited strings spelling out the absolute name of the package or module containing the user-defined class that is being referred to): e.g.,Guido standardized this with PEP 484 almost a decade ago. When you're in Guido's hands, you know you're in good hands.
Oddly, everybody uses the runtime-hostile
TYPE_CHECKINGforward reference antipattern instead. This is what the code you've so helpfully supplied above uses. That's the antipattern. It's an antipattern, because it breaks runtime type-checkers like @beartype,@typeguard, and Pydantic.TYPE_CHECKINGevaluates toFalseat runtime. Runtime introspection has no valid means of inspecting imports isolated toTYPE_CHECKINGblocks. As far as @beartype is concerned, those imports never happened... because they literally don't at runtime.Indeed, a future version of @beartype – probably
beartype 0.20.1...so the next next stable release – will explicitly detect theTYPE_CHECKINGforward reference antipattern and emit fatal warnings (or even exceptions) for each usage of this antipattern.Thanks so much for the detailed example, @deepyaman. I love that you even know about the Kermode spirit bears! We're so proud of them here in Canada. 🤗 🐻❄️ 🤗
[-][Question] How can I make forward references with deferred imports work?[/-][+][Feature Request] Detect and warn about the `TYPE_CHECKING` forward reference antipattern in `beartype.claw` import hooks :vomiting_face: [/+]deepyaman commentedon Jan 9, 2025
Thank you for the quick response! Glad to see it's a straightforward solution. I'll give it a shot in my actual code base and report back soon. 🤞
leycec commentedon Jan 9, 2025
You're most welcome! Let me know if you have any other pressing Python QA needs. @beartype aims to please everyone. Together, we'll crush all the bugs. 😄
TeamSpen210 commentedon Jan 9, 2025
Is that actually allowed? It probably should be since it's unambiguous and useful, but PEP 484 and the current version in the spec don't mention it as a possibility. Instead stringified annotations must be something that can
eval()in the current scope. You'd need to make sure you've importedbearsin this scope to let it work. Even then, I think mypy might be smart enough to notice that the submodule is not necessarily imported and complain, and I know Pyright deliberately doesn't allow implicitly referencing submodules like that because it might not have been loaded.Another solution is to keep the
TYPE_CHECKINGimport, but then define a__getattr__()module hook to do the import, for runtime checkers to use.leycec commentedon Jan 9, 2025
...heh. Standardization intensifies.
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! 😎
It's PEP 563, actually. Consider this example from PEP 563:
typing.TYPE_CHECKINGunconditionally evaluates toFalseat runtime. Likewise,from __future__ import annotationsactivates PEP 563 which then stringifies the type hintexpensive_mod.SomeClassto"expensive_mod.SomeClass". Altogether, this means that CPython reduces the above to the following at runtime:This then implies that
'expensive_mod.SomeClass'is a valid forward reference to the typeSomeClassdefined by the top-level package or moduleexpensive_mod.From @beartype's perspective,
typing.TYPE_CHECKINGandfrom __future__ import annotationsbasically don't exist. All that exists are stringified forward references. Ergo, @beartype transparently supports stringified forward references – because it has to. @beartype has no choice or say in the matter.From @beartype's perspective, there's no need to explicitly import anything. Whether users import things in
if typing.TYPE_CHECKING:blocks is irrelevant to @beartype, because @beartype can't see those things. They're hidden from view. They'd might as well not exist in the first place.Given all of the above, why would there be any need to explicitly import anything in an
if typing.TYPE_CHECKING:block, anyway? Hidden imports are just useless boilerplate. Absolute stringified references already convey all the metadata that @beartype and other runtime type-checkers need to unambiguously resolve forward references to external types.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 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.
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_CHECKINGreference antipattern having to also now define one__getattr__()function per module just to satisfy... what? @beartype? Nobody cares about @beartype. Most third-party packages that don't actively use @beartype are tired of their userbase complaining about @beartype.The Ultimate Solution Emerges, Ultimately
Ultimately, the solution is for @beartype to automate all of this away on behalf of users. When a user activates a
beartype.clawimport hook, an abstract syntax tree (AST) transformation in @beartype will:typing.TYPE_CHECKINGreference antipattern.if typing.TYPE_CHECKING:block containing one or more hidden imports. A warning is necessary, because:if typing.TYPE_CHECKING:blocks are unrepairable, for example. (In fact, @beartype may need to raise a fatal exception in this edge case.)@beartypedecorator rather thanbeartype.clawimport hooks. This includes PyTorch. This antipattern will continue to break those codebases.Thus:
In the above example,
CandDare now objects that transparently proxyisinstance()andissubclass()checks against those external types defined elsewhere.Fun stuff. I vomit. 🤮
Beartype 0.20.0 RC 0: Release the Bears
Beartype 0.20.0: The Balding Bears Are Back in QA Town
[-][Feature Request] Detect and warn about the `TYPE_CHECKING` forward reference antipattern in `beartype.claw` import hooks :vomiting_face: [/-][+][Feature Request] `beartype.claw` import hooks transform runtime-hostile `TYPE_CHECKING` forward reference antipattern to runtime-friendly @beartype forward references = 🥹 [/+]leycec commentedon Nov 15, 2025
As a friendly note to both myself and Claude, hey there, buddy. got tokens? the above transformation isn't quite enough:
Why? Because the
BeartypeForwardReference()class currently prohibits instantiation. We thought we were doing a good thing. The above logic currently raises unreadable exceptions resembling:In other words, @beartype forward references are not yet suitable proxies for external types. They could be. They should be. But they fail to transparently act like external types in all possible contexts. Maybe that's fine, though? No idea. Surely, anybody importing types in a
if TYPE_CHECKING:blocks knows that those types are mostly unusable? No idea.I suppose we could just ignore the fact that @beartype forward references are imperfect proxies by just gating all of this AST transformation logic behind yet another:
BeartypeConf(is_proxy_type_checking_imports: bool = True)configuration setting, defaulting toTruebecause almost everybody wants this to just work already.Sounds good! Let's do something before the Universe implodes! 🪐 💥
Glinte commentedon Dec 9, 2025
I just want to point out that if this defaults to True, there is a possibility of things mysteriously failing when you stop using beartype. This feature is like useful enough that this may be overall good, but it still feels weird.
leycec commentedon Dec 10, 2025
Indeed. It's even worse than that, though. @beartype itself stops everyone from using @beartype by silently reducing to a noop under Python optimization (e.g.,
${PYTHONOPTIMIZE},-O). When Python is optimized, @beartype effectively no longer exists. That includesbeartype.clawimport hooks and thus this feature.You're absolutely right, @Glinte. Why are you like this!? It's like you're a living QA oracle. The Greeks prophesied your coming. Caution is thus warranted. To preserve compatibility across @beartype logic paths and prevent vendor lock-in to @beartype,
beartype.clawimport hooks (probably) only want to conditionally perform this transformation when type hints are lazily evaluated. Fortunately, that's increasingly the default state oftyping. Hints are lazily evaluated under one of two scenarios:from __future__ import annotations. This is trivial to detect from the AST. In fact, we already do just that! Laziness prevails.\o/We can thus trivially decide at AST transformation time whether we're going to conditionally commit to performing this transform or not. If one of the above two constraints is met, we respect the user-configured
conf.is_proxy_type_checking_importsoption and do what the user tells us; else, we ignore the user's preference entirely.Seems reasonable to me, but... video games also seem reasonable to me. They are not.
What's your intuitive read on this sort of QA skullduggery, Master @Glinte? Does lazy evaluation of type hints suffice to render this fully safe... or do we need to dig even deeper? Are edge cases lurking like slathering spiders in the gloom? You can almost hear their mandibles chewing. Is anyone safe anymore? Is nothing sacred? I implore you to answer the questions of the Universe.
Glinte commentedon Dec 10, 2025
Edit: Actually if you disallow instantiation as in the current behaviour it should be fine. This does mean probably some third party library that tries to instantiate stuff inside type hints will fail, and hints like
Annotated[int, GreaterThan(3)]will fail if GreaterThan is a BeartypeForwardReference, but this seems reasonable.Original comment below
Probably not going to suffice for sufficiently bad misuse of if TYPE_CHECKING. I can imagine some newbie doing
This is trivially a NameError in runtime and will get caught by everyone, until you use beartype and it becomes functional.
black-snow commentedon Aug 6, 2026
I couldn't be bothered to read the full thread but I think I ran into a similar issue. Tried beartype an ran into some weird
errors that didn't make much sense to me. After fiddling around a bit I found that I have to remove all
from my codebase to make them go away.
If this is intended we should at least have an error message hinting at this. Or at the very least an FAQ entry that's <enter your search engine here>-able.
leycec commentedon Aug 6, 2026
Totally! In all likelihood, all those super-annoying forward references shenanigans you've hit have already been resolved in the live @beartype codebase. But...
Maybe not, actually.
if TYPE_CHECKING:is still runtime-hostile and @beartype still has yet to explicitly do anything about that. What a shame, huh? I totally neglected this critical issue that's been annoying everyone for years.You're absolutely right, @black-snow. Great avatar name, by the way. This oddness should be (A) publicly documented somewhere and (B) just resolved already. I'm so sorry you've stumbled into all of those roadblocks with @beartype. Certainly, we are imperfect. Someday, this software will do everything everyone wants. Clearly, this is not that day. 😮💨
leycec commentedon Aug 7, 2026
What follows is my message in a bottle to my older self. No one else may care. Older self, you are finally trying to resolve this issue, huh? Good on ya. I have good news and no bad news, really. The good news is this:
<-- shockingly happy emoji face#578 for tiresome details that even my older self no longer wants to read.PEP 484-style
if TYPE_CHECKING:lazily imported type hints, on the other hand, are not trivially supportable by anybody at runtime. It's unclear how or when @beartype will be capable of that. The conclusion for end users is clear:Packages willing to require Python ≥ 3.15 as a mandatory dependency should (A) do that and (B) refactor all PEP 484-style
if TYPE_CHECKING:lazily imported type hints into equivalent PEP 810-style lazily imported type hints:Packages refusing to require Python ≥ 3.15 as a mandatory dependency should instead refactor all PEP 484-style
if TYPE_CHECKING:lazily imported type hints into equivalent PEP 484-style stringified absolute forward references.The Python ≥ 3.15 approach is blatantly superior (both from a syntactic and semantic perspective), but also requires Python ≥ 3.15. Boooooooooo. 😯
The only remaining question then becomes...
So. Uh. What Should @beartype Do?
I'm fairly certain that there exists a clean one-to-one mapping between PEP 484-style
if TYPE_CHECKING:and PEP 810-style lazily imported type hints. That is, given one or more imports isolated to anif TYPE_CHECKING:branch, @beartype can (and should) safely transform those into the equivalentlazyimports via a simple AST transform.The same cannot be said of PEP 484-style stringified absolute forward references. You can't subscript them or
|them, for example. Ergo,beartype.clawimport hooks should now do one of two things when it detects imports isolated to anif TYPE_CHECKING:branch at AST transformation time:lazyimports (like above). There's no cause for issuing warnings or raising exceptions here. If @beartype can simplify everyone's life, it should. 💪 🐻And that's how the
if TYPE_CHECKING:was won. By doing nothing for a decade, waiting for CPython devs to fix the mess, and then pointing at a PEP standard. Feels good, fam.what is even happening on this issue tracker anymore
[-][Feature Request] `beartype.claw` import hooks transform runtime-hostile `TYPE_CHECKING` forward reference antipattern to runtime-friendly @beartype forward references = 🥹 [/-][+][Feature Request] `beartype.claw` import hooks transform runtime-hostile `if TYPE_CHECKING:` antipattern to runtime-friendly PEP 810 `lazy` imports = 🥹 [/+]Test claw `if TYPE_CHECKING:` and lazy forward scopes for full covera…