name: that-depends
description: Guide for using the that-depends library, a Python dependency injection framework.
that-depends
that-depends is a typed dependency-injection framework for Python. The core workflow is:
- Define a
BaseContainer. - Register providers as class attributes.
- Consume dependencies with
@injectandProvide[...]. - Use context decorators for
ContextResourceproviders. - Override providers in tests instead of patching call sites.
Recommended usage
Define providers on a container
Keep providers on BaseContainer subclasses instead of as standalone globals, especially if you need context features.
from that_depends import BaseContainer, providers
class Settings:
def __init__(self) -> None:
self.base_url = "https://api.example.com"
class ApiClient:
def __init__(self, base_url: str) -> None:
self.base_url = base_url
class UserService:
def __init__(self, client: ApiClient) -> None:
self.client = client
class Container(BaseContainer):
settings = providers.Singleton(Settings)
api_client = providers.Factory(ApiClient, base_url=settings.cast.base_url)
user_service = providers.Factory(UserService, client=api_client.cast)
Prefer the decorator API for application code
Use @inject with Provide[Container.provider] defaults.
from that_depends import Provide, inject
@inject
def handle_user(service: UserService = Provide[Container.user_service]) -> UserService:
return service
This is the preferred style over explicit resolve() / resolve_sync() calls in application code.
Provider cheat sheet
| Provider | Use for |
|---|---|
Singleton / AsyncSingleton |
One cached instance |
Factory / AsyncFactory |
New value on each resolution |
Resource |
Cached value with teardown |
ContextResource |
Per-context / per-scope resource |
Sequence / Mapping |
Aggregate multiple providers into read-only collection types |
Selector |
Choose one provider from a key |
State |
Pass runtime state through context |
Best practices
Prefer injection over explicit resolution
Avoid calling resolve() and resolve_sync() inside normal application code. Inject dependencies into function parameters instead.
Reserve explicit resolution for bootstrapping, one-off scripts, REPL usage, or tests.
Avoid:
def create_handler() -> UserService:
return Container.user_service.resolve_sync()
Prefer:
@inject
def create_handler(service: UserService = Provide[Container.user_service]) -> UserService:
return service
Why this is better:
- it keeps call sites easy to override in tests;
- it works naturally with scoped/context-managed providers;
- callers can still pass explicit arguments without overriding providers.
Do not call resolve() / resolve_sync() in injected function bodies
This is especially important for ContextResource providers. The injection system initializes context for dependencies declared in Provide[...] defaults; explicit resolution inside the function body is discouraged and can fail for scoped resources.
Avoid:
import typing
from that_depends.providers.context_resources import ContextScopes
def open_session() -> typing.Iterator[str]:
yield "session"
class Container(BaseContainer):
default_scope = ContextScopes.INJECT
session = providers.ContextResource(open_session).with_config(scope=ContextScopes.INJECT)
@inject(scope=ContextScopes.INJECT)
def bad() -> str:
return Container.session.resolve_sync()
Prefer:
@inject(scope=ContextScopes.INJECT)
def good(session: str = Provide[Container.session]) -> str:
return session
Prefer provider.context() over container_context()
If you only need to initialize context for one provider, prefer the provider decorator/context API. It is more local and easier to read.
Preferred for a single provider:
import typing
from that_depends import Provide, inject, providers, BaseContainer
def request_id_resource() -> typing.Iterator[str]:
yield "req-123"
class Container(BaseContainer):
request_id = providers.ContextResource(request_id_resource)
@Container.request_id.context
@inject
def endpoint(request_id: str = Provide[Container.request_id]) -> str:
return request_id
Use container_context() when you need one of these:
- initialize context for multiple providers or containers at once;
- pass
global_context; - manually control a named scope.
from that_depends import container_context
from that_depends.providers.context_resources import ContextScopes
async with container_context(
Container.request_id,
global_context={"trace_id": "abc-123"},
scope=ContextScopes.REQUEST,
):
...
If you need all ContextResource providers in one container, @Container.context is often cleaner than container_context(Container).
@Container.context
@inject
async def run_endpoint(request_id: str = Provide[Container.request_id]) -> str:
return request_id
Prefer direct provider references
Use Provide[Container.provider] directly in examples and normal application code:
@inject
def fn(service: UserService = Provide[Container.user_service]) -> UserService:
return service
Use overrides in tests
Prefer provider or container override APIs over patching internals.
def test_handler_override() -> None:
fake_service = UserService(ApiClient("https://test.example.com"))
with Container.user_service.override_context_sync(fake_service):
assert create_handler() is fake_service
If you override an upstream cached dependency and need dependents to refresh, use tear_down_children=True.
Container.settings.override_sync(Settings(), tear_down_children=True)
Tear down cached providers in tests and shutdown hooks
Singleton and Resource values are cached. Tear them down between tests or at application shutdown.
import pytest_asyncio
from typing import AsyncIterator
@pytest_asyncio.fixture(autouse=True)
async def di_teardown() -> AsyncIterator[None]:
try:
yield
finally:
await Container.tear_down()
Notes on advanced features
Generatorinjection is supported, but generator injection cannot initializeContextResourcecontexts for you. Pre-initialize the context first if needed.Selector,Sequence, andMappinghelp compose providers instead of manually wiring branches and aggregates in application code.Stateis useful for runtime values that should flow through provider resolution.- For framework integrations, see FastAPI, FastStream, and Litestar support in the docs.
Practical defaults
When writing or reviewing code that uses that-depends, prefer these defaults:
- Put providers on a
BaseContainer. - Use
@injectplusProvide[Container.provider]. - Avoid
resolve()/resolve_sync()in application code. - Use
provider.context()orContainer.context()for context-managed dependencies. - Reach for
container_context()only when multiple providers/containers, global context, or explicit scope control are required. - Use override APIs in tests and
tear_down()in cleanup paths.
Detailed documentation
For full package documentation, read the official documentation.