Skip to content
Dashboard

What is dependency injection in Python?

Python code becomes harder to test when functions create their own database clients or HTTP sessions. Dependency injection (DI) lets the caller pass those objects into a function or class, so tests can supply fakes without patching application modules.

For a FastAPI app on Vercel, use ordinary arguments in business logic, Depends() for endpoint dependencies, and lifespan for resources shared within each function instance.

Copy link to headingWhat is dependency injection in Python?

With dependency injection, the caller chooses which database client, email sender, or other dependency a function or class receives. Martin Fowler described and helped name the pattern in 2004.

A ReportService that accepts a db argument can use a real database in production and a fake in tests. If the service creates a PostgresClient() internally, replacing that client requires changes inside the service or a patch in the test.

DI is related to inversion of control (IoC), the dependency inversion principle (DIP), and the service locator pattern. Each describes a different design choice:

Concept

What it describes

Who controls the relevant choice

Where it appears in Python

Dependency injection

Code receives its dependencies from outside

The caller or startup code chooses the implementation

Function arguments and constructor parameters

Inversion of control

Surrounding code controls execution or wiring

A framework or external assembler

FastAPI invoking endpoints; pytest invoking tests

Dependency inversion principle

High-level policy and low-level implementation depend on abstractions

A design guideline rather than a runtime mechanism

Contracts expressed through protocols or abstract base classes

Service locator

Code retrieves its dependencies from a registry

Consuming code requests a registered implementation

Registry lookups; preferably kept out of business logic

DI and service location can supply the same object. The difference is where the requirement appears. A registry lookup hides the dependency inside the implementation. A constructor argument is declared in the signature, where readers and tests can see it.

Copy link to headingWhy use dependency injection?

DI lets tests replace dependencies without changing application modules. When code creates its own dependencies, tests often replace them with unittest.mock.patch. A patch must target the name that the consuming code looks up. Changing import notifications to from notifications import send_mail can therefore require a different patch target.

Passing dependencies explicitly has several benefits:

  • Tests pass fakes into functions or constructors without patching import paths.

  • Callers can select a dry-run email sender or another storage backend without changing the code that uses it.

  • Production and test setups construct their dependencies in defined locations.

  • Function signatures show which dependencies the code needs.

  • pytest users already work with a similar pattern. Fixtures supply values to test functions that request them by name.

Centralized configuration does not make databases interchangeable. Moving from Postgres to SQLite may require changes to drivers, SQL, types, or constraints.

Injection also adds wiring. Use it where explicit dependencies improve testing, configuration, or resource management enough to justify that work.

Copy link to headingDependency injection patterns in Python

Python supports DI through arguments, functions, and closures. FastAPI adds dependency resolution for endpoints. Protocols and abstract base classes describe the interfaces that those dependencies must satisfy.

Copy link to headingConstructor injection

Constructor injection passes dependencies through __init__. A ReportService(db, mailer) receives its database client and email sender from the caller.

This fits classes that use the same dependencies across several methods. Production code supplies real clients, while tests supply fake objects with the methods the service calls. The test can check whether the service sent the expected report without opening a database connection or sending an email.

A growing constructor can indicate that a class has too many responsibilities. Check whether its methods belong together before adding more dependencies.

Copy link to headingFunction injection

Function injection passes a dependency as an ordinary argument, sometimes with a production default.

For a function declared as def download(url, log=print):, production code can call download(url). A test calls download(url, log=fake_log) and checks what the fake recorded.

Use this for behavior such as logging, reading the clock, or making an HTTP call. A function can receive several dependencies when the operation needs them.

Default arguments do not manage resource lifecycles. A function can receive a shared pool or a context-manager factory. Its caller or application startup code still needs to initialize and close those resources.

Copy link to headingClosures and functools.partial

Closures and functools.partial let startup code supply dependencies before the rest of the application calls a function.

Given def send_report(report, mailer):, partial(send_report, mailer=smtp_mailer) returns a callable with the mailer supplied. A closure created inside the startup code can do the same with lambda report: send_report(report, smtp_mailer).

Tests can construct these callables with fakes. The code that calls them does not need to select the implementation. A named inner function also gives stack traces a more descriptive name than a lambda.

The two approaches differ in when they resolve values. A closure reads a captured variable when it runs, so rebinding that variable can change subsequent calls. A partial stores the supplied object references. Rebinding the original variable does not replace those references, though mutations to the objects remain visible.

Copy link to headingFastAPI dependencies with Depends()

FastAPI resolves dependencies declared with Depends(). An endpoint can declare settings: Annotated[Settings, Depends(get_settings)]. FastAPI calls get_settings when it resolves that endpoint's dependencies.

The FastAPI docs recommend Annotated and show reusable aliases such as SettingsDep = Annotated[Settings, Depends(get_settings)]. A dependency can be a function, class, or another callable object.

Use Depends() for values an endpoint needs, including the current user, parsed parameters, a database session, or a shared pool. FastAPI caches dependency results within a request by default. Set use_cache=False where a dependency must run again.

