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.
  • Optional FastAPI integration in depin.ext.fastapi. 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

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.
  • Overrides do not evict caches. A singleton resolved before the override block is already built, and the override does not replace it. Override before the first resolution, or build a fresh container per test.
  • @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.12.2.tar.gz (499.5 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.12.2-py3-none-any.whl (69.0 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for pydepin-0.12.2.tar.gz
Algorithm Hash digest
SHA256 b4e587555eeed9d208a8d5af4147d7ded86605358a9c9282bc451c2887870abc
MD5 4d088ad8b16db6a107a0b7b8d8c518e8
BLAKE2b-256 0f2e97d135944091034bae0f64d7bfb79267214a9e45605e8ccc39f7a0f17ee8

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: pydepin-0.12.2-py3-none-any.whl
  • Upload date:
  • Size: 69.0 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.12.2-py3-none-any.whl
Algorithm Hash digest
SHA256 ad9bbd33a66dc8eeb69406795dc1be886e780c46aec7e5a256553229b11e95bc
MD5 9faecd997cadc0824dad5d9d87154366
BLAKE2b-256 baa3eb49b9693ad5c99d9bd8d83fce183f3b82a5c8c67dbd46274c21be0482ce

See more details on using hashes here.

Provenance

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

0.15.0

2 files

0.14.0

2 files

0.13.0

2 files

This release

0.12.2 This release

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