Python circular imports and TYPE_CHECKING

ยท 234 words ยท 2 minute read

Avoid Circular Imports with TYPE_CHECKING in Python ๐Ÿ”—

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.

For more details, visit the Python docs on TYPE_CHECKING.