Python
Circular Imports
Programming
Code Issues
Software Development

How to avoid circular imports in Python?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Circular imports in Python occur when two or more modules depend on each other, forming a dependency cycle. This is one of the most common structural problems in Python projects, and it can surface as ImportError, AttributeError, or silently produce None values from partially initialized modules. Learning how to identify and resolve circular imports will help you write cleaner, more maintainable code.

What Are Circular Imports?

A circular import happens when module A imports module B, and module B imports module A (directly or through a chain of other modules). When Python encounters this cycle, it does not re-execute the module that is already being loaded. Instead, it returns whatever has been defined so far, which often means the name you need has not been assigned yet.

Consider two files:

python
1# module_a.py
2from module_b import helper_b
3
4def helper_a():
5    return "A"
python
1# module_b.py
2from module_a import helper_a
3
4def helper_b():
5    return helper_a()

When you run module_a.py, Python begins loading it, hits the import of module_b, starts loading module_b, and then tries to import helper_a from the still-incomplete module_a. At that point helper_a has not been defined yet, so Python raises an ImportError.

Strategy 1: Deferred (Lazy) Imports

The simplest fix is to move the import inside the function that actually needs it. This delays the import until runtime, by which time both modules are fully loaded.

python
1# module_b.py
2def helper_b():
3    from module_a import helper_a  # imported only when called
4    return helper_a()

This approach works well when the cross-module call happens infrequently. The import statement is cached by Python after the first execution, so the performance cost is negligible.

Strategy 2: Restructure Into a Third Module

When two modules share definitions that cause a cycle, extract the shared code into a new, independent module that both can import without conflict.

python
# shared.py
def common_logic():
    return "shared"
python
1# module_a.py
2from shared import common_logic
3
4def helper_a():
5    return common_logic()
python
1# module_b.py
2from shared import common_logic
3
4def helper_b():
5    return common_logic()

This is the most architecturally sound solution because it eliminates the cycle entirely and makes the dependency graph a clean tree.

Strategy 3: Import at the Module Level With import Instead of from

Using import module_a instead of from module_a import helper_a can sometimes sidestep the problem. With the plain import form, Python only needs the module object to exist (which it does as soon as loading starts). The attribute lookup happens later when you actually call module_a.helper_a().

python
1# module_b.py
2import module_a
3
4def helper_b():
5    return module_a.helper_a()

This works because module_a is already registered in sys.modules by the time module_b finishes loading. The attribute is resolved at call time, not at import time.

Strategy 4: The TYPE_CHECKING Guard

When the circular import exists solely to support type annotations, Python provides an elegant escape hatch. The typing.TYPE_CHECKING constant is True only during static analysis (mypy, Pyright) and False at runtime, so the import never actually executes.

python
1from __future__ import annotations  # makes all annotations strings
2from typing import TYPE_CHECKING
3
4if TYPE_CHECKING:
5    from module_a import MyClass
6
7def process(item: "MyClass") -> None:
8    ...

The from __future__ import annotations import (available since Python 3.7) turns every annotation into a string automatically, so you do not need to quote them manually in Python 3.10 and later.

Common Pitfalls

  • Hiding the real design problem. Deferred imports fix the symptom but not the cause. If two modules are tightly coupled, refactoring into a third module is usually the better long-term choice.
  • Circular imports that fail silently. Sometimes Python does not raise an error but instead sets the imported name to None because the module was only partially initialized. This leads to confusing TypeError: 'NoneType' object is not callable messages at runtime.
  • Overusing TYPE_CHECKING for runtime logic. The TYPE_CHECKING guard only helps with annotations. If you try to instantiate or call the imported class at runtime inside that block, it will fail with a NameError.
  • Forgetting from __future__ import annotations. Without this import in Python versions before 3.10, annotations are evaluated eagerly, so a forward-referenced type inside TYPE_CHECKING still triggers an error unless you wrap it in quotes.
  • Creating deep import chains. A cycle does not have to be between just two files. Three or more modules can form a chain (A imports B, B imports C, C imports A) that is much harder to spot. Tools like pydeps or import-linter can visualize your dependency graph.

Summary

  • Circular imports happen when two or more modules depend on each other, causing partially initialized modules and import errors.
  • Deferred imports (moving the import inside a function body) are the quickest fix and work well for infrequent cross-module calls.
  • Extracting shared code into a third module removes the cycle entirely and is the cleanest architectural solution.
  • Using import module instead of from module import name defers attribute resolution to call time, which can break the cycle.
  • The TYPE_CHECKING guard combined with from __future__ import annotations eliminates cycles that exist only for type hints without any runtime cost.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.