depin
Type-first dependency injection for Python 3.12+.
Documentation: https://andrelopes-code.github.io/depin/ · PyPI: pydepin
- Resolution driven by type hints;
ProtocolandAnnotatedare first-class. - Build-time validation:
Container.freeze()catches missing providers, cycles, lifetime violations, and async/sync mismatches before anything runs. - Full async/sync coverage: classes, sync/async factories, generators, async
generators,
@(a)contextmanager, instance context managers. - Safe to share across threads and tasks: a singleton is built exactly once under
contention, and scopes are isolated per
contextvars.Context. - Every failure is a
DepinError. No strayTypeErrorfrom the middle of the library. - A public integration contract:
Host,hosted_container, and a version constant. Every integration depin ships is written on it, and so is any integration you write yourself — nodepin._coreimport required. Writing an integration. - Optional web integrations for FastAPI, Starlette, Litestar and Flask, built on
the framework-free
depin.ext.asgianddepin.ext.wsgimiddlewares that any other ASGI or WSGI framework can install directly. The core has zero runtime dependencies. - Optional command and message integrations for Click, Typer and Taskiq: one
scope per CLI invocation, built on the framework-free
depin.ext.cliseam any other command framework can drive, and one scope per Taskiq message seeded with theTaskiqMessagebeing executed. - No
# type: ignoreat call sites:resolve(),frozen[key],injected(), andInject[T]are all precisely typed underbasedpyright --strictandmypy --strict.
Install
uv add pydepin # core
uv add 'pydepin[fastapi]' # with the FastAPI integration
uv add 'pydepin[starlette]' # with the Starlette integration
uv add 'pydepin[litestar]' # with the Litestar integration
uv add 'pydepin[flask]' # with the Flask integration
uv add 'pydepin[click]' # with the Click integration
uv add 'pydepin[typer]' # with the Typer integration
uv add 'pydepin[taskiq]' # with the Taskiq integration
uv add 'pydepin[pytest]' # with the tested pytest floor enforced
depin.ext.asgi and depin.ext.wsgi are the framework-free middlewares the
four web extras specialise, and depin.ext.cli is the framework-free command
seam Click and Typer specialise; all three import no third-party package and
need no extra.
The depin.ext.pytest fixtures are registered on the pytest11 entry point
by the distribution, so plain pydepin already provides them; the pytest
extra only states the pytest version the plugin is tested against.
Requires Python 3.12+. The distribution is pydepin; the import package is
depin.
Quickstart
from typing import Annotated
from depin import Container, Token
db_url = Token[str]('db.url')
class Database:
def __init__(self, url: str) -> None:
self.url = url
def open_db(url: Annotated[str, db_url]) -> Database:
return Database(url)
class UserRepo:
def __init__(self, db: Database) -> None:
self.db = db
di = Container().value(db_url, 'postgres://...').bind(open_db, provides=Database).bind(UserRepo).freeze()
repo = di[UserRepo]
Scope.SINGLETON is the default, so most bindings need nothing but bind.
The three stages
| Stage | Object | What it does |
|---|---|---|
| Declare | Container |
Mutable builder. Collects bindings; validates nothing. |
| Validate | Container.freeze() |
Runs every static check, then returns the runtime. |
| Resolve | FrozenContainer |
Immutable. Builds and caches values, opens scopes, injects. |
Lifetimes
| Scope | Built | Cached on | Torn down by |
|---|---|---|---|
Scope.SINGLETON |
Once, on first resolution | The container | close() / aclose() |
Scope.SCOPED |
Once per active scope | The scope frame | Exit of scope() / ascope() |
Scope.TRANSIENT |
Every resolution | Nothing | Nothing |
A provider that owns a resource is written as a generator — everything after the
yield is its teardown:
def checkout(pool: Pool) -> Generator[Connection]:
conn = pool.acquire()
yield conn
pool.release(conn)
di = Container().bind(checkout, scope=Scope.SCOPED).freeze()
with di.scope():
conn = di[Connection] # built here, released when the block ends
Read Lifetimes and scopes for nesting, captive dependencies, and shutdown.
Cookbook
Runnable code lives in examples/; each one is executed by the test
suite.
-
Tokens for values:
Token[str]('db.url'), resolved viadi[token]. -
Registries for composition:
Container(infra, services).freeze(). -
Protocols:
@provides(Store)on the implementation, thendi.resolve(Store). -
Aliases for a second name on one binding:
di.alias(Store, to=PostgresStore), with no second instance. -
Tags when several implementations share a key:
di.resolve(Cache, tag='primary'). -
Optional dependencies for a parameter that may go unbound:
def __init__(self, metrics: MetricsSink | None): ..., resolved toNone. -
Collections for plugin points:
di.collect(Handler, [EmailHandler, SmsHandler]), injected asdef __init__(self, handlers: list[Handler]): .... -
Scope-supplied values:
di.scope_value(Request), filled by middleware withframe.provide(Request, request). -
Overrides for tests:
with di.override(Database, FakeDB()): .... -
Function injection with
@di.inject: parameters whose default isinjected(...)are filled from the container, the rest are passed by the caller:@di.inject def handler(uid: int, repo: UserRepo = injected(UserRepo)) -> User: return repo.get(uid) handler(uid=1) # repo injected; call site stays type-clean
FastAPI
from fastapi import FastAPI
from depin import Container, Scope
from depin.ext.fastapi import Inject, RequestScope
di = Container().bind(UserService, scope=Scope.SCOPED).freeze()
app = FastAPI()
app.add_middleware(RequestScope, container=di)
@app.get('/users/{uid}')
async def get_user(uid: int, svc: Inject[UserService]) -> User:
return await svc.get(uid)
Inject[T] is a type-level shortcut: the parameter's static type is T, while
at runtime Inject[T] resolves to Annotated[T, Depends(...)] so FastAPI picks
up the dependency from the annotation. No default-value calls, no # noqa: B008
waivers.
RequestScope runs as pure ASGI middleware, so streaming responses, SSE, and
WebSockets pass through unbuffered. Scoped providers may declare Request to
read headers, URL, cookies, and state — but it is metadata-only: the request body
belongs to the route's typed parameters, and reading it from a provider raises
rather than racing the handler's own parsing.
Full walkthrough: FastAPI guide.
Caveats
- Nested scopes inherit. A
SCOPEDinstance resolved in an outer scope is reused inside a nested scope, not rebuilt. Open sibling scopes for independent instances. - A consumer built before the override keeps its old value.
override()replaces the key immediately, even for a singleton already built — but a consumer resolved earlier keeps the instance it was given. Callreset()to evict it, or use thedepin.ext.pytestfixtures, which callreset()for you. @di.injectuses default-position markers. An injected parameter carries aninjected(...)default, so it must follow non-default parameters or be keyword-only (a normal Python rule). Unlike provider constructors, which resolve from type hints andAnnotated[...],@injectfills only marked parameters and validates them at decoration time, raisingMissingProviderErrorimmediately if a marked key is unregistered.
Project status
Beta, pre-1.0. CI enforces ruff, basedpyright --strict, mypy --strict, the
full test suite with its embedded doctests, and a 95% coverage floor, on Python
3.12–3.14 across Linux, macOS, and Windows, plus the free-threaded builds of 3.13
and 3.14. See the
support policy.
Releases are published from CI via PyPI Trusted Publishing. Minor releases may
still contain breaking changes until 1.0; those are marked in the
changelog.
Development
uv sync --all-extras
uv run ruff format
uv run ruff check
uv run basedpyright
uv run mypy
uv run pytest
The five commands above are the gates every change must pass. See CONTRIBUTING.md for the full workflow and AGENTS.md for the repository conventions that contributors — human or agent — are expected to follow.
Contributing
Contributions are welcome. Read CONTRIBUTING.md for the development setup, the five gates, and commit conventions; all participants are expected to follow the Code of Conduct. To report a vulnerability, follow the security policy.
License
MIT © André Lopes
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 pydepin-0.16.1.tar.gz.
File metadata
- Download URL: pydepin-0.16.1.tar.gz
- Upload date:
- Size: 664.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22ffd4b3fb8395c2992e0599cc73b5d012cce58398fefdf73ced740d1e308b45
|
|
| MD5 |
7041e8661bf6b9f66152c63616706c28
|
|
| BLAKE2b-256 |
47d775d935ea8d6045cb89227828e9b595e7b0cc4bac0584012b0604a8735cfa
|
Provenance
The following attestation bundles were made for pydepin-0.16.1.tar.gz:
Publisher:
release.yml on andrelopes-code/depin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydepin-0.16.1.tar.gz -
Subject digest:
22ffd4b3fb8395c2992e0599cc73b5d012cce58398fefdf73ced740d1e308b45 - Sigstore transparency entry: 2674940155
- Sigstore integration time:
-
Permalink:
andrelopes-code/depin@95e88950ddb9e3055fa9540b0ce319158ef1c151 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/andrelopes-code
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@95e88950ddb9e3055fa9540b0ce319158ef1c151 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pydepin-0.16.1-py3-none-any.whl.
File metadata
- Download URL: pydepin-0.16.1-py3-none-any.whl
- Upload date:
- Size: 97.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af3018232dd4f164f5dfa0c4a80a5b144d200c4add5916bbd1f5cf58c682bd6d
|
|
| MD5 |
bf26cae597119df21db530d9fc06492d
|
|
| BLAKE2b-256 |
a17961a119fa5a6c3943ec27365312c0e7f5f929e40e2103f6109fbb42835bc8
|
Provenance
The following attestation bundles were made for pydepin-0.16.1-py3-none-any.whl:
Publisher:
release.yml on andrelopes-code/depin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydepin-0.16.1-py3-none-any.whl -
Subject digest:
af3018232dd4f164f5dfa0c4a80a5b144d200c4add5916bbd1f5cf58c682bd6d - Sigstore transparency entry: 2674940263
- Sigstore integration time:
-
Permalink:
andrelopes-code/depin@95e88950ddb9e3055fa9540b0ce319158ef1c151 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/andrelopes-code
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@95e88950ddb9e3055fa9540b0ce319158ef1c151 -
Trigger Event:
push
-
Statement type: