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

FastAPI Dependency Injection: When Depends() Causes Circular Import Errors


When you build FastAPI applications with shared dependencies like database sessions, circular import errors often arise. You’ll see something like: ImportError: cannot import name 'get_db' from partially initialized module 'app' (most likely due to a circular import). These errors occur because routers import models, models import deps, and deps import routers—forming cycles during module initialization. We encounter this when organizing code into routers, models, schemas, and deps. FastAPI’s Depends() defers execution, but the import happens upfront. Let’s explore why this happens and how to resolve it without compromising your architecture.

Diagnosing Circular Imports\n\nBefore we explore solutions, let’s diagnose the circular import properly. Understanding the cycle helps you choose the right fix and prevents recurrence.\n\nCommon patterns include:\n\n| Pattern | Files Involved | Typical Error |\n|---------|----------------|--------------|\n| Router → Model → Dep | routers/users.pymodels/user.pydeps.py → back to router | cannot import name 'get_db' from partially initialized module |\n| DB Dep + Schemas | main.pydeps.pyschemas.pymain.py | SessionLocal cyclic reference |\n| Pydantic Models + Deps | models.pydeps.pymodels.py | Depends not resolved in BaseModel |\n| Tests + Shared Deps | conftest.pydeps.pyrouters/ | Test session initialization fails |\n\n### Steps to Identify the Cycle\n\n1. Trigger the import: Run python -c \"import app\" (adjust for your module). The traceback shows the partially initialized module and import chain.\n\n2. Search imports: grep -r \"from .* import.*deps\\|get_db\" . or grep -r \"import deps\" . to find cross-imports.\n\n3. Visualize dependencies: Install pip install pydeps then pydeps your_app_package. This generates a graph showing cycles (loops in the diagram).\n\n4. Module graph: Use python -m modulefinder your_app.py for a text report of imports.\n\nThese steps reveal the cycle quickly, often in under a minute.

Solution 1: Central Dependencies Module\n\nOne effective approach centralizes dependencies in a dedicated app/dependencies.py (or deps.py) module. You import this only from entry points like routers and endpoints. Models, schemas, and database definitions never import it.\n\nThis separation of concerns keeps data models pure—they define structure without runtime knowledge. Dependencies become infrastructure code, loaded only when handling requests.\n\nOf course, this requires auditing and moving imports, but it scales well for growing apps.\n\nHere’s the structure:\n\n\napp/\n├── dependencies.py\n├── database.py\n├── models/\n│ └── user.py # Pure models, no deps\n├── schemas/\n│ └── user.py # Pydantic, no deps\n└── routers/\n └── users.py # Imports deps here\n\n\napp/dependencies.py:\npython\nfrom sqlalchemy.orm import Session\nfrom app.database import SessionLocal\n\n\ndef get_db():\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\napp/routers/users.py (excerpt):\npython\nfrom fastapi import APIRouter, Depends\nfrom sqlalchemy.orm import Session\nfrom app.dependencies import get_db\nfrom app.models.user import User\n\nrouter = APIRouter()\n\n@router.get(\"/users/\")\ndef read_users(db: Session = Depends(get_db)):\n return db.query(User).all()\n\n\nStart your app: uvicorn main:app --reload. Imports load cleanly—no cycles.

Solution 2: String References in Depends\n\nFastAPI allows string references in Depends, like Depends("module:function"). FastAPI resolves the callable at runtime, avoiding import-time cycles.\n\nThis works even if deps.py imports from routers—handy for tight coupling you can’t refactor immediately.\n\napp/routers/users.py:\npython\nfrom fastapi import APIRouter, Depends\nfrom sqlalchemy.orm import Session\n\nrouter = APIRouter()\n\n@router.get(\"/users/\")\ndef read_users(db: Session = Depends(\"app.dependencies:get_db\")):\n # Note: User query assumes db works\n from app.models.user import User\n return db.query(User).all()\n\n\nUse this for quick fixes or prototypes. Though convenient, drawbacks include:\n- Runtime errors on typos or renames (no static checks)\n- Limited IDE autocompletion and refactoring support\n- Type checkers like mypy may complain\n\nVerify with your test client:\npython\nfrom fastapi.testclient import TestClient\nfrom app.main import app\n\nclient = TestClient(app)\nresponse = client.get(\"/users/\")\nassert response.status_code == 200\n\n\nYou might prefer this when refactoring time is short, but consider migrating to Solution 1 for production.

Solution 3: Lazy Imports Inside the Dependency Function\n\nMove problematic imports inside the dependency function itself. They execute only when the function runs—after module loading completes.\n\nThis requires minimal changes: edit deps.py, no router adjustments needed.\n\napp/dependencies.py:\npython\ndef get_db():\n from sqlalchemy.orm import Session\n from app.database import SessionLocal\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\nTrade-offs:\n- Pros: Tiny refactor; works in deeply nested cycles.\n- Cons: Imports repeat per request (though Python caches them); harder debugging (errors appear deeper in stack); mixes concerns slightly.\n\nSuitable for dependencies called infrequently or when restructuring isn’t feasible.

Solution 4: Application Factory Pattern\n\nFor larger applications, adopt an app factory. You create the FastAPI instance inside a function, including routers and deps registration after all modules load.\n\nThis pattern shines in configurable apps or when testing overrides deps easily.\n\napp/core.py:\npython\nfrom fastapi import FastAPI\nfrom app.routers import users\n\nfrom app.dependencies import get_db # Safe here\n\n\ndef create_app():\n app = FastAPI()\n app.include_router(users.router)\n # Register other deps if needed\n return app\n\n\napp/main.py:\npython\nfrom app.core import create_app\n\napp = create_app()\n\n\nBenefits: Full control over init order; easy testing with overrides.\nDrawbacks: Adds boilerplate; overkill for small apps.\n\nUse when your app grows beyond a single main.py.

Testing Dependencies and Edge Cases\n\nNo matter the solution, test your deps thoroughly. Pytest fixtures override production deps with test doubles.\n\nFull pytest setup (tests/conftest.py):\npython\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom app.main import app\nfrom app.dependencies import get_db\n\nSQLALCHEMY_DATABASE_URL = \"sqlite:///./test.db\"\nengine = create_engine(SQLALCHEMY_DATABASE_URL)\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n@pytest.fixture(scope=\"function\")\ndef db_session():\n # Create tables\n Base.metadata.create_all(bind=engine)\n db = TestingSessionLocal()\n try:\n yield db\n finally:\n db.close()\n Base.metadata.drop_all(bind=engine)\n\n@pytest.fixture\ndef override_get_db(db_session):\n def _override():\n try:\n yield db_session\n finally:\n db_session.close()\n return _override\n\n@pytest.fixture\ndef client(override_get_db):\n app.dependency_overrides[get_db] = override_get_db\n yield TestClient(app)\n app.dependency_overrides.clear()\n\n\nTest file (test_routers.py):\npython\ndef test_read_users(client):\n response = client.get(\"/users/\")\n assert response.status_code == 200\n\n\nRun pytest—your deps work in isolation.\n\nEdge cases:\n- Multitenancy: Pass Request to deps: def get_db(request: Request): tenant_id = request.headers.get(\"X-Tenant\")\n- Pydantic cycles: Avoid Depends in model definitions; use factories.\n- Middleware: Register deps after middleware.\n- Reload mode: --reload masks some issues—test full imports.\n\nPrevent recurrence: mypy --strict for static checks; pre-commit for import linting (e.g., isort, flake8).\n\nFor discussion: FastAPI Discord.

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