Alex Lin Wang 王帅

TYPE_CHECKING

Import types conditionally to avoid circular imports and heavy dependencies while maintaining clean type annotations.

Imagine you have a case where you have a circular dependency. Usually, this is a problem, and a quick post on interface stubs may be helpful, but sometimes refactoring it wouldn't make sense, and the circular import may not be practical to refactor.

Instead, what you may do is use TYPE_CHECKING:

from __future__ import annotations  # optional but handy for forward refs
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # Only imported for type checkers; not executed at runtime
    from some_heavy_lib import BigType

class Thing:
    def __init__(self, x: BigType):  # works for type checkers
        self.x = x

The TYPE_CHECKING constant is False at runtime but True when type checkers like mypy or PyCharm analyze your code. This means imports inside the if TYPE_CHECKING: block are only processed during static type checking.

This pattern is useful for several scenarios:

Avoiding circular imports: When two modules need to reference each other's types, you can break the cycle by importing one side only for type checking.

# user.py
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from order import Order

class User:
    def __init__(self, name: str):
        self.name = name
        self.orders: list[Order] = []
# order.py  
from user import User

class Order:
    def __init__(self, user: User, amount: float):
        self.user = user
        self.amount = amount

Skipping heavy dependencies: Import expensive libraries only for type hints without the runtime overhead.

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import pandas as pd
    import numpy as np

def process_data(df: pd.DataFrame) -> np.ndarray:
    # pandas and numpy aren't imported at runtime
    # but type checkers understand the annotations
    pass

Clean annotations: Keep type annotations readable without string literals or complex workarounds.

# Instead of this:
def bad_example(data: "ComplexType") -> "list[ComplexType]":
    pass

# You can write this:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from complex_module import ComplexType

def good_example(data: ComplexType) -> list[ComplexType]:
    pass

TYPE_CHECKING is essential when you need to reference types that would cause circular imports, performance issues, or dependency problems, while still maintaining excellent static type checking support.

Questions or feedback? Feel free to reach out!