Dependencies that use yield can also run cleanup code. By default, cleanup runs after FastAPI sends the response. FastAPI 0.121.0 introduced dependency scopes, including scope="function". This runs cleanup after the endpoint returns and before FastAPI sends the response. Choose it when response handling no longer needs the resource.

Copy link to headingDependency contracts with protocols and abstract base classes

Protocols and abstract base classes (ABCs) describe the interface a dependency must satisfy. Arguments still supply the implementation.

Added in Python 3.8, typing.Protocol supports structural subtyping. A Mailer protocol can declare a send() method without requiring implementations to inherit from it. Each implementation's method signature must satisfy the protocol.

An ABC defines an inheritance-based contract and can include shared behavior. For a dependency with one callable operation, a Callable annotation may be enough.

These contracts help static type checkers catch incompatible implementations and test fakes. Add them when the interface clarifies what the consuming code needs.

Runtime checks are narrower. @runtime_checkable checks whether required attributes exist. It does not check their method signatures or types.

Copy link to headingChoose where dependencies are created and how long they live

Create shared resources during application startup. Give each request or operation ownership of the resources it must release.

Copy link to headingCentralize application wiring

If handlers and services each construct database or API clients, tests may need to replace the setup across several modules. Missing a replacement can cause a test call to reach a real service.

Construct application dependencies in a defined location, such as the entrypoint or lifespan function. Pass them into services and endpoint dependencies from there. This setup location is often called the composition root. Tests can construct their own version with fakes.

On Vercel, the Python runtime recognizes entrypoint filenames, including app.py, index.py, server.py, main.py, wsgi.py, and asgi.py. It also recognizes these filenames in supported locations under src/ or app/. For a custom module, configure tool.vercel.entrypoint in pyproject.toml.

Copy link to headingReplace endpoint dependencies explicitly in tests

FastAPI's app.dependency_overrides maps an original dependency to a replacement. Assigning app.dependency_overrides[get_settings] = get_settings_override makes FastAPI use the replacement wherever it resolves that dependency. This includes endpoints, decorators, and included routers.

Remove overrides in a finally block so failed assertions do not leave replacements behind. With a fresh app per test, app.dependency_overrides.clear() removes that test's overrides.

The testing section below explains how to replace the lifespan startup.

Keep tests and fixtures out of the deployed function bundle. Vercel's Python runtime documentation shows how to configure excludeFiles under functions in vercel.json.

Copy link to headingShare pools and scope sessions to their operations

Creating a pool or shared HTTP client on every request repeats initialization and can increase connection usage.

Open resources that support concurrent use in FastAPI's lifespan. Store them on app.state and expose them through a small dependency. Keep individual database connections, sessions, and transactions scoped to the operation that owns them.

This example creates an asyncpg pool during startup and supplies it to the /orders endpoint through Depends(). An app factory lets tests replace lifespan startup.

import asyncio
import os
from contextlib import asynccontextmanager
from typing import Annotated
import asyncpg
from fastapi import Depends, FastAPI, Request
@asynccontextmanager
async def lifespan(app: FastAPI):
pool = await asyncpg.create_pool(
os.environ["DATABASE_URL"],
min_size=1,
max_size=5,
)
app.state.pool = pool
try:
yield
finally:
try:
await asyncio.wait_for(pool.close(), timeout=0.4)
except TimeoutError:
pool.terminate()
def get_pool(request: Request) -> asyncpg.Pool:
return request.app.state.pool
PoolDep = Annotated[asyncpg.Pool, Depends(get_pool)]
def create_app(*, lifespan_handler=lifespan) -> FastAPI:
app = FastAPI(lifespan=lifespan_handler)
@app.get("/orders")
async def list_orders(pool: PoolDep):
async with pool.acquire() as conn:
rows = await conn.fetch("SELECT id, total FROM orders LIMIT 50")
return [dict(row) for row in rows]
return app
app = create_app()

Each instance initializes 1 connection and allows up to 5. These values are examples. Set your limits according to the database's connection budget and the number of application instances that may run concurrently.

The asyncpg documentation explains that close() waits for acquired connections to return. It recommends a timeout. This example allows a 400 ms graceful-close attempt, then terminates remaining connections if the attempt times out.

The finally block attempts cleanup even if the lifespan context exits with an exception. DATABASE_URL remains in server-side configuration. The endpoint does not include it in its response.

Copy link to headingReplace startup separately when testing

Overriding get_pool changes the pool FastAPI supplies to the endpoint. It does not stop the production lifespan from creating a real pool.

FastAPI's lifespan testing documentation shows that with TestClient(app) runs startup and shutdown. To test without a database, create the app with a replacement lifespan that opens no external resources. Then use app.dependency_overrides[get_pool] to supply a fake pool.

The fake must support the operations the endpoint uses. Here, acquire() returns an asynchronous context manager whose connection has an asynchronous fetch() method.

Remove overrides in a finally block after the test. FastAPI's testing guide covers using TestClient with pytest. Use separate integration tests to check the production lifespan and queries against a test database.

Copy link to headingKeep injection proportional to the problem

