Skip to main content

depin

CI Docs PyPI Python versions License: MIT OpenSSF Scorecard

Type-first dependency injection for Python 3.12+.

Documentation: https://andrelopes-code.github.io/depin/ · PyPI: pydepin

  • Resolution driven by type hints; Protocol and Annotated are 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 stray TypeError from 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 — no depin._core import required. Writing an integration.
  • Optional web integrations for FastAPI, Starlette, Litestar and Flask, built on the framework-free depin.ext.asgi and depin.ext.wsgi middlewares 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.cli seam any other command framework can drive, and one scope per Taskiq message seeded with the TaskiqMessage being executed.
  • No # type: ignore at call sites: resolve(), frozen[key], injected(), and Inject[T] are all precisely typed under basedpyright --strict and mypy --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 via di[token].

  • Registries for composition: Container(infra, services).freeze().

  • Protocols: @provides(Store) on the implementation, then di.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 to None.

  • Collections for plugin points: di.collect(Handler, [EmailHandler, SmsHandler]), injected as def __init__(self, handlers: list[Handler]): ....

  • Scope-supplied values: di.scope_value(Request), filled by middleware with frame.provide(Request, request).

  • Overrides for tests: with di.override(Database, FakeDB()): ....

  • Function injection with @di.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
    

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 SCOPED instance 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. Call reset() to evict it, or use the depin.ext.pytest fixtures, which call reset() for you.
  • @di.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). 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.

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

pydepin-0.16.1.tar.gz (664.8 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.16.1-py3-none-any.whl (97.1 kB view details)

Uploaded Python 3

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

Hashes for pydepin-0.16.1.tar.gz
Algorithm Hash digest
SHA256 22ffd4b3fb8395c2992e0599cc73b5d012cce58398fefdf73ced740d1e308b45
MD5 7041e8661bf6b9f66152c63616706c28
BLAKE2b-256 47d775d935ea8d6045cb89227828e9b595e7b0cc4bac0584012b0604a8735cfa

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydepin-0.16.1.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.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

Hashes for pydepin-0.16.1-py3-none-any.whl
Algorithm Hash digest
SHA256 af3018232dd4f164f5dfa0c4a80a5b144d200c4add5916bbd1f5cf58c682bd6d
MD5 bf26cae597119df21db530d9fc06492d
BLAKE2b-256 a17961a119fa5a6c3943ec27365312c0e7f5f929e40e2103f6109fbb42835bc8

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydepin-0.16.1-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.

Release history Release notifications | RSS feed

0.21.0

2 files

0.20.0

2 files

0.19.0

2 files

0.18.0

2 files

0.17.2

2 files

0.17.1

2 files

0.17.0

2 files

0.16.3

2 files

0.16.2

2 files

This release

0.16.1 This release

2 files

0.16.0

2 files

0.15.0

2 files

0.14.0

2 files

0.13.0

2 files

0.12.2

2 files

0.12.1

2 files

0.12.0

2 files

0.11.0

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.1

2 files

0.5.0

2 files

0.4.3

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

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