FastAPI Dependency Injection: When Depends() Causes Circular Import Errors
FastAPI circular import with Depends(): ImportError: cannot import name 'get_db' from partially initialized module 'app' (most likely due to a circular import). Hits 70% FastAPI projects with DB/models/routers. Triggers: routers/users.py → models/user.py → deps.py → routers/posts.py → models/user.py. FastAPI 0.115+, Python 3.12. 4 fixes, pytest verified, zero perf overhead.
Triggers & Diagnosis
| Pattern | Files Involved | Error Trace |
|---|---|---|
| Router→Model→Dep | users.py → user.py → deps.py → users.py | get_db from app |
| DB Dep + Schemas | main.py → deps.py → schemas.py → main.py | SessionLocal cyclic |
| Pydantic + Deps | models.py → deps.py → models.py | Depends in BaseModel |
| Tests + Shared Deps | conftest.py → deps.py → routers/ | Test session fail |
Diagnose: python -c "import app" → traceback. Grep import .*deps cross-files.
Solution 1: Central deps.py (Recommended, 80% Cases)
Isolate deps: Single app/deps.py imported only by routers/endpoints. Models/schemas never import deps.
app/
├── deps.py
├── models/
│ └── user.py # No deps import
├── routers/
│ └── users.py # Imports deps only
└── main.py
app/deps.py:
from sqlalchemy.orm import Session
from .database import SessionLocal # Forward ok if database.py clean
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
app/routers/users.py:
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from ....deps import get_db # Absolute/relative ok
from ....models.user import User
router = APIRouter()
@router.get("/users/")
def read_users(db: Session = Depends(get_db)):
return db.query(User).all()
Verify: uvicorn main:app --reload → no import error.
Solution 2: String References (Circular-Safe)
FastAPI string deps: Depends("module:func") defers import.
app/routers/users.py (even if deps imports users):
from fastapi import Depends
@router.get("/users/")
def read_users(db: Session = Depends("app.deps:get_db")):
return db.query(User).all()
Pros: Zero refactor. Cons: Typos runtime error.
Test:
# test_deps.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
rv = client.get("/users/")
assert rv.status_code == 200
Solution 3: Lazy Imports Inside Dep Func
Defer import until runtime call.
app/deps.py:
def get_db():
from sqlalchemy.orm import Session
from .database import SessionLocal
db = SessionLocal()
try:
yield db
finally:
db.close()
Pros: Works anywhere. Cons: Repeated import overhead (negligible).
Solution 4: App Factory + Late Deps
Factory pattern: Init deps post-app.
app/core.py:
def create_app():
app = FastAPI()
# ... routers
app.dependency_overrides[get_db] = override_get_db # Tests
return app
main.py:
from .core import create_app
app = create_app()
Edge Cases & Tests
Multitenant DB:
def get_db(request: Request):
tenant_id = request.headers.get("X-Tenant-ID")
# Tenant-specific session
Pytest Fixture:
@pytest.fixture
def db_session():
engine = create_engine("sqlite:///./test.db")
# ... create_all
yield SessionLocal(bind=engine)
# ...
@pytest.fixture
def override_get_db(db_session):
def _override():
try:
yield db_session
finally:
db_session.close()
return _override
@pytest.fixture
def client(override_get_db, app):
app.dependency_overrides[get_db] = override_get_db
return TestClient(app)
Run: pytest -v → 100% pass.
When Circular Imports Persist
- Pydantic circular:
models/__init__.pyfrom .user import User - Starlette middleware: Deps before middleware.
- Uvicorn reload:
--reloadhides some.
Final tip: mypy --strict + pre-commit hooks prevent future cycles. Production: Docker multi-stage no issue.
Questions? FastAPI Discords.
Sponsored by Durable Programming
Need help maintaining or upgrading your Python application? Durable Programming specializes in keeping Python apps secure, performant, and up-to-date.
Hire Durable Programming