Injection adds parameters and setup code. Use it when explicit dependencies, centralized construction, or resource management justify that work.

A short script or thin wrapper may need only ordinary imports. For a localized replacement, pytest's monkeypatch fixture or unittest.mock.patch may be sufficient.

FastAPI resolves Depends() for HTTP and WebSocket endpoints and their dependency graphs. Calling an ordinary function from a command-line script or background job does not resolve its Depends() declarations. Pass dependencies into that code through function or constructor arguments.

Copy link to headingHow dependency injection maps to Vercel

A Python function instance can handle successive requests and, with Fluid compute, several requests concurrently. The instance lifecycle determines how long shared dependencies remain available.

Copy link to headingFunction instances can reuse shared resources

Vercel can reuse the same function instance for successive invocations. Fluid compute supports concurrent invocations within a Python instance and prioritizes existing idle resources before allocating more.

A pool can serve requests on that instance up to its configured capacity. Additional instances create additional pools, so account for scaling when setting the database connection budget.

Vercel also scales functions down when traffic stops. Initialize resources on startup and treat the in-memory state as temporary.

Copy link to headingLifespan supports startup and graceful shutdown

Vercel supports FastAPI lifespan events. Use startup to initialize shared resources and graceful shutdown to attempt cleanup.

The Vercel FastAPI docs allow a maximum of 500 ms for cleanup after SIGTERM. Apply a timeout rather than assuming a pool close or log flush will finish immediately.

The earlier example puts cleanup in finally and limits its graceful-close attempt. Keep work that requires durable completion out of shutdown handlers.

Copy link to headingEnvironment variables supply deployment-specific configuration

Hardcoded connection strings or keys can cause Preview deployments to use production services.

Vercel environment variables support Production, Preview, Development, and custom environment scopes. Python deployments support a combined environment-variable size of 64 KB. Use separate databases for Development, Preview, and Production to keep test and review data out of production.

vercel dev loads Development variables automatically. vercel env pull writes Development values to a local .env file for other local workflows.

A settings dependency can read these values and pass the configuration to the application code. Environment variables configure an implementation. Injection selects and supplies the implementation.

Store passwords, API keys, tokens, and connection strings as Secret variables. Their saved values are write-only. Use Config variables for non-sensitive values that authorized project members need to read.

Copy link to headingStatic delivery changes where checks execute

Vercel's content delivery network (CDN) can serve FastAPI frontends and static files for supported app.frontend() and app.mount() configurations using StaticFiles. Promoted files bypass the application's Vercel Function.

Application middleware and dependency checks do not run for files served by the Vercel CDN. Vercel applies these promotion rules:

  • Frontends guarded by Depends() stay on the function.

  • Middleware on the main app keeps static mounts and frontends on the function.

  • Middleware on a mounted sub-app keeps that sub-app's static mounts on the function.

Setting cdn = true under [tool.vercel.fastapi.static] in pyproject.toml forces promotion even when those guards are present. Use that override only for files that can be public.

Keep protected content on paths where authorization runs. Public images, scripts, and stylesheets can use the Vercel CDN.

Copy link to headingPrepare the app for deployment

Use plain arguments in business logic, Depends() for endpoint dependencies, and lifespan for resources shared within an instance.

Before deploying, declare the application dependencies in a manifest that Vercel can install. For this example, create requirements.txt alongside app.py:

fastapi
asyncpg

Keep httpx and pytest in your development dependencies. Installing packages locally does not declare them for deployment. Vercel's Python runtime guide documents supported manifests, including requirements.txt and pyproject.toml.

Follow the FastAPI deployment guide to deploy your app, or start from a FastAPI starter template.

Copy link to headingFrequently asked questions

Copy link to headingWhat are the types of dependency injection?

Martin Fowler described constructor, setter, and interface injection. Python supports dependency injection through constructors and ordinary function arguments. Closures and functools.partial can supply dependencies before a function is called.

Interface injection uses a dedicated injection interface. A Python Protocol describes a dependency's contract. It does not perform interface injection.

Copy link to headingIs dependency injection the same as inversion of control?

Dependency injection is one technique associated with inversion of control. IoC gives surrounding code control over execution or wiring.

FastAPI invoking an endpoint illustrates inversion of control. FastAPI supplying its dependencies through Depends() illustrates dependency injection.

Copy link to headingDoes Python need a dependency injection library?

Python supports DI through constructors, function arguments, closures, and functools.partial. Start with those language features.

A container can help when a large application has deep dependency graphs or repetitive construction rules. Use one if it reduces setup and lifecycle code while keeping dependencies understandable.

Copy link to headingWhen should FastAPI use lifespan instead of Depends()?

Use lifespan to initialize resources that support concurrent use, such as a database pool or a suitable HTTP client.

Use Depends() to supply endpoint dependencies, including request-specific values and access to shared resources. Keep sessions, transactions, and acquired connections scoped to the operation or request that owns them.

On Vercel, keep graceful-shutdown cleanup within the documented 500 ms allowance. In tests, replacing a dependency does not replace the lifespan startup. Make both replaceable when they would otherwise contact real services.

Ready to deploy?