that-depends

star 250

Guide for using the `that-depends` library, a Python dependency injection framework.

modern-python By modern-python schedule Updated 6/9/2026

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:

  1. Define a BaseContainer.
  2. Register providers as class attributes.
  3. Consume dependencies with @inject and Provide[...].
  4. Use context decorators for ContextResource providers.
  5. 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

  • Generator injection is supported, but generator injection cannot initialize ContextResource contexts for you. Pre-initialize the context first if needed.
  • Selector, Sequence, and Mapping help compose providers instead of manually wiring branches and aggregates in application code.
  • State is 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:

  1. Put providers on a BaseContainer.
  2. Use @inject plus Provide[Container.provider].
  3. Avoid resolve() / resolve_sync() in application code.
  4. Use provider.context() or Container.context() for context-managed dependencies.
  5. Reach for container_context() only when multiple providers/containers, global context, or explicit scope control are required.
  6. Use override APIs in tests and tear_down() in cleanup paths.

Detailed documentation

For full package documentation, read the official documentation.

Install via CLI
npx skills add https://github.com/modern-python/that-depends --skill that-depends
Repository Details
star Stars 250
call_split Forks 15
navigation Branch main
article Path SKILL.md
More from Creator
modern-python
modern-python Explore all skills →