Circular Imports and TYPE_CHECKING
Circular imports often happen when two modules need each other only for type annotations. The typing.TYPE_CHECKING constant is true under static analysis and false at runtime, so imports declared behind it never execute: the cycle disappears and the type checker still sees everything. This note records the pattern and how it applies to ORM relationship declarations.
TYPE_CHECKING is a constant from the typing module. It is True during type checks and False at runtime. This means you can import classes for type hints without causing circular imports at runtime.
When you use an import inside an if TYPE_CHECKING: block, Python skips it at runtime. Yet type checkers like mypy or pyright will see it for verifying your annotations.
Basic Example§
# a.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from b import B # This import won't run at runtime
class A:
def link_to_b(self, b: 'B') -> None:
print('Linking A with B')
# b.py
class B:
pass
Here, A refers to B for type hints without importing it during runtime. That helps avoid circular imports.
Usage in an ORM§
# models.py
from typing import TYPE_CHECKING
from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from database_setup import Base
if TYPE_CHECKING:
from other_models import RelatedModel
class MyModel(Base):
__tablename__ = 'my_model'
id = Column(Integer, primary_key=True)
related_id = Column(Integer, ForeignKey('related_model.id'))
# For type hints only:
related_model: 'RelatedModel' = relationship('RelatedModel')
This prevents runtime loops, because the if TYPE_CHECKING: block loads only for type analysis.
Why Use It§
- Prevents circular imports by avoiding runtime loops between modules.
- Improves readability by separating type hints from the normal import flow.
- Helps type checkers by allowing tools to verify your types.
References§
- typing.TYPE_CHECKING, Python documentation.
- PEP 484 — Type Hints: Runtime or type checking?, python.org.