A Python circular import happens when two modules import each other and one of them isn’t finished loading yet. Python reports it as an ImportError or an AttributeError, usually with a message about a partially initialized module. In this tutorial, you’ll trace what Python does when that happens, then work through fixes ranging from removing the dependency to using workarounds.
To follow along, you should be comfortable with modules and packages, classes, and basic type hints.
By the end of this tutorial, you’ll understand that:
- A Python circular import happens when two modules depend on each other and one asks for a name before the other has finished loading.
- The reliable fix is to remove the dependency so that imports flow in one direction.
ImportErrorandAttributeErrorsignal the same cycle, and which one you get depends on the form of the import statement you used.- Moving an import inside the function that needs it is the only workaround that survives any import order.
- A type-only import still needs a
TYPE_CHECKINGguard, even on Python 3.14, where annotations are evaluated lazily.
To see why the error occurs where it does, you’ll start with the import machinery itself, then build a small project that reproduces the problem.
Take the Quiz: Test your knowledge with our interactive “Python Circular Imports: Why They Happen and How to Fix Them” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python Circular Imports: Why They Happen and How to Fix ThemTest your understanding of Python circular imports, from how the import system loads modules to the fixes and workarounds that break a cycle.
In Short: A Python Circular Import Is a Loop Between Modules
A circular import happens when two or more modules import each other, directly or through a chain. The cycle fails only when one module asks for a name that the other module hasn’t defined yet, because Python is still loading that module. You’ll see this as an ImportError or an AttributeError.
The reliable fix is to restructure your code so imports flow in one direction, toward the module that could exist without the other. Guard imports used only for type hints with TYPE_CHECKING. If you can’t restructure yet, move the import inside the function that needs it.
Why Python Circular Imports Fail
To understand why circular imports happen, you first need to know how Python imports modules.
Whenever Python imports a module, it first creates an entry for that module in sys.modules. It then starts executing the file from top to bottom.
Think of sys.modules as Python’s cache of imported modules. If another import requests a module that’s already in the cache, Python reuses the existing module object instead of importing it again.
The key is that Python adds the module to sys.modules before it finishes executing the file. That means the module can exist in a partially initialized state. Some functions, classes, and variables may not exist yet because Python hasn’t reached those lines of code. If another module tries to import one of those objects before it’s defined, Python raises an ImportError or AttributeError.
A Python Circular Import Example
Suppose you’re building a Python application to manage orders for an e-commerce store. You’ll build it as a shop package with a main.py script beside it:
shop_project/
│
├── shop/
│ ├── __init__.py
│ ├── order.py
│ └── product.py
│
└── main.py
The empty __init__.py makes shop a regular package. You can run the examples without it, but type checkers need it to work out that shop.order and shop.product belong together.
The package also keeps the error message accurate. From Python 3.13, if the failing module sits in the same folder as the script you run, Python suggests renaming the file instead of reporting the circular import. Keeping the modules inside shop/ avoids that misleading hint.
Every stage of this tutorial is a complete, runnable version of this project. The examples target Python 3.14, and the tutorial points out where older versions behave differently. You can download them all here:
Get Your Code: Click here to download the free sample code you’ll use to learn how circular imports break and how to fix them.
You have an Order class that tracks the product you order and any additional items you add. Since it works with Product objects, order.py imports the Product class:
shop/order.py
from shop.product import Product
SHIPPING_FEE = 4.99
class Order:
def __init__(self, product: Product, quantity: int) -> None:
self.product = product
self.quantity = quantity
self.extras: list[Product] = []
def add_shipping(self) -> None:
self.extras.append(Product("Shipping", SHIPPING_FEE))
def get_total(self) -> float:
subtotal = self.product.price * self.quantity
return subtotal + sum(extra.price for extra in self.extras)
Now look at product.py. Its order() method creates an Order object, and is_in_order() accepts an Order object to check whether the product belongs to that order. So product.py imports the Order class from order.py:
shop/product.py
from shop.order import Order
class Product:
def __init__(self, name: str, price: float) -> None:
self.name = name
self.price = price
def order(self, quantity: int) -> Order:
return Order(self, quantity)
def is_in_order(self, order: Order) -> bool:
return order.product.name == self.name