Skip to main content

CI codecov Python >=3.10 PyPI version License: MIT

Downloads Docs Dependencies PEP 561 async ready

Maintainability CodeFactor Scrutinizer Code Quality Reliability Rating Maintainability Rating

Quality gate status Bugs Code Smells Duplicated Lines (%) Technical Debt

mypy: strict linting: ruff Ruff uv Hatch

Hits-of-Code LoC GitHub issues GitHub pull requests

Minimal dependency injection container for Python. Provides immutable rule definitions, singleton/transient lifetimes, scoped caching, nested attribute resolution, cycle detection, and optional validation, logging, and ordering layers. Includes auto-wiring, function injection, yield-provider finalization, qualifiers, graph validation, dependency visualization, framework integrations, parallel async resolution, and modern typing support.

How to use it

Installation

pip install doppy-di

Requires Python 3.10 or later.

Basic usage

from doppy_di.container import ContainerBuilder

builder = ContainerBuilder()

# Register a singleton service
builder.service("answer", lambda: 42, lifetime="singleton")

# Register a transient service with dependencies
builder.service("greeting", lambda name: f"Hello, {name}!", deps=["name"])
builder.value("name", "World")

container = builder.build()

print(container.get("answer"))    # 42
print(container.get("greeting"))  # Hello, World!

Scoped caching

with container.scope("request") as scope:
    a = scope.get("greeting")
    b = scope.get("greeting")
    assert a is b  # cached within scope
# scope cache is cleared on exit

Override for testing

with container.override("answer", 99):
    print(container.get("answer"))  # 99
print(container.get("answer"))      # restored to 42

Override on an unregistered key raises UnregisteredTypeError — prevents silent no-op overrides.

Use cases

Service registration with dependency injection

Register factories with explicit lifetime and dependency list. Container resolves the dependency graph on first access.

builder.service("db", lambda: Database("sqlite:///app.db"), lifetime="singleton")
builder.service("repo", lambda db: Repository(db), deps=["db"])
container = builder.build()
repo = container.get("repo")

Value objects and constants

Inject pre-computed values or configuration objects.

builder.value("config", {"debug": True, "port": 8080})
container.get("config")  # {"debug": True, "port": 8080}

Aliasing

Create an alias that delegates resolution to another key.

builder.service("real_service", lambda: Service(), lifetime="singleton")
builder.alias("service", "real_service")
assert container.get("service") is container.get("real_service")

Nested attribute resolution

Access nested attributes of resolved services using tuple keys.

builder.service("db", lambda: Database("prod"), lifetime="singleton")
# resolve db.connection directly
container.get(("db", "connection"))  # returns db.connection

Scoped request context

Use named scopes for per-request caching without polluting the global singleton cache.

def handle_request(request_id: str) -> dict:
    with container.scope(request_id) as scope:
        user = scope.get("current_user")
        data = scope.get("request_data")
        return process(user, data)

Validation at build time

Enable build-time validation to catch missing dependencies early.

builder.service("a", lambda b: A(b), deps=["b"])
try:
    container = builder.build(validate=True)
except ContainerBuildError as e:
    print(e.missing)  # [("a", "b")]

Duplicate key policy

Control behaviour on duplicate registration.

from doppy_di.container import DuplicateKeyPolicy

strict = ContainerBuilder(duplicate_policy=DuplicateKeyPolicy.FAIL)
strict.service("x", lambda: 1)
strict.service("x", lambda: 2)  # raises DuplicateKeyError

warning = ContainerBuilder(duplicate_policy=DuplicateKeyPolicy.WARN)
warning.service("x", lambda: 1)
warning.service("x", lambda: 2)  # logs warning, overwrites

Optional runtime layers

The devkit package provides optional extensions:

from doppy_di.devkit import LoggingContainer, ValidatingContainer

container = LoggingContainer(container)           # log all get operations
container = ValidatingContainer(container)         # validate before resolving
from doppy_di.devkit.nested import NestedRules, SameValuePolicy

nested = NestedRules()
nested.add_rule("parent", "child", SameValuePolicy())
from doppy_di.devkit import ChildrenFirstPolicy, ParentFirstPolicy
from doppy_di.devkit.policy import OrderPolicy

# control the order of nested field resolution
policy = ChildrenFirstPolicy()

Auto-wiring

Mark classes with @injectable for automatic registration. Container.scan() discovers all injectable classes in a package; lazy registration on get() works without scan().

from doppy_di import injectable
from doppy_di.container import ContainerBuilder

@injectable(scope="singleton")
class Database:
    pass

@injectable
class Service:
    def __init__(self, repo: Database) -> None:
        self.repo = repo

builder = ContainerBuilder()
container = builder.build()
container.scan(__name__)          # batch discovery
svc = container.get(Service)      # or lazy: no scan() needed

Function injection

