A DI container: what it actually does for you — and how dishka differs from dependency-injector
A container does not solve “reduce coupling”; it solves two concrete jobs: assemble the object graph and draw the boundary of how long those objects live. The main divergence between the two libraries is on the second one. From that follows both the fact that two repositories of one request end up in different transactions, and the answer to which of them is faster — which depends on the first question.
Full technical treatment
TL;DR
- A container does two things: it assembles the object graph and it draws the boundary of their lifetime. Everybody remembers the first; the second is where the libraries actually differ.
- dishka is built around lifespan:
Scope.APP → REQUEST, the boundary iswith container(), and a dependency is found by its type annotation. - dependency-injector is built around assembly: providers,
Provide[...]markers, wiring throughwire(). It has no ready-made request scope — you draw the boundary yourself. - Hence the main consequence:
Factoryhands two repositories of one request two different sessions, that is, two transactions. This is not a bug in the library — it is what its documentation says. - On speed there is no single winner: dependency-injector hands out a ready object 6 times faster, and dishka does "one session per request" 2.3 times faster.
Why have a container at all
Without one, assembly spreads across the handlers:
def handler(request):
session = Session(pool)
users = UserRepo(session)
orders = OrderRepo(session)
try:
return UnitOfWork(users, orders).run(request)
finally:
session.close()Three lines out of seven do nothing but create objects. Add a dependency to
UnitOfWork and every handler needs editing.
But look at session more closely. It is shared by both repositories and
it is closed on the way out. Neither property is about assembly; both are
about lifetime. From here on the libraries start answering differently.
The main difference
dishka knows what a request is. You declare that the session lives in
Scope.REQUEST, and within one request it is shared by everyone who asked for
it and closes itself on the way out.
dependency-injector has no such notion. Its Factory warns honestly in
the documentation that it creates a new object each time a dependency is
requested — and it does: UserRepo got one session, OrderRepo a second.
If one session per request is what you need, there is ContextLocalSingleton.
But then the request boundary is yours to draw:
try:
return container.uow()
finally:
container.session.reset() # forget it and the session outlives the requestAnd the nastiest part: without that line one request looks correct. The bug shows up only from the second one.
When you find out it was wired wrongly
- dishka validates the whole graph when the container is created. No factory for a dependency means a failure at application startup, where CI sees it too.
- dependency-injector builds the container silently. The error arrives on
the first call, and it arrives as a
TypeErrorout of your own class. - And if you forget to call
wire(), there is no error at all: a marker object arrives as the argument, and the failure happens later and elsewhere.
What did not hold up: the common claim that dependency-injector is untyped. mypy catches a result-type mismatch equally well in both libraries.
What it costs
Only the same work can be compared on speed, and there are two such places — with opposite results:
- handing out an object that already exists: 40 ns for
dependency-injector against 238 for dishka. The reason is simple — its
providers are compiled (
providers.abi3.so); - doing "one session per request": 2462 ns for dishka against 5583 for
ContextLocalSingletonwith a reset — and within those 2462 ns dishka also closes the session, whilereset()merely drops the reference.
Everything else is incomparable. Factory is cheaper than a full dishka
request precisely because it opens no request boundary, closes no session and
hands out two instead of one.
How to choose
The question is not "which is faster" but whether you need the request boundary as a concept.
You do, if a single request has more than one consumer of a shared resource — two repositories with one session are already enough. Then dishka closes the question with a mechanism rather than with discipline.
You do not, if your container is a registry of objects that live as long as
the process: configuration, a pool, clients. Then dependency-injector does
exactly what is needed, faster, and it has a Configuration provider that
reads YAML, INI and environment variables.
What is definitely not worth doing is choosing by the numbers in a README. Two hundred nanoseconds on a resolve are invisible in any web application. The visible difference is the other one: where two writes ended up in different transactions.
TL;DR
A container does two jobs, and the second one usually goes unnamed:
- assemble the graph — build an object together with everything it needs, without listing that at every call site;
- draw the lifetime boundary — say that this session lives for exactly one request, is shared by everyone who asks for it during that request, and is closed on the way out.
dishka builds its model around the second job: "Scope is a lifespan of a dependency", the boundary is a context manager, and a dependency is found by its type annotation.
dependency-injector builds its model around the first: providers gathered
into a container, with the request boundary drawn by you. Its Factory is
documented outright — "Factory provider creates new objects" — and therefore
hands two repositories of one request two different sessions, that is, two
transactions.
What the measurements say (3.13.7, dishka 1.10.1, dependency-injector 4.49.1):
- on the same work the order is reversed: handing out a ready object — dependency-injector is 6 times faster (40 ns against 238); doing "one session per request" — dishka is 2.3 times faster (2462 ns against 5583);
- a wiring mistake is visible in dishka when the container is created, in
dependency-injector on the first call — and a forgotten
wire()produces no error at all: the marker object arrives as the argument; - the common claim that dependency-injector is untyped does not survive a
check: mypy catches a mismatch equally well in both. What it does not catch
is something else — a substitution inside
Provide[...].
What a container actually does
The term is twenty-two years old and came from a text with a precise date.
As a result with a lot of discussion with various IoC advocates we settled on
the name Dependency Injection.
Fowler states the idea itself through a separate assembler object:
The basic idea of the Dependency Injection is to have a separate object, an
assembler, that populates a field in the lister class with an appropriate
implementation for the finder interface.
Notice what the definition does not contain. No "reduce coupling", no "easier testing", no "inversion of control" — those are consequences, not the mechanism. The mechanism is an assembler that knows what to fill with what.
The container's first job is to assemble the graph. Without one the code looks like this:
def handler(request):
session = Session(pool)
users = UserRepo(session)
orders = OrderRepo(session)
unit = UnitOfWork(users, orders)
try:
return unit.run(request)
finally:
session.close()Almost the whole body of the handler is assembly: four lines out of eight do
nothing but create objects. Add one more dependency to UnitOfWork and you
walk through every handler.
The second job — and this is the one that goes unnamed. Look at session
in that example: it is shared by both repositories and it is closed in a
finally. Neither property is about assembly; both are about lifetime —
how long an object lives and who shares it over that stretch. This is exactly
where the two libraries diverge, and the difference costs more than any
difference in speed.
One session per request — or one each
The check is simple. Two repositories in one request, both needing a session.
dishka answers that question with a notion of lifespan.
Scope is a lifespan of a dependency. Standard scopes are (with some
skipped): APP → REQUEST → ACTION → STEP
class MyProvider(Provider):
pool = provide(Pool, scope=Scope.APP)
users = provide(UserRepo, scope=Scope.REQUEST)
orders = provide(OrderRepo, scope=Scope.REQUEST)
@provide(scope=Scope.REQUEST)
def session(self, pool: Pool) -> Iterable[Session]:
session = Session(pool)
yield session
session.close()Within one REQUEST the session is single, verified with an is comparison.
On leaving the scope, whatever follows the yield runs — "The finalization of
dependencies runs in reverse creation order." The boundary is a context
manager: "To enter a nested scope, you call it and use it as a context
manager."
dependency-injector has no built-in hierarchy of scopes — and does not hide it. Its main provider is documented like this:
Factory provider creates new objects. Factory injects the dependencies every
time when creates a new object.
Every time means every time. UserRepo asked for a session and got a new one;
OrderRepo asked and got another. Measured: two different session numbers,
and users.session is orders.session is False. From which follows something
the measurement itself does not show: two writes of one request go into
different transactions, and you find out on the first rollback.
If one session per request is what you need, the library provides
ContextLocalSingleton for it: the session becomes one per execution
context. But now you draw the request
boundary yourself:
try:
return container.uow()
finally:
container.session.reset() # without this the session outlives the requestdependency-injector does have a provider with teardown — Resource: "Resource
provider provides a component with initialization and shutdown". But its
initialization "happens only once", that is, it is application level rather
than request level; for a request the documentation points to a separate
ContextLocalResource, and its boundary is still yours to draw.
And here is the important part: without reset() everything looks correct
for exactly one request. The session is shared, the transaction is single,
the tests are green. The second request gets the very same session — and that
is visible only once there are two requests.
How a dependency is found
The second divergence is in how you say what to inject.
dishka looks it up by type annotation. container.get(Service) and that is
all; the factory is found by the type you declared. Same in a handler: a
parameter typed FromDishka[Service].
dependency-injector looks it up by a marker pointing at a specific attribute of a specific container:
Wiring marker specifies what dependency to inject, e.g. Provide[Container.bar].
When wiring is done functions and methods with the markers are patched to
provide injections when called.
The difference looks stylistic and has three practical consequences, all three checkable.
First: the annotation and the marker are never compared. Here is code that passes both the container and mypy:
@inject
def handler(service: Service = Provide[Container.config]) -> str:
return service.run()A parameter declared Service receives a Config. It fails later —
AttributeError: 'Config' object has no attribute 'run' — and somewhere else.
Second: a forgotten wire() produces no error. There is nothing to patch,
the default value of the argument stays as it was, and the handler receives
the marker object itself:
print(type(handler()).__name__) # 'Provide'No exception, no warning. The symmetric case in dishka — an unwrapped handler
— fails at once: TypeError: missing 1 required positional argument.
Third: a dependency found by type can be checked in advance. Since the factory is looked up by the annotation, the whole graph can be walked when the container is built — which is what dishka does.
How much code has to be rewritten if you change libraries
The previous section raises a question worth asking before the choice: if the library is replaced in a year, what exactly gets rewritten.
The answer starts the same way for both, and that is worth stating outright, because the argument is usually had about something else. Neither touches the application layer. dishka states this as a requirement on itself:
- Usage of container must not require modification of objects we are creating.
- Container must not require being a global variable.
- Container can require code changes on the borders of scopes (e.g. application
start, middlewares, request handlers).
The third point is not a footnote but an honestly named zone of presence: the borders of scopes. dependency-injector promises the same about application structure and addresses its markers to the same place:
With wiring you do not need to change the traditional application structure of
your framework. […] Place wiring markers in the functions and methods where you
want the providers to be injected (Flask or Django views, Aiohttp or Sanic
handlers, etc).
So the question is not "how much code is infected" but "what does the marker name in the adapter layer".
And here the difference is fundamental. The form of the markers is identical
— both are Annotated: FromDishka[Service] is Annotated[Service, FromComponent()], and dependency-injector supports Annotated[Service, Provide[Container.service]]. What differs is the content. The first names a
type. The second names a path to an attribute of a specific container.
Hence the price of moving:
- a marker that names a type becomes another marker of the same type when the library changes — a mechanical edit, and a checkable one: the type in the annotation is still there;
- a marker that names
Container.servicecarries the container class's name and its attribute's name into an application file. Moving is not an import swap but a rename across the whole adapter layer, and a typo in it is not caught by a type checker (exactly the defect taken apart above).
There is also something dishka does not have at all: a list of files with markers. dependency-injector has one explicitly, because wiring is done by a call:
container.wire(modules=["yourapp.module1", "yourapp.module2"])That is at once an inconvenience (the list has to be maintained) and the thing
the other library lacks: the boundary of infection is visible in one place, and
it can be counted. Counting that is what to do when choosing — the number of
files in wire() against the number of files carrying FromDishka[].
Both document ways to loosen the coupling, and neither is free.
dependency-injector has string identifiers: With string identifiers you don't need to use a container to specify an injection
.
The container import leaves the application module — and the type check weakens
even further than in the case above.
dishka has DishkaRoute, which removes @inject but not FromDishka[]:
automatic injection works only for HTTP, not for websockets
, and the
parameter marker itself stays mandatory. Removing that too is possible — in the
source, default_parse_dependency returns None for a bare type annotation, so
nothing is injected by default, but wrap_injection's parse_dependency
parameter is public, and your own integration may inject by type. At the price of
your own integration.
The point to take away: what is detachable in both is not the application layer — that is detachable to begin with — but the adapter layer. And the question when choosing is one: does the marker name your type, or their container.
When you find out it was wired wrongly
This is the property people notice last and pay for most.
The difference is one line of code:
container = make_container(MyProvider())
# dishka: GraphMissingFactoryError — right here, at application startup
container = MyContainer()
# dependency-injector: container built; the TypeError comes on the first calldependency-injector does have check_dependencies(), and it is good: it finds
an unbound dependency with a clear message. But you have to call it yourself,
and it covers only declared providers.Dependency — a forgotten constructor
argument is not included.
On typing — against expectation. The claim that dishka is type-safe and
dependency-injector is not was checked with a mypy run and did not hold:
container.get(Service) and container.service() both return Service, and
assigning the result to a variable of another type is caught in both cases.
What mypy does not catch is exactly one thing — a substitution inside
Provide[...], because nothing ever compares the marker with the annotation.
dishka has a symmetric case of its own: requesting a REQUEST-scoped
dependency at APP level gives a NoFactoryError naming the scope — clear,
but already at run time rather than inside make_container. dependency-injector
cannot have this error at all: there is no declared scope in it to get wrong —
you draw the boundary yourself.
What it costs
These libraries can be compared on speed only where they do the same thing — and there are two such places.
Handing out an object that already exists. Here dependency-injector is six times faster: 40 ns against 238. The reason shows up in one line:
from dependency_injector import providers
print(providers.__file__) # .../providers.abi3.soThat is a compiled module. dishka is pure Python.
Doing "one session per request". Here the order is reversed: 2462 ns for
dishka against 5583 for ContextLocalSingleton with a reset. That is 2.3× in
favour of the uncompiled library: the compiled one has to reach the same result
the long way round. The caveat is the same as in the paragraph below, only
pointing the other way: within those 2462 ns dishka also closes the session,
while reset() merely drops the reference. The slower one here is the one
doing more.
Everything else in the measurement must not be compared row to row. Factory
assembles the graph in 1840 ns against dishka's 2462 — and opens no request
boundary, closes no session, and hands out two instead of one. "Faster" here
means "did less".
Worth its own line is the price of the boundary itself: entering and leaving a scope without a single resolve costs 785 ns out of 2462. Almost a third of a request's cost is not assembly but lifetime.
And the startup price: make_container takes 969 µs against 170 µs for a
DeclarativeContainer. At application startup that means nothing; in a test
suite that builds a container per test it means something. What it buys is a
walk over the whole graph.
How to choose
The question is not speed — the numbers above show that the answer to that depends on which operation your application performs more often. The question is whether you need the request boundary as a concept of the language.
You do, if a single request has more than one consumer of a shared resource:
two repositories with one session are enough for Factory to become a source
of silent bugs and for ContextLocalSingleton to become an obligation not to
forget reset() in every branch. Then dishka closes the question with a
mechanism rather than with discipline.
You do not, if your container is a registry of singletons: configuration, a
pool, clients for external services, everything living as long as the process.
Then dependency-injector does exactly what is needed and does it faster — plus
it has a Configuration provider that reads YAML, INI and environment
variables.
What is worth avoiding in either case is choosing by the benchmarks in a README. Two hundred nanoseconds on a resolve are invisible in any web application: next to a single database round trip that is zero. The difference that is visible is the other one — where two writes ended up in different transactions.
Version history
| Version | Change | What this means for your code |
|---|---|---|
| 2004 | Martin Fowler publishes the text in which the technique gets its name: “we settled on the name Dependency Injection”. The same text names the three forms — constructor, setter and interface injection — and draws the line against a service locator: “every user of a service has a dependency to the locator”. | |
| dependency-injector 4.x | The library ships as a compiled extension (providers.abi3.so), and it shows in the price of the operation it exists for: handing out a ready object, 40 ns. The model stays as it was: providers, Provide[...] markers and wiring through wire(); there is no built-in hierarchy of lifespans in it. | |
| dishka 1.x | The model is built around lifespan: Scope.APP → REQUEST → ACTION → STEP, entered through a context manager, finalized in reverse creation order. A dependency is found by its type annotation, and therefore the whole graph is validated inside make_container — a wiring mistake is visible at startup rather than in production. | |
| checked on | Python 3.13.7, dishka 1.10.1, dependency-injector 4.49.1, mypy 2.3.1. The numbers belong to these builds, not to “the libraries”: in six months they need taking again. |
What measured this
The numbers in this article come from these scripts. Each one opens from here, together with the record of the run: what it was measured on, what came out, and with what spread.
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- A container does two jobs, and the second one usually goes unnamed:
- assemble the graph — build an object together with everything it needs, without listing that at every call site;
- draw the lifetime boundary — say that this session lives for exactly one request, is shared by everyone who asks for it during that request, and is closed on the way out.
- dishka builds its model around the second job: "Scope is a lifespan of a dependency", the boundary is a context manager, and a dependency is found by its type annotation.
- dependency-injector builds its model around the first: providers gathered into a container, with the request boundary drawn by you. Its
Factoryis documented outright — "Factory provider creates new objects" — and therefore hands two repositories of one request two different sessions, that is, two transactions. - What the measurements say (3.13.7, dishka 1.10.1, dependency-injector 4.49.1):
- on the same work the order is reversed: handing out a ready object — dependency-injector is 6 times faster (40 ns against 238); doing "one session per request" — dishka is 2.3 times faster (2462 ns against 5583);
- a wiring mistake is visible in dishka when the container is created, in dependency-injector on the first call — and a forgotten
wire()produces no error at all: the marker object arrives as the argument; - the common claim that dependency-injector is untyped does not survive a check: mypy catches a mismatch equally well in both. What it does not catch is something else — a substitution inside
Provide[...].
In fact
- What reduces coupling is dependency injection itself — the fact that an object receives its parts rather than creating them. No container is needed for that: passing them as constructor arguments is enough. A container solves two other jobs: it assembles the graph so the dependencies are not listed in every handler, and it draws the lifetime boundary. Fowler names it outright: an assembler “that populates a field in the lister class with an appropriate implementation”.
- Faster at the operation it exists for: handing out an object that already exists, 40 ns against dishka's 238, six times. On the job “one session per request” the order is reversed: 2462 ns for pure Python against 5583 for the compiled library, because the compiled one has to reach that result the long way round — through
ContextLocalSingletonand a hand-writtenreset(). - The opposite. Measured: two repositories of one request get TWO different sessions, and
users.session is orders.sessionis False. The documentation promises exactly this: “Factory injects the dependencies every time when creates a new object”. For one session per request you needContextLocalSingleton— and areset()that you call. - Checked with a run of mypy 2.3.1:
container.service()returnsService, and assigning the result to a variable of another type is caught — exactly as with dishka'scontainer.get(Service). The divergence is elsewhere: a substitution insideProvide[...]is invisible to both mypy and the container. A parameter declaredServiceand receiving aConfigpasses both checks. - That depends on the library, and the spread is wide. dishka fails inside
make_container— at startup. dependency-injector builds the container silently, and theTypeErrorarrives on the first call. And a forgottenwire()produces no error AT ALL: the handler receives the internalProvideobject as its argument, and the failure happens later, elsewhere, with a message that has nothing to do with wiring. - The mechanics differ at the foundation: dishka looks a factory up BY THE TYPE ANNOTATION and can therefore walk the whole graph at startup; dependency-injector looks it up BY A MARKER pointing at an attribute of a specific container, and therefore patches your functions on
wire(). From that follow the different failure behaviour, the different checkability and the different price of the request boundary. Taste has nothing to do with it.
By version
- 2004
- Martin Fowler publishes the text in which the technique gets its name: “we settled on the name Dependency Injection”. The same text names the three forms — constructor, setter and interface injection — and draws the line against a service locator: “every user of a service has a dependency to the locator”.<
- dependency-injector 4.x
- The library ships as a compiled extension (
providers.abi3.so), and it shows in the price of the operation it exists for: handing out a ready object, 40 ns. The model stays as it was: providers,Provide[...]markers and wiring throughwire(); there is no built-in hierarchy of lifespans in it.< - dishka 1.x
- The model is built around lifespan:
Scope.APP → REQUEST → ACTION → STEP, entered through a context manager, finalized in reverse creation order. A dependency is found by its type annotation, and therefore the whole graph is validated insidemake_container— a wiring mistake is visible at startup rather than in production.< - checked on
- Python 3.13.7, dishka 1.10.1, dependency-injector 4.49.1, mypy 2.3.1. The numbers belong to these builds, not to “the libraries”: in six months they need taking again.<
What is covered
- What a container actually does
- One session per request — or one each
- How a dependency is found
- How much code has to be rewritten if you change libraries
- When you find out it was wired wrongly
- What it costs
- How to choose
- Version history
- What measured this
Common misconceptions
A DI container is there to reduce coupling
What reduces coupling is dependency injection itself — the fact that an object receives its parts rather than creating them. No container is needed for that: passing them as constructor arguments is enough. A container solves two other jobs: it assembles the graph so the dependencies are not listed in every handler, and it draws the lifetime boundary. Fowler names it outright: an assembler “that populates a field in the lister class with an appropriate implementation”.
A compiled library is faster, therefore dependency-injector is faster
Faster at the operation it exists for: handing out an object that already exists, 40 ns against dishka's 238, six times. On the job “one session per request” the order is reversed: 2462 ns for pure Python against 5583 for the compiled library, because the compiled one has to reach that result the long way round — through ContextLocalSingleton and a hand-written reset().
dependency-injector's Factory is the equivalent of a request scope
The opposite. Measured: two repositories of one request get TWO different sessions, and users.session is orders.session is False. The documentation promises exactly this: “Factory injects the dependencies every time when creates a new object”. For one session per request you need ContextLocalSingleton — and a reset() that you call.
dependency-injector is untyped, so its errors only show up at run time
Checked with a run of mypy 2.3.1: container.service() returns Service, and assigning the result to a variable of another type is caught — exactly as with dishka's container.get(Service). The divergence is elsewhere: a substitution inside Provide[...] is invisible to both mypy and the container. A parameter declared Service and receiving a Config passes both checks.
If the container is wired wrongly the application simply will not start
That depends on the library, and the spread is wide. dishka fails inside make_container — at startup. dependency-injector builds the container silently, and the TypeError arrives on the first call. And a forgotten wire() produces no error AT ALL: the handler receives the internal Provide object as its argument, and the failure happens later, elsewhere, with a message that has nothing to do with wiring.
The difference between containers is a matter of taste; the mechanics are the same
The mechanics differ at the foundation: dishka looks a factory up BY THE TYPE ANNOTATION and can therefore walk the whole graph at startup; dependency-injector looks it up BY A MARKER pointing at an attribute of a specific container, and therefore patches your functions on wire(). From that follow the different failure behaviour, the different checkability and the different price of the request boundary. Taste has nothing to do with it.
Check yourself
Two repositories in one request, both needing a session. The provider is declared as providers.Factory(Session, pool=pool). How many sessions are there?
Sources & further reading
6 SOURCES
- Martin Fowler — Inversion of Control Containers and the Dependency Injection patternSource. The 23 January 2004 text in which the term appeared: «As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection». The forms of injection come from the same page: «There are three main styles of dependency injection. The names I'm using for them are Constructor Injection, Setter Injection, and Interface Injection», as does the definition of what a container is for: «The basic idea of the Dependency Injection is to have a separate object, an assembler, that populates a field in the lister class with an appropriate implementation for the finder interface». And the criterion separating injection from a locator: «The key difference is that with a Service Locator every user of a service has a dependency to the locator».https://martinfowler.com/articles/injection.html
- dishka — Key conceptsOfficial documentation. The definition the whole library is built around: «Scope is a lifespan of a dependency», and the standard ladder: «Standard scopes are (with some skipped): APP → REQUEST → ACTION → STEP». From the same page on the container — «Container is an object you use to get your dependencies» — and on a nested scope: «To enter a nested scope, you call it and use it as a context manager». The order of teardown is stated outright too: «The finalization of dependencies runs in reverse creation order».https://dishka.readthedocs.io/en/stable/concepts.html
- dependency-injector — Factory providerOfficial documentation. The promised behaviour of the main provider, from which the whole two-sessions section grows: «Factory provider creates new objects» and «Factory injects the dependencies every time when creates a new object». This is not an oversight in the library but its documented contract.https://python-dependency-injector.ets-labs.org/providers/factory.html
- dependency-injector — Resource providerOfficial documentation. The only provider with teardown: «Resource provider provides a component with initialization and shutdown». And its extent: «Resource initialization happens only once» — that is, application level rather than request level; for per-context instances the documentation points to a separate Context Local Resource.https://python-dependency-injector.ets-labs.org/providers/resource.html
- dependency-injector — WiringOfficial documentation. The mechanism that separates this library from dishka by how a dependency is found: «Wiring feature provides a way to inject container providers into the functions and methods», «Wiring marker specifies what dependency to inject, e.g. Provide[Container.bar]». And what happens on wiring: «When wiring is done functions and methods with the markers are patched to provide injections when called». Hence the behaviour when wire() is forgotten: there is nothing to patch, and the marker itself arrives as the argument.https://python-dependency-injector.ets-labs.org/wiring.html
- dishka — QuickstartOfficial documentation. The way the container is asked, from which the whole difference in checkability follows: the documentation examples show `container.get(APIClient)` and `request_container.get(Service)` — a dependency is requested by type, not by provider name. The same page declares a factory with a scope: `@provide(scope=Scope.REQUEST)`.https://dishka.readthedocs.io/en/stable/quickstart.html