Skip to content

Commit 2e9d42e

Browse files
authored
docs(profile): steer newcomers to modern-di, and state the declaration-model edge accurately (#48)
* docs(profile): steer newcomers to modern-di, name the decorator-free edge that-depends' own README says "if you're starting a new project, consider modern-di", but nothing on the org surfaces said so -- and the two canonical blurbs actively inverted it: modern-di reads "Powerful", that-depends reads "Simple". A newcomer with no other signal picks "simple", i.e. the one we steer away from. Adds a one-line steer above the DI table on profile/README.md and the docs site: modern-di for new projects, that-depends when you need async resolution. Leaves the canonical one-liners untouched, since those are synced across the GitHub description and pyproject on both repos. Also plays modern-di's strongest card in the launch copy. Verified against source, not taken on trust: modern_di defines and exports no DI decorator, while dishka needs @provide on providers and @Inject on every handler. The copy states the limit of the claim too -- FromDI(...) at the FastAPI boundary is still a marker -- so the claim survives the scrutiny an HN thread applies. * fix(playbook): correct the decorator claim -- @Inject IS needed in 7 integrations The previous copy said modern-di "ships no @inject/@provide decorator at all". That was wrong: it generalized from FastAPI and Litestar. Checked all 12 integrations -- @Inject is required in Starlette, Flask, aiohttp, Celery, arq, aiogram and Typer, and only FastAPI, Litestar, FastStream and taskiq are free of it. The universal, defensible difference is narrower: providers are plain class attributes, so there is no @provide anywhere, where dishka requires one on every provider. The @Inject advantage is real but holds in 4 of 11 integrations, not all of them. Copy now states both the claim and its limits, so it survives the scrutiny an HN thread applies -- which is the entire point of the honest framing. * fix(playbook): "sync-only" was wrong -- async apps are first-class modern-di is not sync-only. The container is an async context manager, async finalizers are supported (there is a dedicated AsyncFinalizerInSyncCloseError pointing at close_async), and async resources -- an aiohttp session, an asyncpg pool -- are constructed in the framework lifespan and injected by type via a ContextProvider, which is a documented recipe. Most of the 13 integrations are async frameworks. What is actually synchronous is the resolve path: container.resolve(X) never awaits. That is the claim the copy should make, and it now does, across all ten places that said "sync-only" or "sync resolution by design". Also drops the advice to use Dishka for async. The ContextProvider recipe is the answer, so the copy points there instead. Dishka is still named honestly as the more established alternative -- we just do not send people to it. Fixes the same misframing in the org-profile and docs-site DI steers added earlier on this branch.
1 parent 38ad18a commit 2e9d42e

3 files changed

Lines changed: 34 additions & 11 deletions

File tree

docs/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ catalog below.
9696

9797
## Dependency injection { #di }
9898

99+
!!! tip "Which one should I use?"
100+
101+
Start new projects on [`modern-di`](https://github.com/modern-python/modern-di)
102+
— a minimal core plus the integrations listed below. Async apps are first-class:
103+
the resolve path itself stays synchronous, and resources that need to `await` are
104+
constructed in the framework lifespan
105+
([recipe](https://modern-di.modern-python.org/recipes/async-lifespan/)).
106+
[`that-depends`](https://github.com/modern-python/that-depends) is the async-first
107+
sibling and stays maintained; it ships a migration guide to `modern-di`.
108+
99109
- [`modern-di`](https://github.com/modern-python/modern-di) — powerful DI framework with scopes.
100110
- [`modern-di-aiogram`](https://github.com/modern-python/modern-di-aiogram)`modern-di` integration for aiogram.
101111
- [`modern-di-aiohttp`](https://github.com/modern-python/modern-di-aiohttp)`modern-di` integration for aiohttp.

planning/launch-playbook.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,24 @@ Suggested order: **modern-di (HN+Reddit)** → **stack announcement** →
3535
> I kept building a FastAPI API, a FastStream worker, and a Typer CLI that all shared the same business logic — and rewiring the same dependencies three times, once per entrypoint. modern-di is my attempt at *one* typed wiring shared across all of them.
3636
>
3737
> What it does:
38-
> - **Type-based autowiring** — your constructor type hints *are* the graph; no `Provide[...]` markers or string keys.
38+
> - **Type-based autowiring** — your constructor type hints *are* the graph, and providers are plain class attributes on a `Group`. There is no `@provide` decorator and no string keys. In the FastAPI, Litestar, FastStream and taskiq integrations your handlers need no DI decorator either — just the framework's own route decorator.
3939
> - **Explicit scopes** (APP → REQUEST → …) with **build-time** cycle- and scope-violation checks.
40-
> - **Sync resolution by design**async setup/teardown lives in the framework lifespan, not in resolution. This is deliberate and permanent, not a missing feature.
40+
> - **Async apps are first-class; the *resolve path* is synchronous.** `container.resolve(X)` never awaits — but the container is an async context manager, async finalizers are supported, and genuinely async resources (an `aiohttp` session, an `asyncpg` pool) are constructed in the framework lifespan and injected by type. Most of the integrations are async frameworks. Keeping `await` out of resolution is deliberate and permanent, not a missing feature.
4141
> - **Official integrations** for FastAPI, Litestar, Starlette, Flask, aiohttp, FastStream, Celery, arq, taskiq, aiogram, gRPC, and Typer — plus a first-party pytest plugin that turns any dependency into a fixture.
4242
> - Zero-dependency core, MIT, 116 releases.
4343
>
4444
> Where it honestly stands:
4545
> - If you're building a single FastAPI service and everything is request-scoped, FastAPI's `Depends` is enough — you don't need this.
46-
> - The closest library is **Dishka**, which is considerably more established (~1.2k stars to my ~60) and supports async resolution and custom scopes. If you need either, use Dishka. Integration coverage is now roughly comparable between the two. modern-di's bets are a simpler sync-only model, the first-party pytest plugin, and being one consistent small stack.
46+
> - The closest library is **Dishka** — considerably more established (~1.2k stars to my ~60), with custom scopes and `await` inside resolution. modern-di deliberately takes the other road: an async resource (an `aiohttp` session, an `asyncpg` pool) is constructed in the framework lifespan and injected by type via a `ContextProvider`, so the resolve path stays synchronous. There's a recipe for exactly this: https://modern-di.modern-python.org/recipes/async-lifespan/
47+
> - Integration coverage between the two is now roughly comparable. modern-di's bets are a lighter declaration model, a smaller core, the first-party pytest plugin, and being one consistent small stack.
48+
>
49+
> On the declaration model, precisely — because I'd rather state the limits than have them found: providers are plain class attributes, so there's no `@provide` anywhere, and in the FastAPI/Litestar/FastStream/taskiq integrations handlers need no DI decorator. But seven of the other integrations (Starlette, Flask, aiohttp, Celery, arq, aiogram, Typer) *do* need an `@inject`, and at the FastAPI boundary you still name the provider (`FromDI(Dependencies.user_service)`), because that's how FastAPI's own DI hooks in. So: lighter than Dishka on declarations, not "decorator-free."
4750
> - It's young but actively developed, and deliberately conservative — the docs have a "design decisions" page for what it leaves out on purpose (auto-binding, in-package integrations, graph rendering).
4851
>
4952
> Docs include a full comparison and a "do you even need a DI container?" page: https://modern-di.modern-python.org
5053
> Repo: https://github.com/modern-python/modern-di
5154
>
52-
> Happy to dig into the design — especially the sync-only decision.
55+
> Happy to dig into the design — especially the decision to keep `await` out of the resolve path.
5356
5457
Etiquette: post Tue–Thu ~8–10am ET; never ask for upvotes; reply to every
5558
comment; the honest "when not to use it" is what earns goodwill.
@@ -63,16 +66,16 @@ comment; the honest "when not to use it" is what earns goodwill.
6366
**Body:**
6467

6568
> **What My Project Does**
66-
> modern-di is a dependency-injection framework. You declare providers once (type-based autowiring from constructor hints), and resolve them with explicit scopes (APP → REQUEST → …) and build-time cycle/scope checks. The same container wires your FastAPI app, your FastStream workers, your Celery/arq/taskiq tasks, your Typer CLI, and your tests — via 13 official integrations and a first-party pytest plugin that turns any dependency into a fixture. Sync resolution by design; async lives in the framework lifespan.
69+
> modern-di is a dependency-injection framework. You declare providers once (type-based autowiring from constructor hints), and resolve them with explicit scopes (APP → REQUEST → …) and build-time cycle/scope checks. The same container wires your FastAPI app, your FastStream workers, your Celery/arq/taskiq tasks, your Typer CLI, and your tests — via 13 official integrations and a first-party pytest plugin that turns any dependency into a fixture. Async apps are first-class — most of the integrations are async frameworks; what's synchronous is the *resolve path* itself, with async construction and teardown handled in the framework lifespan.
6770
>
6871
> **Target Audience**
6972
> Python teams whose business logic runs behind *more than one entrypoint* (an API plus workers/CLIs) and who want one wiring instead of three. It's production-intended but young — early adopters welcome. If you have a single web service where everything is request-scoped, framework-native `Depends`/`Provide` is enough and modern-di is overkill.
7073
>
7174
> **Comparison**
7275
> - **vs FastAPI `Depends` / Litestar `Provide`:** great inside one app, but don't span workers/CLIs or give typed app-scoped singletons; modern-di shares one wiring across all entrypoints.
7376
> - **vs `dependency-injector`:** type-based autowiring instead of `Provide[...]` markers; nested request scopes; first-party pytest plugin.
74-
> - **vs Dishka** (the closest library): Dishka is considerably more established (~1.2k stars) and supports async resolution + custom scopes — pick it if you need those. Integration coverage is now roughly comparable. modern-di bets on a simpler sync-only model and a first-party pytest plugin.
75-
> - **vs `that-depends`** (my earlier framework): modern-di adds explicit scopes and drops global state; that-depends stays maintained for async resolution.
77+
> - **vs Dishka** (the closest library): Dishka is considerably more established (~1.2k stars), has custom scopes, and awaits inside resolution. modern-di takes the other road — async resources are built in the lifespan and injected by type through a `ContextProvider` ([recipe](https://modern-di.modern-python.org/recipes/async-lifespan/)), so resolution stays sync. Integration coverage is now roughly comparable. modern-di bets on a lighter declaration model (providers are plain class attributes — no `@provide`; and no `@inject` in the FastAPI/Litestar/FastStream/taskiq integrations, though seven others do need one), a smaller core, and a first-party pytest plugin.
78+
> - **vs `that-depends`** (my earlier framework): modern-di adds explicit scopes and drops global state; that-depends remains the async-first sibling and stays maintained.
7679
>
7780
> Docs + comparison: https://modern-di.modern-python.org
7881
@@ -83,7 +86,7 @@ comment; the honest "when not to use it" is what earns goodwill.
8386
Submit the link **https://modern-di.modern-python.org** (the docs, not the bare
8487
repo), tag `python`. Authored comment:
8588

86-
> Typed DI for Python with explicit scopes, built so one container wires a FastAPI app, FastStream workers, and a Typer CLI together. Sync-only by design. The docs include an honest comparison vs dependency-injector/Dishka and a "do you even need DI?" section. Author here — happy to discuss the sync-only call.
89+
> Typed DI for Python with explicit scopes, built so one container wires a FastAPI app, FastStream workers, and a Typer CLI together. Async apps are first-class; the resolve path itself never awaits, by design. The docs include an honest comparison vs dependency-injector/Dishka and a "do you even need DI?" section. Author here — happy to discuss the no-await-in-resolve call.
8790
8891
Only post if you have a Lobsters account with karma; it's allergic to drive-by
8992
self-promo.
@@ -130,9 +133,12 @@ of four parallel copies that drift apart. Thirteen official integrations cover t
130133
web frameworks (FastAPI, Litestar, Starlette, Flask, aiohttp), the task queues
131134
(Celery, arq, taskiq), messaging, gRPC, and the CLI. It uses type-based autowiring
132135
(your constructor hints are the graph), explicit scopes with build-time validation,
133-
and a first-party pytest plugin that turns any dependency into a fixture. It's
134-
deliberately sync-only:
135-
async setup and teardown belong in the framework lifespan, not in resolution. If
136+
and a first-party pytest plugin that turns any dependency into a fixture. Async
137+
applications are first-class — most of those integrations are async frameworks.
138+
What is deliberately synchronous is the *resolve path*: a resource that genuinely
139+
needs to `await` (an `aiohttp` session, an `asyncpg` pool) is constructed in the
140+
framework lifespan and injected by type through a `ContextProvider`, so resolution
141+
itself never awaits. If
136142
you only have a single web service, your framework's own `Depends` is enough —
137143
modern-di earns its place when you have a second entrypoint. (The docs include an
138144
honest comparison with dependency-injector and Dishka, and a "do you even need

profile/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ Open-source templates and libraries for building production-ready Python applica
2121

2222
### Dependency injection
2323

24+
**Which one?** Start new projects on [`modern-di`](https://github.com/modern-python/modern-di)
25+
— a minimal core plus the integrations below. Async apps are first-class: the
26+
resolve path itself stays synchronous, and resources that need to `await` are built
27+
in the framework lifespan. [`that-depends`](https://github.com/modern-python/that-depends)
28+
is the async-first sibling and stays maintained; it ships a migration guide to
29+
`modern-di`.
30+
2431
| Project | What it is | Stars | Downloads |
2532
|---|---|---|---|
2633
| [`modern-di`](https://github.com/modern-python/modern-di) | Powerful dependency-injection framework with IoC container and scopes | [![Stars](https://img.shields.io/github/stars/modern-python/modern-di)](https://github.com/modern-python/modern-di/stargazers) | [![Downloads](https://static.pepy.tech/badge/modern-di/month)](https://pepy.tech/projects/modern-di) |

0 commit comments

Comments
 (0)