Type-first dependency-injection library for Python
Project description
depin
Type-first dependency-injection for Python 3.12+.
- Resolution driven by type hints;
ProtocolandAnnotatedare first-class. - Build-time validation:
Container.freeze()catches missing providers, cycles, lifetime violations (a singleton that would capture a scoped provider), and async/sync mismatches before anything runs. - Full async/sync coverage: classes, sync/async factories, generators, async generators,
@(a)contextmanager, instance context managers. - Optional FastAPI integration in
depin.ext.fastapi. Core has zero runtime dependencies.
Install
uv add pydepin # core
uv add 'pydepin[fastapi]' # with FastAPI integration
Requires Python 3.12+.
Quickstart
from typing import Annotated
from depin import Container, Scope, Token
db_url = Token[str]('db.url')
class Database:
def __init__(self, url: str) -> None:
self.url = url
def make_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(make_db, scope=Scope.SINGLETON, provides=Database)
.bind(UserRepo, scope=Scope.SINGLETON)
.freeze()
)
repo = di[UserRepo]
Cookbook
See examples/ for runnable code. Highlights:
-
Tokens for values:
Token[str]('db.url'), resolved viadi[token]. -
Generator providers for lifecycle:
def session() -> Generator[Session]: ...withyield; teardown runs on scope exit. -
Async generators +
async with di.ascope(): ...for per-request DB sessions. -
Tag +
providesfor multiple implementations of aProtocol. -
Override for tests:
with di.override(Database, with_=FakeDB()): .... -
Frame-provided values (
di.frame_provides(Request)) for middleware-injected context. -
Function injection with
@frozen.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
Use
injected(Token[...])for token values andinjected(Svc, tag='...')for tagged providers.
FastAPI
from fastapi import FastAPI
from depin import Container, Scope
from depin.ext.fastapi import RequestScope, Inject
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]):
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 parameter's annotation. No default-value calls, no
# noqa: B008 waivers, no extra imports.
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.
Caveats
- Lifecycle teardown. Singleton providers that use
yield/ context managers are torn down byawait frozen.aclose(). Call it on app shutdown; scope-local providers are drained automatically when theirscope()/ascope()block exits. - Nested scopes inherit. A
SCOPEDinstance resolved in an outer scope is reused inside a nested scope, not rebuilt. Open sibling scopes for independent instances. @frozen.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). The marker keeps call sites type-clean — no# pyright: ignoreneeded. 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.
Status
v0.2.0 is a clean break from 0.1.x. The migration is breaking; older code will not run unchanged.
| 0.1.x | 0.2.0 |
|---|---|
Container() resolves directly |
Container().freeze() -> FrozenContainer |
Inject(fn) default value |
svc: T = injected(T) under @frozen.inject, or Inject[T] (fastapi ext) |
Container.Depends(X) |
frozen[X], frozen.resolve(X), or Inject[T] (fastapi ext) |
Scope.REQUEST |
Scope.SCOPED |
RequestScopeService.request_scope() |
frozen.scope() / frozen.ascope() |
Development
uv sync --all-extras
uv run ruff format
uv run ruff check
uv run basedpyright
uv run pytest
See CONTRIBUTING.md for the full workflow and CLAUDE.md for repository conventions.
Contributing
Contributions are welcome. Read CONTRIBUTING.md for the development setup, the four 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
Project details
Release history Release notifications | RSS feed
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.3.0.tar.gz.
File metadata
- Download URL: pydepin-0.3.0.tar.gz
- Upload date:
- Size: 102.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a517da9a5f9b6b2b6d5a3dfc9b8961c5aeb60ab067bf9e0bbc4952d639dcf952
|
|
| MD5 |
5705622f8076cf52425f4a939df4124e
|
|
| BLAKE2b-256 |
d331f94b92b98fdd25957e60d21fa1b16cd3a044f695e697fa59d5b0782ee0d8
|
Provenance
The following attestation bundles were made for pydepin-0.3.0.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.3.0.tar.gz -
Subject digest:
a517da9a5f9b6b2b6d5a3dfc9b8961c5aeb60ab067bf9e0bbc4952d639dcf952 - Sigstore transparency entry: 1624864598
- Sigstore integration time:
-
Permalink:
andrelopes-code/depin@310cfab9f0eeab968109213dead5dc2fc807b289 -
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@310cfab9f0eeab968109213dead5dc2fc807b289 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pydepin-0.3.0-py3-none-any.whl.
File metadata
- Download URL: pydepin-0.3.0-py3-none-any.whl
- Upload date:
- Size: 30.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e8c990430230bd86803dc0ca1b6ae028cfe9eadccc63d499b9171d35097976b0
|
|
| MD5 |
b9b8b1c8516947c875d765ec202244f0
|
|
| BLAKE2b-256 |
60b324341a31a723c22482e9c171f89919e08aef53f2d4ca90d021b666e5cb65
|
Provenance
The following attestation bundles were made for pydepin-0.3.0-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.3.0-py3-none-any.whl -
Subject digest:
e8c990430230bd86803dc0ca1b6ae028cfe9eadccc63d499b9171d35097976b0 - Sigstore transparency entry: 1624864607
- Sigstore integration time:
-
Permalink:
andrelopes-code/depin@310cfab9f0eeab968109213dead5dc2fc807b289 -
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@310cfab9f0eeab968109213dead5dc2fc807b289 -
Trigger Event:
push
-
Statement type: