Circular Imports — How They Happen and How to Fix Them
Imagine two friends, Alice and Bob, trying to introduce each other. Alice can't introduce Bob until she knows who Bob is, but Bob can't introduce Alice until he knows who Alice is. They're stuck in a loop. For Python, this means a module isn't fully defined yet when another module tries to pull a name from it.
The Setup
You are structuring an API backend. A User model needs to send automated emails using an EmailService, while the EmailService needs to check a User's billing tier before formatting notifications. You define them in separate files, leading to a circular import error on startup.
What Does This Print?
import sys
try:
# Module 'user' starts loading:
sys.modules['user'] = object()
# Module 'email_service' imports 'User' from 'user'
from user import User
except ImportError as e:
print(f"ImportError caught: {e}")
The Output
Python attempts to load email_service, which immediately triggers an import of User from user. However, because user is still in the middle of executing its imports, the User class has not yet been defined in its namespace, resulting in an ImportError.
Why Python Does This
Python tracks imported modules in the sys.modules dictionary. When a module is imported, an empty module object is created and added to sys.modules before its code is executed. When email_service attempts to import from user, Python sees user already exists in sys.modules and returns it without re-executing it. But because the execution of user.py was suspended to resolve email_service, the name User hasn't been defined in the user module object yet, causing the attribute lookup to fail.
The Fix
class User:
def notify(self):
# Defer import until runtime to break the compilation circular loop
from email_service import EmailService
EmailService().send(self)
Importing the entire module ('import module_name') defers the resolution of its attributes until they are actually accessed (e.g., 'module_name.ClassName'). This allows both modules to complete their initial loading without immediate name lookups that would trigger a NameError in a partially defined module.
How This Fails in Real Systems
A large Django monolith suffered a circular import when the Order model imported the Invoice model to generate bills, and Invoice imported Order to update checkout statuses. A developer added some utility imports that triggered this loop, instantly crashing the production WSGI application servers during a midnight deploy.