What is typing.TYPE_CHECKING, and why guard an import with it?
answer
- A constant with two different truths
- Never true while the program runs
- The analyser reads the block anyway
- Import that exists only for the checker
- typing.TYPE_CHECKING guards a typing-only import
basics
~20 styping.TYPE_CHECKING is a constant that is False whenever the program actually runs, but every static type checker analyses the block as if it were True. An import placed inside that block therefore exists only for the checker.
solid answer
~40 s`typing.TYPE_CHECKING` is an ordinary module-level constant bound to `False`. Static analysers special-case it and analyse the guarded block as though it were `True`, so `if TYPE_CHECKING: from decimal import Decimal` hands the checker a name the interpreter never imports. Teams reach for it for three reasons: a module needed only by annotations, an expensive import that would slow start-up for nothing, and an import that would otherwise close a dependency cycle. The price is that the name genuinely does not exist at runtime, so nothing that evaluates the annotation later can find it. Since 3.14 annotations are evaluated lazily by default, so a plain function or class hint naming a guarded import no longer explodes when the module is imported; on older runtimes you quote the hint or add the future import.
code
python · 12 linesfrom typing import TYPE_CHECKING
if TYPE_CHECKING:
from decimal import Decimal
def to_cents(amount: "Decimal") -> int:
return int(amount * 100)
print(TYPE_CHECKING)
print("Decimal" in globals())go deeper
Recall the two facts that matter: the constant is False when the program runs, and type checkers analyse the block anyway. Be able to say what the guard is for in one sentence.
Explain the mechanics: the checker resolves the condition without executing anything, so the name exists for analysis only. Say which uses of a guarded name are safe (annotations) and which are not (base classes, decorators, isinstance checks).
Show judgment about when the guard earns its keep — start-up cost, cycle breaking, stub-only names — and about what it breaks. Be ready to say what happens in a service that inspects annotations at runtime once the import is hidden.
Own the convention across a codebase: whether the project still carries the future import, what the minimum runtime is, and how linting keeps guarded blocks from drifting into code that is actually called.
### The constant itself There is no magic in `typing.TYPE_CHECKING`. It is a module-level name in the standard library bound to the boolean `False`, and it has been there since 3.5.3. Print it in a REPL and you get `False`, every time, on every implementation. The magic lives entirely in the tools that read your source without running it. A static type checker never executes the module. It walks the syntax tree and reasons about it, and one of the handful of conditions it resolves symbolically rather than leaving unknown is a reference to `TYPE_CHECKING`. The checker declares that condition true and analyses the body. The interpreter, doing the opposite thing, evaluates `False` and skips the body. The same lines of source are therefore visible to one consumer and invisible to the other, deliberately. ### What the guard is for Three distinct reasons, and it is worth being able to name them separately. The first is cost. A module may be imported purely so a parameter can be annotated with a class that lives there. Nothing in the running code touches it. Hiding that import shortens start-up and shrinks the import graph, which matters most in short-lived processes and command-line tools where import time dominates. The second is cycles. If two modules refer to each other and one direction is needed only for annotations, that direction is a fake edge in the dependency graph. The guard deletes it at runtime while keeping it for the checker. The third is names that have no runtime object at all. Some declarations exist only in stub files or only for the type system; there is nothing to import when the program runs, so the guard is the only way to mention them. ### What it costs The guarded name is absent at runtime. That is the whole point, and it is also the whole hazard. Every use of the name has to be one the interpreter never evaluates. Annotations on functions, methods, module-level variables and class attributes are the safe category, because since 3.14 (PEP 649) they are not evaluated when the definition executes. They are compiled into a separate function that runs only when something asks for the annotations. On 3.13 and earlier, an annotation was evaluated eagerly unless the module carried `from __future__ import annotations` (3.7, PEP 563) or the hint was written as a string literal, which is why so much library code still carries the future import: it must import cleanly on the oldest runtime the project supports. Everything that is not an annotation still evaluates immediately, and this is where the guard bites. A base class in a `class` statement is evaluated. A decorator you actually apply is evaluated. A module-level alias written as a plain assignment is evaluated. The first argument of a `cast()` call is evaluated unless you quote it. An `isinstance()` check is evaluated. If the name you moved behind the guard appears in any of those positions, the module raises `NameError` on import, and the fix is to keep the import real rather than to guard it. ### Where teams get it wrong The most common mistake is treating the block as a lazy import: developers assume the module will be pulled in the first time the name is used. It will not be. There is no deferral mechanism here, only a branch that is never taken. The second is assuming that hiding the import makes the annotation resolvable anyway. Anything that inspects hints at runtime, such as a framework that reads a function signature to build validation or dependency injection, will look for a name the module never bound. That failure surfaces at inspection time rather than at import time, which makes it harder to trace. ### Practical shape The convention that survives review is: one guarded block near the top of the module, immediately after the real imports; only names used in annotations inside it; the future import present if the project still supports runtimes older than 3.14. Some linters can detect typing-only imports and move them into the guard for you, which keeps the block honest as code changes. And if a name inside the block starts being called at runtime, it must be promoted back to a real import in the same change.
- If the constant is always False at runtime, how does a checker decide to look inside the block?It never runs the code at all. A checker resolves a small set of conditions symbolically before analysing a branch: a reference to `TYPE_CHECKING` is treated as true, and `sys.version_info` and `sys.platform` comparisons are resolved against the version and platform it was told to analyse. The body is then analysed as live code, while the interpreter simply skips a false branch.
- Besides annotation-only imports, what else belongs behind the guard?A heavy module pulled in solely to name a class in a hint, an import that would close a dependency cycle, and names that exist only for the type system with no runtime object behind them. Anything the running code calls, subclasses, decorates with, or passes to `isinstance()` must stay a real import — the guard offers no lazy loading, only a branch nobody takes.
It is a stage direction in a script: the actors never speak the line, but everyone reading the script knows what is meant to happen there.
saying these in an interview costs you the question
- Says the constant becomes True while a checker runs the code
- Thinks the guarded import happens lazily on first use
- Puts a module the running code actually calls inside the guard
- Believes a checker plugin defines it rather than typing
- Assumes every annotation stays resolvable at runtime afterwards