Everything works fine when your Flask app lives in a single app.py. Then you do the sensible thing — split it into routes.py, models.py, extensions.py — and suddenly Python throws an ImportError that makes no sense at all.
Circular imports are Flask’s most disorienting beginner trap, and they don’t just happen to beginners. Even experienced developers hit them when restructuring a codebase.
TLDR: Quick Fix for Flask Circular Imports
Root Cause: Two or more modules import from each other, creating a dependency loop Python can’t resolve.
The Fix: Use the Application Factory Pattern — create your Flask app inside a function and use lazy imports or Flask extensions properly.
# extensions.py — define extensions without attaching to an app
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
# app.py — create and configure the app here
from flask import Flask
from extensions import db
def create_app():
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
db.init_app(app)
from routes import main
app.register_blueprint(main)
return app
Import blueprints inside create_app() — never at the module level when they also import from app.py.
What the Error Looks Like
Flask circular import errors appear in a few different forms depending on which module Python was in the middle of loading:
ImportError: cannot import name 'db' from partially initialized module 'app'
(most likely due to a circular import)
ImportError: cannot import name 'User' from partially initialized module 'models'
(most likely due to a circular import)
ImportError: cannot import name 'app' from partially initialized module 'routes'
(most likely due to a circular import)
The phrase “partially initialized module” is the tell. Python started loading a module, hit an import statement mid-way through, jumped to load the second module — and that second module tried to import back from the first one before it finished loading. Python doesn’t know what to do, so it fails.
How to Diagnose the Circular Chain
Before reaching for a fix, trace the import loop. Read the full traceback carefully — Python usually shows you exactly which modules are involved.
Traceback (most recent call last):
File "app.py", line 2, in <module>
from routes import main
File "/myapp/routes.py", line 3, in <module>
from app import db
File "/myapp/app.py", line 2, in <module>
from routes import main
ImportError: cannot import name 'main' from partially initialized module 'routes'
That tells you: app.py imports routes.py, and routes.py imports app.py. That’s your loop. Once you can see the cycle, you know what to break.
Cause #1: Importing the App Object Directly in Routes
This is the most common cause. You create your app object in app.py, then your routes need it for @app.route, so you import it — but app.py also imports from your routes file.
The code that causes it:
# app.py
from flask import Flask
from routes import main # <-- imports routes at module level
app = Flask(__name__)
app.register_blueprint(main)
if __name__ == "__main__":
app.run()
# routes.py
from flask import Blueprint
from app import db # <-- imports from app.py
main = Blueprint("main", __name__)
@main.route("/")
def index():
users = db.session.query(User).all()
return "ok"
When Python loads app.py, it immediately tries to load routes.py (line 2). Inside routes.py, it immediately tries to load app.py again (line 2). But app.py isn’t finished yet — app and db don’t exist yet — so Python raises ImportError.
The fix: Move the blueprint import inside create_app(), and get db from an extensions module (see Cause #3):
# app.py
from flask import Flask
from extensions import db
def create_app():
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
db.init_app(app)
from routes import main # imported AFTER db is available
app.register_blueprint(main)
return app
# routes.py
from flask import Blueprint
from extensions import db # no longer imports from app.py
main = Blueprint("main", __name__)
Cause #2: Models Importing from Routes (or Vice Versa)
A subtler version of the same problem. Your models need a utility function from your routes, or your routes import a constant defined in models.py — and models already import from routes.
# models.py
from flask_sqlalchemy import SQLAlchemy
from routes import some_helper # importing from routes
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80))
# routes.py
from flask import Blueprint
from models import User # models imports from routes, routes imports from models
main = Blueprint("main", __name__)
def some_helper():
return "helper value"
Both files depend on each other. Python can’t load either one completely.
The fix: Utilities shared between models and routes belong in a separate utils.py or helpers.py module that neither imports from routes.py nor models.py:
# utils.py
def some_helper():
return "helper value"
# models.py
from flask_sqlalchemy import SQLAlchemy
# No import from routes.py
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80))
# routes.py
from flask import Blueprint
from models import User
from utils import some_helper # clean, no loop
main = Blueprint("main", __name__)
Think of your module dependency graph like a tree. Routes and models should only import from modules “below” them (extensions, utils, helpers) — never from each other.
Cause #3: Extensions Defined Inside app.py
Flask extensions like SQLAlchemy, LoginManager, and Migrate need to be initialized with an app object. The temptation is to define them right next to app:
# app.py — this causes circular imports
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy(app) # db is attached to app immediately
# models.py
from app import db # forces import of all of app.py
Now every file that needs db must import all of app.py, including your blueprint registrations. That’s a circular dependency waiting to happen.
The fix — use an extensions module:
# extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()
# app.py
from flask import Flask
from extensions import db, login_manager, migrate
def create_app(config=None):
app = Flask(__name__)
app.config["SECRET_KEY"] = "your-secret-key"
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
# Initialize extensions with app
db.init_app(app)
login_manager.init_app(app)
migrate.init_app(app, db)
# Register blueprints after extensions are ready
from routes.auth import auth_bp
from routes.main import main_bp
app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
return app
# models.py
from extensions import db # clean — only imports extensions.py
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return f"<User {self.username}>"
# routes/main.py
from flask import Blueprint, render_template
from extensions import db
from models import User
main_bp = Blueprint("main", __name__)
@main_bp.route("/")
def index():
users = User.query.all()
return render_template("index.html", users=users)
This structure means every file imports either from extensions.py or from the standard library — nothing circular.
Cause #4: The __init__.py Trap
When you use packages (directories with __init__.py), it’s easy to accidentally create circular imports through the package’s init file.
myapp/
__init__.py ← creates app AND imports routes here
routes.py ← imports from myapp (the package)
models.py
# myapp/__init__.py — common mistake
from flask import Flask
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
from myapp import routes # pulls in routes at package init time
# myapp/routes.py
from myapp import app # imports the package, which triggers __init__.py again
That loop goes: __init__.py → routes.py → __init__.py. Same problem, different packaging.
The fix: Keep __init__.py minimal — it should only call create_app(), not define routes or import from sub-modules at the top level:
# myapp/__init__.py — clean version
from flask import Flask
from .extensions import db
def create_app():
app = Flask(__name__)
app.config.from_object("config.DevelopmentConfig")
db.init_app(app)
with app.app_context():
from . import routes # deferred import inside function
return app
Alternatively, keep __init__.py completely empty and have a separate factory.py with create_app().
Still Not Working?
If you’ve applied the application factory pattern and still see circular imports, check these less-obvious culprits:
current_app vs. app: If you need the app object inside a route or model method, use flask.current_app instead of importing app directly. current_app is a proxy that resolves at request time without creating an import dependency:
from flask import current_app
def get_upload_folder():
return current_app.config["UPLOAD_FOLDER"]
Type annotations at module level: If you use type hints that reference other models, Python evaluates them at import time. Use from __future__ import annotations or quote the type to make it a string:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from models import Order # only imported when type checking, not at runtime
class User(db.Model):
orders: list[Order] # works fine because annotations are lazy
celery.py or task files importing from app.py: Celery tasks often need the app context. Set up a dedicated celery_worker.py that calls create_app() and creates the Celery instance there, rather than importing from your main app.py.
Wildcard imports (from models import *): These force Python to fully evaluate the module immediately to know what * contains. Replace them with explicit imports.
The Complete Project Structure That Avoids Circular Imports
Here’s a battle-tested Flask project layout that eliminates circular import issues:
myapp/
├── run.py # entry point: imports create_app and calls it
├── config.py # configuration classes (no Flask imports)
├── extensions.py # db, login_manager, etc. — initialized without app
├── models/
│ ├── __init__.py # re-exports models
│ ├── user.py # imports from extensions only
│ └── post.py # imports from extensions only
├── routes/
│ ├── __init__.py # empty or minimal
│ ├── auth.py # imports from extensions and models
│ └── main.py # imports from extensions and models
├── services/
│ └── email.py # business logic, no Flask app imports
└── app.py # create_app() lives here
# run.py
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(debug=True)
The import direction is always one-way: routes → models → extensions → nothing Flask-specific. Nothing ever imports back “up” the chain.
Summary Checklist
When you hit a Flask circular import, work through this list:
- [ ] Identify the loop: Read the full traceback to find which two (or three) modules are cycling
- [ ] Move extension definitions to
extensions.py:db = SQLAlchemy()with no app attached - [ ] Use
init_app()pattern: Calldb.init_app(app)insidecreate_app(), not at module level - [ ] Import blueprints inside
create_app(): Never at the top ofapp.py - [ ] Routes import from
extensions.pyandmodels.py: Not fromapp.py - [ ] Models import from
extensions.pyonly: Not from routes or app - [ ] Shared utilities live in
utils.py: A module nothing else depends on - [ ] Use
current_appproxy instead of importing theappobject directly inside functions - [ ] Keep
__init__.pyminimal: Don’t import sub-modules at package init time
Once you restructure around the application factory pattern, circular imports stop being a recurring problem. The dependency graph flows in one direction, Python can load each module completely before moving to the next, and your app scales cleanly as you add more routes, models, and blueprints.
If you’re chasing down the exact source of your import error, paste your Python traceback into Debugly’s trace formatter to get a clean, readable view of the full call stack — it makes it much easier to spot which module is the problem.
For related Flask debugging topics, check out our guide on Flask request context errors — another common issue that trips developers up when code runs outside the normal request lifecycle.