Use @inject and Depends() to inject dependencies into plain functions and methods. Supports sync and async.

from doppy_di import inject, Depends

@inject(container=container)
def handle_event(event: Event, service: UserService = Depends()):
    return service.process(event)

Yield providers

Register generator factories for resources that need cleanup. The scope calls close() on exit.

def make_session():
    try:
        yield Database()
    finally:
        cleanup()

builder.service("session", make_session, lifetime="transient")
with container.scope("req") as scope:
    session = scope.get("session")   # acquires
# session finalized on scope exit

Async generators are supported via async with container.ascope().

Qualifiers

Register multiple rules for the same type using a qualifier string.

builder.service(Database, qualifier="read", factory=lambda: Database("read"))
builder.service(Database, qualifier="write", factory=lambda: Database("write"))

read_db = container.get(Database, qualifier="read")

Graph validation

Call container.validate() to check the entire dependency graph at once, without resolving.

errors = container.validate(strict=False)   # collect all errors
container.validate(strict=True)             # raise on first error

Graph visualization

Render the dependency graph as Mermaid, Graphviz, or JSON.

print(container.visualize("mermaid"))   # graph TD  Service --> Database
print(container.visualize("graphviz"))  # digraph G { Service -> Database; }
data = container.visualize("json")      # {"Service": {"deps": ["Database"]}}

Parallel async resolution

Resolve independent dependencies concurrently with get_many().

a, b = await container.get_many(["a", "b"], parallel=True)

Async containers also support aget() and ascope().

Async-first resolution

aget() resolves sync and async factories, sync and async resources, and resolves independent dependency branches concurrently. Sync factories are called directly with no await overhead.

async def make_db():
    return Database("async")

builder.service("db", make_db)
container = builder.build()

db = await container.aget("db")

Async yield providers are finalized on cancellation:

async def make_session():
    try:
        yield Database()
    finally:
        await cleanup()

builder.service("session", make_session)
container = builder.build()

async with container.ascope("req") as scope:
    session = await scope.aget("session")
# session finalized on scope exit

Mixed-graph rules: a sync factory depending on an async dependency raises AsyncDependencyInSyncContextError when resolved via get(). A sync factory returning an awaitable raises SyncFactoryReturningAwaitableError. Cancelled aget() finalizes partially-created resources and raises ResolutionCancelledError.

Provider facade

Declarative providers convert to rules on assignment. Import from doppy_di.providers; the package-level Factory protocol is untouched.

from doppy_di import Container, Scope
from doppy_di.providers import Factory, Singleton, Value, Resource

services = Container()
services.config = Value({"debug": True})
services.db = Resource(create_db, Scope.APP)
services.repo = Factory(UserRepository, db=services.db)
services.service = Singleton(UserService, repo=services.repo)

Providers: Factory, Singleton, Scoped, Value, Resource, Coroutine, Alias, Selector, ListOf, DictOf. Assignment is attribute-style; dependencies may reference other providers before they are assigned.

Config profiles and child containers

Derive environment-specific containers without mutating the base.

builder = ContainerBuilder()
builder.value("env", "base")
container = builder.build()

prod = container.with_profile("prod", {"env": "prod"})
assert container.get("env") == "base"
assert prod.get("env") == "prod"

child() layers rules over the parent; parent rules added later stay visible. diff(other) returns a DiffReport of added/removed/changed keys. export_config() serializes the effective configuration to JSON.

Compile / plan mode

Compile the graph once into an immutable ExecutionPlan.

plan = container.compile()
assert plan.get("b") == 2

compile() validates the full graph up front. The plan is immutable and resolves through the live container, so lifetimes, caches and scopes keep identical semantics. ExecutionPlan.serialize() / deserialize() persist the plan to JSON. Fully opt-in: if compile() is never called there is zero overhead.

Observability and tracing

Set a tracer callback to observe every resolution.

events = []

def tracer(key, duration, cache_hit, scope):
    events.append((key, duration, cache_hit, scope))

container.set_tracer(tracer)
container.get("a")
container.get("a")  # cache hit

Pass set_tracer(None) to disable. When no tracer is set there is no timing and no dispatch — zero overhead. Child containers inherit the parent tracer.

OpenTelemetry integration via the optional extra:

pip install "doppy-di[otel]"
from doppy_di.ext.otel import otel_adapter

container.set_tracer(otel_adapter())
container.get("a")  # emits doppy.resolve:'a' span

Pluggable resolution policies

Control the order of dependency resolution. Policies are opt-in; the default behaviour is unchanged when none is specified.

from doppy_di import (
    ResolutionChildrenFirstPolicy,
    EagerPolicy,
    ParallelPolicy,
)

# container-wide policy
container = builder.build(policy=ResolutionChildrenFirstPolicy())

# per-call policy
container.get("a", policy=EagerPolicy())

