no, it wouldnt solve using if TYPE_CHECKING to un-cycle imports, but that’s a separate issue IMO.
with that, you’d still need this constant, so that shows this can’t be a full replacement for current use, and is still an orthogonal improvement.
no, it wouldnt solve using if TYPE_CHECKING to un-cycle imports, but that’s a separate issue IMO.
with that, you’d still need this constant, so that shows this can’t be a full replacement for current use, and is still an orthogonal improvement.
Is it a good idea to make TYPE_CHECKING more prominent? I’m not sure it’s actually a good idea for libraries to be using it, especially for avoiding imports. If you as a user are doing runtime typing and a library you use opts to do this, it’ll mean it’s impossible to evaluate the annotations. It’s even worse if it’s made an actual constant for code removal reasons, since then the data might not be present anywhere at all except by statically analysing source code. The constant is lying to checkers, meaning they lose the ability to verify code is fully accurate. By putting TYPE_CHECKING in builtins, we’re implying that this is the preferred approach. If you absolutely need it for performance, speccing a way to do it makes sense, but it probably shouldn’t be the easy solution.
When considering runtime type information, maybe we should rephrase the use of TYPE_CHECKING (or __type_checking__ ) from „static type checkers assume this is True“ to „code behind this guard is only relevant for type checking“. This could become a runtime flag like __debug__ in the future.
Like __debug__ runtime type information will have a performance impact, so it might be reasonable to make this configurable. We accept that conflicting priorities exist and users can choose whether performance or more accurate runtime type information is desired. Instead of „lying with this variable“ (or worse with if False hacks) we‘re explicit and honest about the currently chosen priority. Runtime type checkers then at least know whether they have full information or not and could adapt their behavior.
That said, it does not have to be decided in this PEP whether __type_checking__ may become configurable in the future, but leaving this as an option prevents rendering us in a corner with respect to runtime type checking.
Agreed, and I touched on the impact on maintainers above and it wasn’t really addressed:
I appreciate that, but I think the PEP needs to take a clearer stance on how the new constant’s intended to be used and what the broader implications for the open source community would be, because this has the potential to cause quite a bit of churn, depending on how it ends up being received by the userbase.
It’d be good to know what the endgame here is. If we’re not gonna push this on libraries, workarounds for lazily importing (a locally re-exported) typing already exist, which also do not break runtime typing.
It is not intention of this PEP. This PEP just standardize these snippets:
from typing import TYPE_CHECKING; ... if TYPE_CHECKING:import typing; ... if typing.TYPE_CHECKING:import typing as t; ... if t.TYPE_CHECKING:TYPE_CHECKING = False; ... if TYPE_CHECKING:if False: # TYPE_CHECKINGWhen TYPE_CHECKING is variable, code under if TYPE_CHECKING: are unmarshalled (allocated) but never evaluated. You can not use it anyway.
Only exception is Sphinx use case. It try to assign typing.TYPE_CHECKING=True before importing to collect more typing data.
But it should be fixed by some lazy execution/import mechanizm. It is out of scope of this PEP.
It is not entirely clear what is meant by “runtime typing” or what exactly constitutes it being “broken”. If the expectation is that it should always be possible to evaluate all type annotations at runtime then that can never fully work and so in some sense runtime typing will always be broken.
Here is a simple example:
if TYPE_CHECKING:
from optional_dependency import NotInstalledType
class A:
def func(...) -> NotInstalledType
import optional_dependency
return optional_dependency.func(...)
The type annotation here refers to a type that may not exist at runtime if the package that contains it is not installed. No alternative spelling of TYPE_CHECKING, type import etc will make it possible to evaluate that annotation reliably at runtime.
Does the class A “break runtime typing” if any one of its methods has an annotation that cannot be evaluated? If so then runtime typing is a somewhat misguided concept and there needs to be some understanding of what its limitations are rather than an expectation that it could ever be made to work in general.
I think most of the time what people refer to as runtime typing does not really require that every single annotation be evaluated at runtime. An important exception though is the sphinx autodoc case which really does need to get every single annotation.
The only way this can be made to work reliably in general for sphinx is to get the annotations statically rather than trying to extract them at runtime. Consider e.g. this case where the docs should say that a function returns a special type MPZ but that type never actually exists at runtime. Basically sphinx needs to get the annotations the same way that a static type checker would. Maybe sphinx could literally use some existing static type checker to do this rather than setting TYPE_CHECKING=True at runtime which doesn’t generally work.
The minimum expectation is that packages should not lie about their importable symbols. We’ve already seen libraries which define entire import-only types in pyis, which are unusable at runtime. These cause disruption; the programmer must have knowledge of whether a symbol is a runtime symbol or a typing symbol, and the only way they can reliably know that - save for running the code that’s affected - is by exploring the source code of their dependencies. TYPE_CHECKING has long been thought of as a kludge because it allows people to lie in this same manner about their code. Introducing a built-in TYPE_CHECKING constant facilitates this pattern - the likelihood of typing symbols being defined in a typing-only context will increase if importing typing itself can be avoided.
The community’s very keen on not having annotations or typing-only imports being evaluated eagerly - the former is why we got stringified annotations in the first place. They’re less keen on having unevaluable annotations, which impose both a higher cognitive burden and a higher maintainability burden on their users. Increasingly more imports might have to be guarded and fewer types might be usable with runtime type checkers (which do exist), and other validators and parsers.
This isn’t to say that the PEP should be rejected for this reason, but the repercussions it might have should be acknowledged and it should explain why alternatives (say, pyis and lazy imports) were dismissed.
Let’s explore the two alternatives I suggested with aiohttp, which is a heavy package to import.
You can create an empty _typing_imports.py module, and a corresponding _typing_imports.pyi module, containing just:
import aiohttp as aiohttp
Elsewhere in your code, you can import your typing-only module:
from . import _typing_imports as _t # Let's shorten it too
async def fetch_some_data(value: _t.aiohttp.ClientSession) -> None: ...
This solution is slightly more involved, but it also has some benefits. In addition to avoiding the typing import, thus satisfying the motivation of this PEP:
aiohttp itself wherever it might be imported).aiohttp imports throughout your codebase unnecessary.aiohttp annotations so that they can be used at runtime, e.g. by defining a module-level __getattr__ in your _typing_imports.py:def __getattr__(name):
if name == 'aiohttp':
import aiohttp
return aiohttp
raise AttributeError(name)
Not if aiohttp is an optional dependency and is not installed. If it isn’t there then the annotation can never be evaluated. Anything that depends on being able to evaluate arbitrary annotations is unavoidably broken in many situations. Avoiding this breakage means defining achievable limits on what annotation consumers can do rather than expecting that annotations can always be evaluated.
Maybe my example before was too abstract but let me make it more concrete with the example of sympy’s Matrix class and the special __array__ method used by numpy. This makes it possible to convert a Matrix into an ndarray with a = np.array(M):
if TYPE_CHECKING:
from numpy import ndarray
class Matrix:
def __array__(self) -> ndarray:
import numpy as np
return np.array(self.values)
There is no guarantee that numpy is available at runtime so there is no guarantee that the annotation can be evaluated and none of the different things suggested here can change that. I would rather have the evaluation fail always than have it fail conditionally depending on whether or not numpy is installed. No one should depend on being able to evaluate that annotation.
They do exist but unless used in some limited way with very simple concrete types the general idea of runtime type checking is misguided. It does not take deep thought to see that runtime type checking with arbitrary static types can never actually work: what is the type of an empty list [] and can you append 1 to it?
An example less likely to run into people saying this is a misuse of optional dependencies might look something like this:
def fast_sum[T](array_like: tuple[T] | list[T] | np.ndarray[T]) -> T:
""" Use the fastest available summation for a supported array-like"""
...
A function like this accepts numpy arrays, but doesn’t require numpy at all, only uses it if the consumer of the function passes it. This is an example that should look like what the array API provides, intentionally, but from the input side, not the output, specifically so that people can see there are cases where the function should clearly exist, rather than be conditionally defined based on the import succeeding or not.
Annotations are largely seen as for static analysis, and runtime introspection is not guaranteed to work in any part of the typing specification, even the new annotationslib in 3.14 notes that various functions can raise.
Annotations are largely seen as for static analysis, and runtime introspection is not guaranteed to work in any part of the typing specification, even the new annotationslib in 3.14 notes that various functions can raise.
Runtime introspection is used by some of the most popular libraries around. Clearly we consider it to be valuable to have introduced PEP 649. I’m not sure why we’re debating this point now; it’s a distraction. If this is the slant we’re taking, what was even the point of insisting through all these years that static types should conform to Python’s syntax? PEPs were rejected when they attempted to introduce typing-only syntax.
I’ll try to reiterate. Library A defines some type which depends on typing. It can be a TypedDict, whatever, it doesn’t matter. Till now, it couldn’t avoid the typing import, so its author wouldn’t have considered guarding its declaration. If this is accepted, they’d now be enticed to do this:
if __type_checking__:
import typing
class Foo(TypedDict): pass
This type is no longer importable by consumers of the library. They, too, will have to guard their imports, but there’s nothing in the syntax of the language to tell them that. Type checkers don’t care; they know whatever’s under __type_checking__ to exist in their universe. This will cause unnecessary strife, it’s gonna cause churn when users seek out library authors in an effort to shave a few μs off Python’s start-up time and it will change how Python’s written in the long term. And yes, runtime introspection will suffer.
I certainly don’t. I think the string annotations we had prior were vastly superior because they were simpler and clear that it’s on the person introspecting to resolve them. 649 does nothing to change the fact that they may not be resolvable; it just changes the mechanism for how they are deferred.
This is resolvable currently by not publicly exporting that type. Users importing it anyway, even though it only exists for type information would then be clear they are importing a not-exported symbol.
While I’m sympathetic to those wanting all annotations to resolve at runtime, and I even do my part here, there are cases where it’s truly not possible. The obvious cases with functions that accept types that from libraries without relying on those libraries above is enough, but even without cases like that, the way to make types deferred properly without harming runtime introspection is also incredibly obtuse without a deep understanding of the competing needs.
I don’t think there’s ever a world where annotations always resolve at runtime as they show up in static analysis. There are libraries that use if TYPE_CHECKING to resolve circular imports that are only circular because of annotations, there’s no runtime interdependence on import.
There are also types that only exist in stubs for native modules, such as protocols that describe what the native code accepts. Those won’t go away any time soon, but are also a divergence between runtime type checking and statically available information.
Cases like these show that this pep is important because of how needed the ability not to actually import just for typing is, and that there are cases that no amount of future improvements to import speed will change.
For most intents and purposes, runtime annotations cannot be assumed to exist. Libraries that introspect and use annotations for runtime effect are the special case, not the norm, and they can document for their users that they require the annotations to be resolvable.
Certainly if a library isn’t installed those symbols can’t be imported, but the proposal in this topic is to allow avoiding importing typing itself. That is available, accessing annotations should resolve those. Actually even static checkers are going to have issues if they’re checking code with non-installed libraries.
Past steering councils have been clear about the value of runtime uses of annotations. See e.g. Type annotations, PEP 649 and PEP 563
It’s clear runtime uses of type annotations serve a real, sensible purpose, and Python benefits from supporting them.
Some of the detractions have been of the nature of “If in some future typing.TYPE_CHECKING” is no longer needed, we have a useless builtin" and “we shouldn’t encourage the use of it because it enables lying to a type checker”, the examples given show there are cases beyond that, so those examples help address those specific critiques about making this more prominent. Solving import time probably makes typing.TYPE_CHECKING okay for those cases, but making it a builtin won’t be leaving us with a useless builtin in any conceivable 10 year term future
The discussion of 0 or 100 makes no sense.
Your opinion seems to criticize all typing.TYPE_CHECKING use cases and forces all library authors to provide type hints that can be resolved at runtime.
I use pydantic and I know the benefits of using type information at runtime.
But pydantic uses type information defined by pydantic or dataclass. It does not use type information from all libraries at runtime.
TYPE_CHECKING does not force unresolvable type hints. It only provides one way to solve problems for library authors. Library authors should decide whether to use if TYPE_CHECKING or not. We should not force them to use or not use it.
I have already explained in Rejected Ideas why this PEP is not useless even if import typing becomes very fast.
Adding a section on lazy import would just repeat the same content twice.
To put the runtime type information argument in perspective:
Adding type information is generally helpful, but a tradeoff. We leave this to the authors. Therefore, type information is always potentially incomplete.
The same applies to runtime type information specifically. It is generally helpful, but a tradeoff. With type stubs and the existing typing.TYPE_CHECKING we have patterns that don’t support runtime type information. We leave the decision to use them to the authors. These patterns make the anyways incomplete static type information a little more incomplete for runtime. The worsening is only gradual.
IMHO we should not reject typing patterns solely on the argument that they don’t support runtime type information. While runtime type information is desirable, I’m not aware that we aspire parity with static information. This proposal solves real issues and I don’t see that we have equivalent runtime-compatible alternatives. While there could be better solutions in an ideal world (zero cost), this is as good as we can get under current circumstances.
We should give the authors appropriate tools with good documentation when an how to use them, and what the tradeoffs are. But in the end, we should enable and trust the authors to do the tradeoff decisions themselves.
Your opinion seems to criticize all
typing.TYPE_CHECKINGuse cases and forces all library authors to provide type hints that can be resolved at runtime.
That they should at the very least resolve to something at runtime, yes, so that accessing them does not produce errors. The opacity of typing imports (and by extension, typing-only symbols) has not been seriously addressed by any of the replies so far, other than to say that it is “resolvable by not publicly exporting [unimportable symbols]”, which is to say that we’ll make it easier for people to expose unimportable symbols, but we won’t assume any responsibility for the degradation in DX. The workaround I proposed in #149 for anyone who’d like to avoid the typing import has been overlooked as well. Clearly, I’m in the minority here thinking this change is detrimental, but I don’t think repeating the same points over and over again while they go unaddressed is fruitful. ![]()
I think we can all agree that getting errors when inspecting annotations at runtime is bad. But there are other ways to address this, that don’t put the onus on every single library that provides inline type hints. Specifically PEP 649 already addresses this to some extent by augmenting inspect.get_annotations and typing.get_type_hints with an additional mode that doesn’t error when it can’t resolve a name.
Unresolvable names are instead replaced with a ForwardRef. Admittedly runtime libraries usually won’t be able to do much with a ForwardRef[1], but they at least have access to the rest of the type information, without having to jump through hoops. So the runtime use of that incomplete type information can decide whether it should emit an error or if it instead should treat the missing name as Any/objectand perhaps emit a warning.
I agree that it would be nice if all libraries were written in such a way that all type hints were accessible at runtime with minimal runtime overhead, but I don’t think it’s realistic to expect everyone to go through the trouble[2]. Just like it’s not realistic to expect every library to provide complete high quality type hints. And as always, there will be a spectrum. There will be libraries that don’t provide any type information at runtime at all, libraries that provide complete type information and everything in between. Ultimately, it’s the library maintainer’s decision, what works best for them and the majority of their users.
We should strive to improve the experience on all sides of the equation, not just the runtime type consumers at the expense of the library maintainers or vice versa. I don’t believe there’s going to be a significant cost for runtime type consumers if this PEP were accepted, in a world where PEP 649 is already accepted and will be part of Python 3.14[3], which will make it much easier to work with incomplete runtime type information.
Ultimately the focus of everyone that cares deeply about the availability of runtime type information should be to make it easy to reduce the runtime cost of that information being there, when it’s not being used. That means driving forward proposals like deferred imports, so people won’t be tempted to reach for TYPE_CHECKING for that use-case, because the alternative is less verbose and easier to understand.
although they still definitely could. Like SQLAlchemy resolves forward references to models, even if they’re not available in the module’s scopes at runtime ↩︎
Considering how obtuse the current workarounds are ↩︎
this hopefully includes a backport of the upgraded get_type_hints function in typing_extensions ↩︎