The go-to resource for upgrading Python, Django, Flask, and your dependencies.

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.pymodels/user.pydeps.pyrouters/posts.pymodels/user.py. FastAPI 0.115+, Python 3.12. 4 fixes, pytest verified, zero perf overhead.

Triggers & Diagnosis

PatternFiles InvolvedError Trace
Router→Model→Depusers.py → user.py → deps.py → users.pyget_db from app
DB Dep + Schemasmain.py → deps.py → schemas.py → main.pySessionLocal cyclic
Pydantic + Depsmodels.py → deps.py → models.pyDepends in BaseModel
Tests + Shared Depsconftest.py → deps.py → routers/Test session fail

Diagnose: python -c "import app" → traceback. Grep import .*deps cross-files.

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__.py from .user import User
  • Starlette middleware: Deps before middleware.
  • Uvicorn reload: --reload hides 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