Metadata-Version: 2.5
Name: pydepin
Version: 0.17.2
Summary: Type-first dependency-injection library for Python
Project-URL: Homepage, https://andrelopes-code.github.io/depin/
Project-URL: Documentation, https://andrelopes-code.github.io/depin/
Project-URL: Repository, https://github.com/andrelopes-code/depin
Project-URL: Issues, https://github.com/andrelopes-code/depin/issues
Project-URL: Changelog, https://github.com/andrelopes-code/depin/blob/main/CHANGELOG.md
Author-email: André Lopes <andrelopes.code@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: annotated,dependency-injection,di,fastapi,inversion-of-control,ioc,protocol,type-hints,typing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Provides-Extra: click
Requires-Dist: click>=8.1; extra == 'click'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.133; extra == 'fastapi'
Requires-Dist: starlette>=1.1; extra == 'fastapi'
Provides-Extra: flask
Requires-Dist: flask>=3.1; extra == 'flask'
Provides-Extra: litestar
Requires-Dist: litestar>=2.13; extra == 'litestar'
Provides-Extra: pytest
Requires-Dist: pytest>=9.0.3; extra == 'pytest'
Provides-Extra: starlette
Requires-Dist: starlette>=1.1; extra == 'starlette'
Provides-Extra: taskiq
Requires-Dist: taskiq>=0.11.19; extra == 'taskiq'
Provides-Extra: typer
Requires-Dist: typer>=0.16; extra == 'typer'
Description-Content-Type: text/markdown

# depin

[![CI](https://github.com/andrelopes-code/depin/actions/workflows/ci.yml/badge.svg)](https://github.com/andrelopes-code/depin/actions/workflows/ci.yml)
[![Docs](https://github.com/andrelopes-code/depin/actions/workflows/docs.yml/badge.svg)](https://andrelopes-code.github.io/depin/)
[![PyPI](https://img.shields.io/pypi/v/pydepin.svg)](https://pypi.org/project/pydepin/)
[![Python versions](https://img.shields.io/pypi/pyversions/pydepin.svg)](https://pypi.org/project/pydepin/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/andrelopes-code/depin/blob/main/LICENSE)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/andrelopes-code/depin/badge)](https://scorecard.dev/viewer/?uri=github.com/andrelopes-code/depin)

Type-first dependency injection for Python 3.12+.

**Documentation:** <https://andrelopes-code.github.io/depin/> · **PyPI:** [`pydepin`](https://pypi.org/project/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](https://andrelopes-code.github.io/depin/latest/guide/integrations/).
- 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

```bash
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

```python
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:

```python
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](https://andrelopes-code.github.io/depin/latest/guide/lifetimes/)
for nesting, captive dependencies, and shutdown.

## Cookbook

Runnable code lives in [`examples/`](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:

  ```python
  @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

```python
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](https://andrelopes-code.github.io/depin/latest/guide/fastapi/).

## 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.

## 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](https://andrelopes-code.github.io/depin/latest/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](https://andrelopes-code.github.io/depin/latest/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](CHANGELOG.md).

## Development

```bash
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](CONTRIBUTING.md) for the full workflow and
[AGENTS.md](AGENTS.md) for the repository conventions that contributors — human
or agent — are expected to follow.

## Contributing

Contributions are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) for the
development setup, the five gates, and commit conventions; all participants are
expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md). To report a
vulnerability, follow the [security policy](SECURITY.md).

## License

[MIT](LICENSE) © André Lopes
