backend / python core / tricky questions / 21_circular_imports.md

Circular imports

2 min read source

Circular imports

The gotcha

When module A imports B and B imports A, one of them sees a partially initialized version of the other. The error usually surfaces as ImportError: cannot import name 'X' from partially initialized module or as AttributeError later.

Minimal repro

# a.py
from b import b_func

def a_func():
    return b_func() + 1

# b.py
from a import a_func

def b_func():
    return a_func() + 1
>>> import a
ImportError: cannot import name 'b_func' from partially initialized module 'b'
(most likely due to a circular import)

Why it happens

When you import a:

  1. Python creates an empty a module object and starts executing top of a.py.
  2. from b import b_func triggers import b. Python creates empty b module, starts executing b.py.
  3. from a import a_func looks up a — which exists (step 1) but is partially initialized (no a_func yet, since step 1 hasn’t gotten past line 1).
  4. The from a import a_func raises because a_func doesn’t exist yet on the half-built a.

Two modules importing each other is a code-smell — it usually means the boundary is wrong.

How to fix

Option 1: refactor — extract shared code

If A and B both need helpers, put them in a separate c.py that neither imports the other:

# c.py — shared utilities
def shared(): ...

# a.py
from c import shared
# b.py
from c import shared

This is almost always the right answer.

Option 2: lazy import (function-local)

If you must keep the cycle, defer the import until the function runs — by which time both modules are fully loaded:

# a.py
def a_func():
    from b import b_func   # imported at call time, not module-load time
    return b_func() + 1

Performance: each call re-runs the from ... import statement, which is cheap (just dict lookup) but not free.

Option 3: import b instead of from b import x

A module reference is created early; attribute lookup happens at call time:

# a.py
import b           # creates a's reference to b's namespace
def a_func():
    return b.b_func() + 1   # attribute resolved when called

This works as long as b.b_func exists by the time a_func runs.

Option 4: TYPE_CHECKING for type hints

If the cycle is only for type annotations:

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from b import B   # imported only by type checker, not at runtime

Interview angle

“You see ImportError: cannot import name X from partially initialized module Y. What’s happening and how do you fix it?” The answer that lists all four options (refactor, lazy import, module-level import, TYPE_CHECKING) and recommends refactoring first is strong.