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) -> 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 a default-position marker. An injected parameter carries the injected default, so it must follow non-default parameters or be keyword-only (a normal Python rule). The key comes from the annotation, in the same grammar provider constructors use — a class, Annotated[T, Tag(...)], Annotated[T, Named(...)], T | None. @inject fills only marked parameters and validates them at decoration time, raising MissingProviderError immediately if a marked key is unregistered.

Performance

Every workload is paired with the simplest honest Python doing the same work, and the two are proved to behave identically before either is timed. Results are published per workload with their baseline, their uncertainty, and the readings they do not support — there is no overall score and no claim to be the fastest library.

Measured figures, methodology, scaling curves and reproduction instructions: performance. No numbers are quoted here, so nothing on this page can go stale.

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.20.0.tar.gz (9.1 MB view details)

Uploaded Source

Built Distribution

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

pydepin-0.20.0-py3-none-any.whl (127.3 kB view details)

Uploaded Python 3

File details

Details for the file pydepin-0.20.0.tar.gz.

File metadata

  • Download URL: pydepin-0.20.0.tar.gz
  • Upload date:
  • Size: 9.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pydepin-0.20.0.tar.gz
Algorithm Hash digest
SHA256 32bde5973e7f5e1edae16114121bc59c424f02d694e4d0fa007d873e0ae4f097
MD5 97a72d0d1441345c026c35e17549ff52
BLAKE2b-256 d2525405953e50c1b7239490852ba008cdd58371ef574c6b8605a49f2ed2959f

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydepin-0.20.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.20.0-py3-none-any.whl.

File metadata

  • Download URL: pydepin-0.20.0-py3-none-any.whl
  • Upload date:
  • Size: 127.3 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.20.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c71cd504664a86c2e171176e6e763457b9d3b10dc814d56fc388b72f48594ced
MD5 1d1ecb4fcc9eac95496a7fcb6256a211
BLAKE2b-256 9c42a347ead8fbce71b190c806d874c010722c0c4cf48088af525d0e5bd97e47

See more details on using hashes here.

Provenance

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

Release history Release notifications | RSS feed

0.21.0

2 files

This release

0.20.0 This release

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

0.16.1

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