Skip to main content

Type-first dependency-injection library for Python

Project description

depin

CI PyPI Python versions License: MIT

Type-first dependency-injection for Python 3.12+.

  • Resolution driven by type hints; Protocol and Annotated are 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 via di[token].

  • Generator providers for lifecycle: def session() -> Generator[Session]: ... with yield; teardown runs on scope exit.

  • Async generators + async with di.ascope(): ... for per-request DB sessions.

  • Tag + provides for multiple implementations of a Protocol.

  • 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 is injected(...) 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 and injected(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 by await frozen.aclose(). Call it on app shutdown; scope-local providers are drained automatically when their scope() / ascope() block exits.
  • Nested scopes inherit. A SCOPED instance resolved in an outer scope is reused inside a nested scope, not rebuilt. Open sibling scopes for independent instances.
  • @frozen.inject uses default-position markers. An injected parameter carries an injected(...) 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: ignore needed. Unlike provider constructors, which resolve from type hints and Annotated[...], @inject fills only marked parameters and validates them at decoration time, raising MissingProviderError immediately 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


Download files

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

Source Distribution

pydepin-0.3.0.tar.gz (102.0 kB view details)

Uploaded Source

Built Distribution

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

pydepin-0.3.0-py3-none-any.whl (30.6 kB view details)

Uploaded Python 3

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

Hashes for pydepin-0.3.0.tar.gz
Algorithm Hash digest
SHA256 a517da9a5f9b6b2b6d5a3dfc9b8961c5aeb60ab067bf9e0bbc4952d639dcf952
MD5 5705622f8076cf52425f4a939df4124e
BLAKE2b-256 d331f94b92b98fdd25957e60d21fa1b16cd3a044f695e697fa59d5b0782ee0d8

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydepin-0.3.0.tar.gz:

Publisher: release.yml on andrelopes-code/depin

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for pydepin-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e8c990430230bd86803dc0ca1b6ae028cfe9eadccc63d499b9171d35097976b0
MD5 b9b8b1c8516947c875d765ec202244f0
BLAKE2b-256 60b324341a31a723c22482e9c171f89919e08aef53f2d4ca90d021b666e5cb65

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydepin-0.3.0-py3-none-any.whl:

Publisher: release.yml on andrelopes-code/depin

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page