TYPE_CHECKING for Avoiding Circular Imports
Overview
In Python, using type hints often requires importing classes from other modules. If two modules import each other just for type hinting, it creates a circular import error at runtime. To solve this, Python provides `typing.TYPE_CHECKING`. It is a special constant that is `True` when a static type checker (like `mypy`) analyzes the code, but `False` when Python actually runs it. from typing import TYPE_CHECKING if TYPE_CHECKING: from database import UserSession def close_session(session: 'UserSession') -> None: pass By placing type-only imports inside an `if TYPE_CHECKING:` block, you hide them from Python at runtime. This cleanly breaks the circular import cycle. The mental model is 'two parallel universes': one universe where the type checker runs (and sees the imports), and another where Python runs the code (and skips the imports). if TYPE_CHECKING: from models import User def is_admin(obj: object) -> bool: return isinstance(obj, User) # NameError: name 'User' is not defined **Edge Case**: Because `User` is never imported at runtime, any attempt to use it as an actual value (like in an `isinstance()` check) will crash.
It solves the catch-22 of needing to import a module to annotate types, but not being able to import it because it would cause a circular import crash.
Where used: Bidirectional relationships in ORM models, Complex project structures, Interdependent service layers
Why learn this
- You can type hint complex architectures without restructuring your entire codebase to avoid import cycles.
- It reduces runtime overhead because type-only modules aren't loaded into memory.
Code walkthrough
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from models import User
def process(data: 'User') -> None:
print('Processed')
process(None)
Focus: if TYPE_CHECKING:
Aha moment
from typing import TYPE_CHECKING
print(TYPE_CHECKING)
Prediction: What is the value of `TYPE_CHECKING` at runtime?
Common guess: `True`, because type checking type-hints are built into Python.
It prints `False`. Python itself does not enforce types at runtime; the static type checker runs entirely separately.
Common mistakes
- Using the imported name directly at runtime: If an import is guarded by `if TYPE_CHECKING:`, it doesn't exist at runtime. Using it outside of string-based type hints (or `from __future__ import annotations`) will cause a `NameError`.
- Forgetting string annotations: Without `from __future__ import annotations`, you must use string literals for types imported under `TYPE_CHECKING`, like `def foo(user: 'User'):`.
Glossary
- circular import
- An error that occurs when two or more modules attempt to import each other, creating an infinite loop. Example: `import models`.
- static analysis tools
- Programs that analyze your code without running it to find potential errors, like incorrect data types. Example: `mypy app.py`.
Recall questions
- What is the value of `typing.TYPE_CHECKING` when a Python script is executed?
- How does `TYPE_CHECKING` prevent circular import errors?
Understanding checks
What does this code print when executed by Python?
`20`
At runtime, `TYPE_CHECKING` is always `False`, so the code inside the `else` block executes.
Where is the runtime bug in this code?
`return User()` raises a `NameError`.
The `User` class is defined inside the `TYPE_CHECKING` block, so it doesn't exist at runtime. You cannot instantiate it.
Practice tasks
Fix the NameError
This code raises a `NameError` because `Database` is imported under `TYPE_CHECKING` but used natively as a type hint. Fix the type hint so it works at runtime by making it a string.
Challenge
Future annotations
Instead of string quotes, use `from __future__ import annotations` at the top of the file so `Database` can be written normally.