Learning how to avoid circular imports in Python starts with understanding what causes them. A circular import occurs when two Python modules each try to import from the other, creating a dependency loop that the import system cannot resolve. Module A needs something from module B to finish loading, but module B needs something from module A to finish loading, and neither can complete initialization without the other. Python detects this stalemate and raises an ImportError, usually with a message about a partially initialized module. The error message can be confusing if you have not encountered it before, but the underlying cause is always the same: two modules that know too much about each other.
Circular imports are one of the most common stumbling blocks when you learn to avoid circular imports in Python. They appear suddenly when you split a working single-file script into multiple modules and discover that the functions you separated still need to call each other across file boundaries. The immediate impulse is to rearrange the import statements until the error goes away, but that approach treats the symptom rather than the cause. Avoiding circular imports in Python requires understanding why the dependency loop exists and making a structural change to break it.
What causes a circular import
The simplest circular import involves two modules that each import a name from the other at the top level. When Python imports the first module, it begins executing that module's code from top to bottom. When it reaches the import statement that pulls in the second module, it pauses execution of the first module, switches to the second module, and starts executing the second module's code. When the second module's import statement tries to pull in the first module, Python sees that the first module is already in the process of being loaded, with only the code before its import statement having run so far. Any name defined after that import statement does not exist yet, and accessing it produces an AttributeError wrapped in an ImportError.
The error is not random. It depends on which module is imported first, which means the same two modules might work when imported in one order and fail when imported in another. This order-dependence is what makes circular imports frustrating to debug: the bug appears and disappears based on seemingly unrelated changes to import statements in a third file that happens to trigger the import chain in a different sequence.
A common scenario involves two modules that both define classes or functions that reference each other. An orders module might define an Order class that needs a function from a customers module to look up customer details, while the customers module defines a Customer class that needs a function from the orders module to fetch a customer's order history. Both classes are reasonable, both imports make logical sense, and yet putting them in separate files creates a circular dependency that Python cannot resolve at import time.
The three reliable fixes
The most robust fix for a circular import is to extract the shared dependency into a third module. If both module A and module B need a particular function, class, or constant, move that shared piece into a new module C and have both A and B import from C without importing from each other. This breaks the cycle by making the dependency graph one-directional: both modules depend on the shared module, but neither depends on the other.
from shared.utils import format_price
from shared.constants import TAX_RATEThis pattern works because the shared module has no reason to import from either A or B. It contains only the code that both modules need, and the original modules call each other's functions indirectly through a higher-level orchestrator module that imports both A and B and coordinates their interaction.
The orchestrator pattern turns a two-way dependency into a one-way flow:
from orders import calculate_total
from customers import get_loyalty_discount
def final_price(order_id, customer_id):
subtotal = calculate_total(order_id)
discount = get_loyalty_discount(customer_id)
return subtotal - discountThe orchestrator knows about both modules, but the modules only know about the shared code and about themselves.
The second fix is to move the import statement inside a function body instead of keeping it at the module's top level. When an import is inside a function, it does not execute until the function is called, by which time both modules have typically finished loading. This is a valid stopgap that lets you keep the code working while you plan a more thorough restructuring. It is not a permanent solution because it hides the dependency and can cause performance surprises if the import-in-function is called in a tight loop, but it is useful for unblocking development.
The third fix, which applies when two modules are so tightly coupled that separating them creates more problems than it solves, is to merge them back into a single module. Some pairs of concepts genuinely belong together, and the attempt to separate them into different files was premature. If you find that every function in module A calls at least one function from module B and vice versa, the modules are not separate concerns; they are two halves of the same concern that should live in one file. This is not a failure of design; it is a recognition that module boundaries should follow conceptual boundaries, and sometimes the conceptual boundary is larger than you initially estimated.
Preventing circular imports through design
The best way to handle circular imports is to avoid creating them in the first place. Before splitting a module, ask yourself whether the two resulting modules would need to import from each other. If the answer is yes, the split is probably wrong. Either the shared code should be extracted into a third module, or the two pieces should stay together.
A useful mental model is to think of your module dependency graph as a directed acyclic graph, a tree-like structure where dependencies flow in one direction from higher-level modules to lower-level modules. High-level modules orchestrate the application and import from many lower-level modules. Mid-level modules implement business logic and import from utility modules. Low-level utility modules handle generic tasks like string formatting and file I/O and import from no other modules in your project. When every import arrow points downward in this hierarchy, circular imports are structurally impossible.
This layered architecture also makes your code easier to test and reason about. You can test a utility module in complete isolation because it depends on nothing. You can test a business logic module by mocking the utility modules it imports. You can test the orchestrator by mocking the business logic modules. This is the same layered thinking that guides how you organize Python code with packages, where each package represents a layer in the dependency hierarchy.
What does not fix circular imports
Reordering import statements within a file does not fix circular imports because the problem is not the order of the lines but the mutual dependency between modules. Changing the order might change which module fails first or which specific name triggers the error, but the underlying cycle remains and will surface in a different form.
Using wildcard imports or importing entire modules instead of specific names also does not fix the problem. The circular dependency exists regardless of the import style, and changing the style only changes the error message. Similarly, adding conditional imports that only run under certain circumstances hides the problem rather than solving it and makes the code harder to understand because the import relationships are no longer visible at the top of the file.
The only real fix is structural: break the cycle by extracting shared code, moving imports to function scope as a temporary measure, or merging modules that should not have been separated. For more on keeping import paths clean and predictable, see the article on importing modules in Python, which covers the import resolution process in detail.
Rune AI
Key Insights
- A circular import happens when module A imports from module B and module B imports from module A, causing one module to be partially initialized when the other tries to use it.
- The most reliable fix is to extract shared code into a third module that both original modules import one-directionally.
- Moving an import inside a function body is a valid quick fix but should be temporary.
- Reordering imports does not fix the underlying circular dependency and can make the problem harder to diagnose.
- If two modules are so interdependent that separating them causes circular imports, consider merging them into a single module.
Frequently Asked Questions
Why does Python allow circular imports in some cases but not others?
What is the difference between a circular import and a circular dependency?
Can I use a lazy import inside a function to avoid circular imports?
Conclusion
Circular imports are a symptom of modules that depend on each other in both directions. The fix is always structural: extract the shared dependency into a third module, move the import into a function body as a temporary measure, or merge the two modules if they are so tightly coupled that separation causes more problems than it solves. The best circular import is the one that does not exist.
More in this topic
Define and Call Python Functions
Learn the exact syntax for defining and calling Python functions with the def keyword, including naming rules, the function body, and how the call stack works.
Python Function Parameters and Arguments
Learn the difference between parameters and arguments, how to pass values by position and by keyword, and the rules for default parameter values.
Python Functions Explained
Learn what Python functions are, why they matter for organizing code, and how they form the foundation of non-trivial programs.