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.11.0.tar.gz (431.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.11.0-py3-none-any.whl (63.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pydepin-0.11.0.tar.gz
  • Upload date:
  • Size: 431.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.11.0.tar.gz
Algorithm Hash digest
SHA256 80b185b5c465c883d9ebd6d8a4dae3b5f4c90971f0f0116eb3ce1e9ee808637b
MD5 d0c5e998735293b7824ddcd0af6f898b
BLAKE2b-256 de1b8ba2bd2a52b7c3ed5c5df741ed3c41b126792f3349726bfaf98917ea826c

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: pydepin-0.11.0-py3-none-any.whl
  • Upload date:
  • Size: 63.6 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.11.0-py3-none-any.whl
Algorithm Hash digest
SHA256 da2370c1971fa7a5a27cb3ec8761eec0be61da1ea0f49f6825d2b41821a6ba54
MD5 1d7a84f93f4f0ec018fee8960964856e
BLAKE2b-256 c174acc6c56a09a3a4b79b274b40fd36b0cdb1a1b842c8f299557ba8aa892dd8

See more details on using hashes here.

Provenance

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

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

This release

0.11.0 This release

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