You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
> 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.
36
36
>
37
37
> 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.
39
39
> -**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.
41
41
> -**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.
42
42
> - Zero-dependency core, MIT, 116 releases.
43
43
>
44
44
> Where it honestly stands:
45
45
> - 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."
47
50
> - 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).
48
51
>
49
52
> Docs include a full comparison and a "do you even need a DI container?" page: https://modern-di.modern-python.org
> 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.
53
56
54
57
Etiquette: post Tue–Thu ~8–10am ET; never ask for upvotes; reply to every
55
58
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.
63
66
**Body:**
64
67
65
68
> **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.
67
70
>
68
71
> **Target Audience**
69
72
> 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.
70
73
>
71
74
> **Comparison**
72
75
> -**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.
> -**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.
@@ -83,7 +86,7 @@ comment; the honest "when not to use it" is what earns goodwill.
83
86
Submit the link **https://modern-di.modern-python.org** (the docs, not the bare
84
87
repo), tag `python`. Authored comment:
85
88
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.
87
90
88
91
Only post if you have a Lobsters account with karma; it's allergic to drive-by
89
92
self-promo.
@@ -130,9 +133,12 @@ of four parallel copies that drift apart. Thirteen official integrations cover t
130
133
web frameworks (FastAPI, Litestar, Starlette, Flask, aiohttp), the task queues
131
134
(Celery, arq, taskiq), messaging, gRPC, and the CLI. It uses type-based autowiring
132
135
(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
136
142
you only have a single web service, your framework's own `Depends` is enough —
137
143
modern-di earns its place when you have a second entrypoint. (The docs include an
138
144
honest comparison with dependency-injector and Dishka, and a "do you even need
Copy file name to clipboardExpand all lines: profile/README.md
+7Lines changed: 7 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -21,6 +21,13 @@ Open-source templates and libraries for building production-ready Python applica
21
21
22
22
### Dependency injection
23
23
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
+
24
31
| Project | What it is | Stars | Downloads |
25
32
|---|---|---|---|
26
33
|[`modern-di`](https://github.com/modern-python/modern-di)| Powerful dependency-injection framework with IoC container and scopes |[](https://github.com/modern-python/modern-di/stargazers)|[](https://pepy.tech/projects/modern-di)|
0 commit comments