This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
modern-di is a zero-dependency Python dependency injection framework that wires up object graphs from type annotations, manages lifetimes via hierarchical scopes, and supports both sync and async finalizers. Framework integrations (FastAPI, FastStream, LiteStar) live in separate repositories and are published as separate PyPI packages.
This project uses just (task runner) and uv (package manager).
just install # uv lock --upgrade && uv sync --all-extras --frozen --group lint
just lint # eof-fixer + ruff format + ruff check --fix + ty check
just lint-ci # same checks without auto-fixing (used in CI)
just test # uv run pytest (with coverage by default)
just test-branch # pytest with branch coveragejust test passes extra args to pytest:
just test tests/providers/test_factory.py
just test tests/providers/test_factory.py -k test_nameWithout just:
uv run ruff format . && uv run ruff check . --fix && uv run ty check
uv run pytestScope is an IntEnum with five levels: APP=1 → SESSION=2 → REQUEST=3 → ACTION=4 → STEP=5. Providers are bound to a scope; a provider can only be resolved from a container of the same or deeper (higher int) scope. Trying to resolve a REQUEST-scoped provider from an APP container raises a clear error.
Container is the central object. A root container is created with Container(scope=Scope.APP, groups=[MyGroup]). Child containers are created via container.build_child_container(scope=Scope.REQUEST, context={...}). Child containers share the parent's providers_registry and overrides_registry but have their own cache_registry and context_registry.
Group is a namespace class (cannot be instantiated) used to declare providers as class-level attributes:
class MyGroup(Group):
my_service = providers.Factory(scope=Scope.APP, creator=MyService)Factory parses the creator's __init__ type hints at declaration time via types_parser.parse_creator(). During resolution it looks up each parameter type in providers_registry and recursively resolves dependencies. There is no separate Singleton class — singleton behavior is Factory(cache_settings=CacheSettings()). Pass kwargs={} to supply static arguments that bypass type-based resolution. Pass skip_creator_parsing=True for callables whose signatures cannot be introspected.
ContextProvider is for runtime values injected at container creation time (e.g. a request object). container_provider is an auto-registered singleton that resolves to the Container itself.
container.resolve(SomeType)→ looks up type inproviders_registry→ callsresolve_provider(provider)resolve_providerchecksoverrides_registryfirst (returns override immediately if found)- Finds the container at the correct scope via
find_container(scope)walking the parent chain - Checks
cache_registry; if cached, returns immediately - Compiles kwargs: for each parsed parameter, finds a matching provider by type and resolves it recursively
- Calls the creator, stores result in cache if
cache_settingsconfigured
| Registry | Shared? | Purpose |
|---|---|---|
ProvidersRegistry |
Shared across all containers | type → provider mapping |
CacheRegistry |
Per-container | provider_id → cached instance |
ContextRegistry |
Per-container | type → runtime context object |
OverridesRegistry |
Shared across all containers | provider_id → override object (for testing) |
modern_di/container.py— Container class, the main entry pointmodern_di/providers/factory.py— Factory and CacheSettings (singleton pattern via caching + optional finalizer)modern_di/providers/context_provider.py— ContextProvider for runtime-injected valuesmodern_di/providers/container_provider.py— auto-registered provider that resolves to the Container itselfmodern_di/types_parser.py— Signature introspection engine (parses type hints for DI wiring)modern_di/scope.py— Scope enummodern_di/group.py— Group base class for provider namespacesmodern_di/errors.py— Error message templates
- Create a
Groupsubclass with providers as class attributes, pass toContainer(groups=[...]) - Use
container.resolve_provider(provider)(by reference) orcontainer.resolve(SomeType)(by type) - For overrides:
container.override(provider, mock_obj)/container.reset_override(provider) - For scope chain tests:
app_container.build_child_container(scope=Scope.REQUEST) asyncio_mode = "auto"in pytest config — async test functions work without extra markers
- Line length: 120 characters
ruffwithselect = ["ALL"]and minimal ignores;tyfor type checking- Coverage excludes
TYPE_CHECKINGblocks - Design principle: conservative feature set; resolution is sync-only (async was removed in 2.x); no global state