Built-in policies: DefaultResolutionPolicy, LazyPolicy, ResolutionParentFirstPolicy, ResolutionChildrenFirstPolicy, EagerPolicy, ParallelPolicy. Implement the ResolutionPolicy protocol (order(graph, root)) for custom strategies.

Note: resolution policies are exported under the aliases ResolutionChildrenFirstPolicy / ResolutionParentFirstPolicy. The top-level ChildrenFirstPolicy / ParentFirstPolicy names belong to the devkit nested-field ordering (see "Optional runtime layers" above).

Graph introspection and CLI

Query the dependency graph programmatically.

g = container.graph()
g.nodes()               # all registered keys
g.edges()               # (key, dependency) pairs
g.dependencies_of("a")  # direct deps
g.dependents_of("a")    # direct dependents
g.to_mermaid()          # mermaid
g.to_dot()              # graphviz
g.to_json()             # dict
g.to_text()             # text tree

Inspect or lint container definitions from a file:

doppy-di graph container.py --format mermaid
doppy-di explain db --file container.py
doppy-di check container.py --root service --strict

check reports missing dependencies, cycles, duplicate registrations, unused registrations (with --root), and lifetime violations.

Framework integrations

Optional first-party integrations for FastAPI, aiogram, and Typer live in doppy_di.ext.*.

from doppy_di.ext.fastapi import setup_doppy
setup_doppy(app, container)                 # per-request scope

from doppy_di.ext.aiogram import setup_doppy
setup_doppy(bot, container)                 # per-update scope

from doppy_di.ext.typer import setup_doppy
setup_doppy(app, container)                 # inject into commands

Modern typing support

The public API supports TypeAlias, TypedDict, ParamSpec, TypeGuard, and Self for improved static checking with mypy strict. No runtime overhead.

Additional information can be found in Documentation.

How to contribute

  1. Fork the repository.
  2. Create a feature branch (git checkout -b feat/my-feature).
  3. Install development dependencies: uv sync --extra dev.
  4. Make changes. Format and lint with: uv run ruff format . && uv run ruff check --fix .
  5. Type-check: uv run mypy.
  6. Run tests: uv run pytest.
  7. Commit messages must follow Conventional Commits (enforced via commitlint).
  8. Open a pull request against main.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

doppy_di-2.20.0.tar.gz (425.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

doppy_di-2.20.0-py3-none-any.whl (54.9 kB view details)

Uploaded Python 3

File details

Details for the file doppy_di-2.20.0.tar.gz.

File metadata

  • Download URL: doppy_di-2.20.0.tar.gz
  • Upload date:
  • Size: 425.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for doppy_di-2.20.0.tar.gz
Algorithm Hash digest
SHA256 4bc63c5d3068ca5989ff130a7917e91976e2834d1ec54b71c4bf9a983150116d
MD5 efcf6074616ffb86531dc8911083795d
BLAKE2b-256 ddf32082946ad84d30dbc868da76846545542a63f8d4ffa917fce5f2ac85f76a

See more details on using hashes here.

File details

Details for the file doppy_di-2.20.0-py3-none-any.whl.

File metadata

  • Download URL: doppy_di-2.20.0-py3-none-any.whl
  • Upload date:
  • Size: 54.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for doppy_di-2.20.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ef3e3126682459839ebc6377d01ed76459be42fc2e69d5d7371a1c0808fe3290
MD5 db633c1aafbd4d5de8aacddb756b8276
BLAKE2b-256 3ab31bfa5b8b5a3fd2d4fc6e7aa788ef09dd2d453dcb15a0f9578a5d060943ca

See more details on using hashes here.

Release history Release notifications | RSS feed

2.36.0

2 files

2.35.0

2 files

2.34.0

2 files

2.33.0

2 files

2.31.0

2 files

2.30.0

2 files

2.29.0

2 files

2.28.0

2 files

2.27.0

2 files

2.26.1

2 files

2.26.0

2 files

2.25.0

2 files

2.24.0

2 files

2.23.0

2 files

2.22.0

2 files

2.21.0

2 files

This release

2.20.0 This release

2 files

2.19.0

2 files

2.18.0

2 files

2.17.0

2 files

2.16.0

2 files

2.15.0

2 files

2.14.0

2 files

2.13.0

2 files

2.12.0

2 files

2.11.0

2 files

2.10.0

2 files

2.9.0

2 files

2.8.0

2 files

2.7.0

2 files

2.6.0

2 files

2.5.0

2 files

2.4.0

2 files

2.3.0

2 files

2.2.0

2 files

2.1.0

2 files

2.0.0

2 files

1.5.2

2 files

1.5.1

2 files

1.5.0

2 files

1.4.1

2 files

1.4.0

2 files

1.3.0

2 files

1.2.1

2 files

1.2.0

2 files

1.1.1

2 files

1.1.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page