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().
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
- Fork the repository.
- Create a feature branch (
git checkout -b feat/my-feature). - Install development dependencies:
uv sync --extra dev. - Make changes. Format and lint with:
uv run ruff format . && uv run ruff check --fix . - Type-check:
uv run mypy. - Run tests:
uv run pytest. - Commit messages must follow Conventional Commits (enforced via commitlint).
- 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file doppy_di-2.17.0.tar.gz.
File metadata
- Download URL: doppy_di-2.17.0.tar.gz
- Upload date:
- Size: 405.6 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e169413310d7f8e2c08b765bdccc6102fc6d786657fe827764ee8204f827006a
|
|
| MD5 |
c8ce4c9587a9b3088f0cc3c1a96558de
|
|
| BLAKE2b-256 |
2231abf1dff4c3b45f0163ad3e2ac4aa350d05e9696d7135ffd4e6a5808e4d28
|
File details
Details for the file doppy_di-2.17.0-py3-none-any.whl.
File metadata
- Download URL: doppy_di-2.17.0-py3-none-any.whl
- Upload date:
- Size: 46.2 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4c0f80ebcb30075ecbc1dd2ec8b19351dd5d1bee9148bb8b1038346a5478847
|
|
| MD5 |
a81fe489f069cd594b820ec2da73b5e8
|
|
| BLAKE2b-256 |
8c8ad80a365526fde38541b77bcbef27464a43fbc35b46ec269c37e5799ebcae
|