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.
  • 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[pytest]'      # with the tested pytest floor enforced

depin.ext.asgi and depin.ext.wsgi are the framework-free middlewares those four specialise; they 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.15.0.tar.gz (602.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.15.0-py3-none-any.whl (88.3 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for pydepin-0.15.0.tar.gz
Algorithm Hash digest
SHA256 46613a8d073556fdf83cb102fed447c5709a498689f10c93fb30d83e2238db32
MD5 b433b3999f5220793ba71a3e38376df6
BLAKE2b-256 82e0818fb0cf48e766d72d6e0a96fee5ae8a7a1eda32b37a07eee124b59e9b49

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: pydepin-0.15.0-py3-none-any.whl
  • Upload date:
  • Size: 88.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.15.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b9f704776078ca075bc8c26d238647b463e28b0c73168add6e453306404e7636
MD5 90d269f13308becb52ca87578b8726d7
BLAKE2b-256 e1f2e42e31a556cf08489f5e91b3879781f5a6d966c5565e18587e3cf581d339

See more details on using hashes here.

Provenance

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

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

0.16.1

2 files

0.16.0

2 files

This release

0.15.0 This release

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