Key Concepts¶
Dishka is a modern, performance-oriented Dependency Injection (DI) framework for Python emphasizing lifecycle management through scopes, modularity via components, and clean integration with popular frameworks.
Providers: The Source of Truth¶
- Configuration: Dependencies are configured within
Provider
classes. - Factories: Factories are declared using the
@provide
decorator on methods or by applyingprovide()
to a class which inspects its__init__
. - Finalization: Resource cleanup (e.g., closing database connections) is handled elegantly by defining a provider as a generator. The code after the
yield
statement is executed upon scope exit. - Async Support: Asynchronous factories (
async def
) are fully supported viamake_async_container
andawait container.get()
.
Scopes: Managing Lifecycles¶
- Hierarchical Lifespans: Dishka’s core feature is its strict, hierarchical scope system that defines the lifespan of dependencies. The default flow is
APP
→REQUEST
. - Caching: Dependencies are lazy-loaded and cached within their scope. Requesting the same dependency multiple times in the same scope instance returns the same object.
- Scope Dependency Rules: A dependency can only rely on objects from its own scope or a parent scope. For instance, a
REQUEST
-scoped object can depend on anAPP
-scoped one, but the reverse is not allowed. - Entering Scopes: You enter a new, nested scope by using the container as a context manager:
with app_container() as request_container:
.
Container: The Access Point¶
- Creation: The
Container
is the object used to retrieve dependencies and is created bymake_container
ormake_async_container
, into which you pass your providers. - Retrieval: The primary method for accessing a dependency is
container.get(MyType)
. - Concurrency: The top-level (
APP
) container is thread/task-safe by default to prevent race conditions during the creation of singletons.
Components: True Isolation for Modularity¶
- Preventing Collisions: To avoid type collisions in large or modular applications, providers can be grouped into named Components.
- Isolation: By default, providers within one component are isolated and cannot see dependencies from another.
- Inter-Component Communication: To explicitly request a dependency from another component, use the
Annotated
type hint withFromComponent
:my_param: Annotated[int, FromComponent("X")]
. This enables the creation of well-defined boundaries between different parts of an application.
Context Data & Framework Integration¶
- Runtime Data Injection: Data that is only available at runtime (like an incoming HTTP request object) can be injected into the dependency graph. This is achieved by marking a dependency with
from_context
in a provider and then passing the actual value when entering a scope, for examplewith container(context={Request: request_obj}):
. - Seamless Integration: Dishka provides integrations for popular frameworks like FastAPI, aiohttp, and others, which automate scope management and context injection. The typical pattern involves:
- Calling
setup_dishka(container, app)
during application startup. - Using an
@inject
decorator on handlers or views. - Type-hinting dependencies in handlers with
FromDishka[MyService]
, which is a convenient alias for requesting a dependency from the default component.
- Calling
A Practical Guide¶
This guide provides the core syntax and patterns you’ll use to integrate Dishka into a project.
1. The Core Workflow: A Complete Example¶
This is the fundamental pattern for using Dishka in a standalone script.
import os
from collections.abc import Iterable
from dishka import Provider, Scope, make_container, provide
# 1. Your application classes (no changes needed for Dishka)
class ApiClient:
def __init__(self, api_key: str):
print(f"ApiClient created with key: ...{api_key[-4:]}")
self.api_key = api_key
def fetch(self): return "data"
class Database:
def __init__(self): print("Database connection opened.")
def query(self): return "db_result"
def close(self): print("Database connection closed.")
class Service:
def __init__(self, db: Database, client: ApiClient):
self._db = db
self._client = client
def process(self):
return f"{self._db.query()} and {self._client.fetch()}"
# 2. Create a Provider to declare how objects are created
class MyProvider(Provider):
# APP-scoped: created once and shared across the application
scope = Scope.APP
# provide a simple value by returning it
@provide
def get_api_key(self) -> str:
return os.getenv("API_KEY", "fake_key_1234")
# provide a class by instantiating it with its dependencies
# Dishka automatically injects `api_key` from the provider above
api_client = provide(ApiClient)
# REQUEST-scoped: Use a generator for dependencies that need cleanup
@provide(scope=Scope.REQUEST)
def get_db(self) -> Iterable[Database]:
db = Database() # Setup code
yield db # The object is used within the scope
db.close() # Teardown code runs after scope exit
# Dishka will automatically resolve Database and ApiClient dependencies
service = provide(Service, scope=Scope.REQUEST)
# 3. Create and manage the container
# The app_container is for APP-scoped objects
app_container = make_container(MyProvider())
print("--- First Request ---")
# Enter a new scope (REQUEST) using a context manager
with app_container() as request_container:
service_instance = request_container.get(Service)
print(service_instance.process())
# request_container.get(Service) again here would return the SAME instance
# The `with` block has exited, so the Database connection is closed.
print("\n--- Second Request ---")
with app_container() as request_container2:
service_instance_2 = request_container2.get(Service) # A NEW instance
print(service_instance_2.process())
# 4. Close the main container on application shutdown
app_container.close()
2. Key Provider
Syntax¶
Your Provider
is where you define all dependency creation logic.
-
Simple Instantiation: For classes with dependencies that Dishka already knows how to create, just pass the class to
provide
.
python # Automatically resolves ApiClient's `settings` dependency api_client = provide(ApiClient, scope=Scope.APP)
-
Interface Binding: To provide a concrete implementation for a protocol or abstract class, use the
provides
argument.
```python
from typing import Protocolclass UserRepo(Protocol): …
class UserRepoImpl(UserRepo): …When UserRepo is requested, an instance of UserRepoImpl will be provided¶
user_repo = provide(UserRepoImpl, provides=UserRepo, scope=Scope.REQUEST)
``` -
Asynchronous Factories: For async applications, use
async def
andmake_async_container
.
```python
from dishka import make_async_containerclass MyAsyncProvider(Provider):
@provide(scope=Scope.APP)
async def get_async_db(self) -> AsyncDatabase:
return await connect_async(…)Use the async container¶
container = make_async_container(MyAsyncProvider())
db = await container.get(AsyncDatabase)
await container.close()
```
3. Framework Integration (FastAPI Example)¶
Dishka automates scope and context management within frameworks.
- Add the Framework Provider: Include the specific provider for your framework to get access to framework objects like
Request
. - Setup Dishka: Call
setup_dishka
to add the middleware to your app. - Inject into Handlers: Use the
@inject
decorator and theFromDishka
type hint.
from fastapi import FastAPI
from dishka import make_async_container
from dishka.integrations.fastapi import (
setup_dishka,
inject,
FromDishka,
FastapiProvider, # 1. Import framework provider
)
# Assume MyProvider and Service classes from the first example
# Create an async container and include the FastapiProvider
container = make_async_container(MyProvider(), FastapiProvider())
app = FastAPI()
@app.get("/")
@inject # 3. Use the inject decorator
async def read_root(
# Type hint your dependency with FromDishka
service: FromDishka[Service],
) -> dict:
result = service.process()
return {"result": result}
# 2. Setup dishka on the app instance
setup_dishka(container, app)
# FastAPI's lifespan can be used to close the container
@app.on_event("shutdown")
async def shutdown():
await container.close()
When a request hits /
, Dishka’s middleware automatically:
1. Enters the REQUEST
scope.
2. Creates a new Service
and Database
instance.
3. Provides the Service
instance to the read_root
handler.
4. Exits the REQUEST
scope after the response is sent, closing the Database
.
4. Components for Isolation¶
When multiple parts of your app provide the same type (e.g., two different Database
connections), use components to avoid conflicts.
-
Assigning a Component: Add a
component
attribute to a provider.```python
class UsersProvider(Provider):
component = “users” # Assign this provider to the “users” component
scope = Scope.APP
db = provide(UsersDatabase, provides=Database)class OrdersProvider(Provider):
component = “orders” # Assign this provider to the “orders” component
scope = Scope.APP
db = provide(OrdersDatabase, provides=Database)
`` * **Requesting from a Component**: When injecting, use
Annotatedwith
FromComponent` to specify the source.```python
from typing import Annotated
from dishka import FromComponent@inject
async def get_users(
# Request the Database provided by the “users” component
db: Annotated[Database, FromComponent(“users”)],
):
…
`` The
FromDishka[T]hint is a shortcut for
Annotated[T, FromComponent()]`, which targets the default (unnamed) component.
Page last modified: 2025-10-04 11:51:12