A person in goggles uses a snake tong on one of two snakes biting each other's tails in a loop around a Python logo, next to a computer console labeled Shared.

Python Circular Imports: Why They Happen and How to Fix Them

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.
  • ImportError and AttributeError signal 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_CHECKING guard, 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 Them

Test 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:

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:

Language: Python Filename: 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:

Language: Python Filename: 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

Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Article

Already a member? Sign-In

Locked learning resources

The full article is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Article

Already a member? Sign-In

About Srujana Maddula

Srujana is a professional AI engineer and an expert technical writer. She builds AI and ML projects, writes tutorials for developers, and keeps a close eye on what's shipping across the tech industry.

» More about Srujana

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

What Do You Think?

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!

Become a Member to join the conversation.

Keep Learning

Related Topics: intermediate python