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; CONTEXT.md
opens with what it does and owns the vocabulary. Read it before naming a concept in code, a test
name, or an issue title. Every framework integration (ls docs/integrations/) lives in a separate
repository and ships as a separate PyPI package, modern-di-pytest included.
just (task runner) and uv (package manager). The justfile is the source of truth:
just --list, or read it. Every recipe carries its intent as a comment. The one thing it does not
say: just docs-build runs mkdocs --strict over the site only. Local links in every Markdown file,
root and docs/agents/ included, are checked by the links job in CI (lychee, offline), which has
no just recipe.
Run just install before just lint, in every checkout and worktree. uv.lock is gitignored and
CI's just install runs uv lock --upgrade before it syncs, so CI always lints with the newest
ruff and ty; a plain uv sync keeps whatever the checkout resolved last time, and a stale ty
passes code that CI rejects.
- Scope:
IntEnum,APP=1 → SESSION=2 → REQUEST=3 → ACTION=4 → STEP=5. A provider resolves only from a container of the same or deeper (higher-int) scope; otherwise a clear error is raised. - Container: the central object. A child (
build_child_container) shares the parent's providers/overrides registries; cache and context are per-container.container.validate()(cycle + transitive-scope checks) is the only thing that validates.
Every module under modern_di/ is named for what it does; read it. What a single-file read will
not tell you:
resolver_compiler.pyis the single resolve path. AFactoryresolver is generated from a source template per resolver shape andexec'd with the factory's constants as globals; anAliascompiles to its source's resolver, and the other provider types compile to closures. A new provider type must add a branch here orcompile_resolverraises, and branches independency_graph(redirect_target,dependencies_of,collect_errors), which fall through silently. Nothing in the template calls a helper on the hot path: the per-node frame budget is the point, andtest_resolve_costs_exactly_one_resolver_frame_per_nodesays why. Overrides are compiled in: an override change drops the compiled resolvers.exceptions/owns every message and every glyph. A raise site passes structured facts, never formatting; the class renders its own f-string and sets adocs_slug(its page underdocs/troubleshooting/, enforced bytests/test_docs_slug_census.py). Add a message, a glyph, or a class here, never at the raise site. Submodules split by family;__init__re-exports every public name.registries/:providers_registry(type → provider, plus the shared plan/resolver memos) andoverrides_registryare shared tree-wide.cache.py:CacheItemand the functions over a container's own cache items and creation order (fetch, count, close). Cache and context are per-container and live in the container's slots; the context is a plain dict.dependency_graph.pywalksWiringPlan.provider_kwargs, so whatvalidate()traverses is exactly whatresolve()follows. Explicit-stack, never recursive: a caller runs it inside aRecursionErrorhandler near CPython's stack limit.types.py:UNSETis load-bearing on the resolve path: the miss marker for both the override lookup and the cache slot, separating "not passed" from "explicitlyNone".
A test declares a Group subclass with providers as class attributes → Container(groups=[...]), then
container.resolve(SomeType) or resolve_provider(provider), with override/reset_override for
mocks. Scope chains come from build_child_container.
Every link in README.md must be absolute: https://github.com/modern-python/<repo>/blob/main/<path>,
or .../tree/main/<path> for a directory. Never a relative path: README.md is also the PyPI long
description, and PyPI does not rewrite relative links, so a relative one 404s on the package page.
- Design principle: conservative feature set; resolution is sync-only (async resolution was removed
in 2.x), though finalizers may still be sync or async (
close_sync/close_async); no global state - Docstrings: public API documents the contract; internal helpers get a one-line contract, plus at most one or two lines for a genuinely non-obvious constraint. Never narrate implementation or justify code to a reviewer; cross-file rationale lives in an invariant test's docstring
ruff(select = ["ALL"]) andtyare configured inpyproject.tomland run byjust lint
GitHub issues on modern-python/modern-di, via gh. See docs/agents/issue-tracker.md.
The five canonical roles, each label string equal to its name. See docs/agents/triage-labels.md.
Single-context: CONTEXT.md and docs/adr/ at the repo root. See docs/agents/domain.md.
Docs pages, PR and issue bodies, and commit messages run through the
humanizer skill when it is installed
(npx skills add blader/humanizer). Repo conventions it does not cover (heading and nav
casing, colons in place of dashes in gloss bullets, contrast markers in code samples) are in
docs/agents/docs-style.md.
Maintainers only. See docs/agents/release.md.