Repository navigation
Releases: modern-python/modern-di
Release list
4.3.1
What's Changed
- ci: test Python 3.15 and 3.15t as supported versions by @lesnik512 in #736
Full Changelog: 4.3.0...4.3.1
4.3.0
4.3.0 adds provider.derive(fn) and fixes the scope of providers written inline in a Factory's kwargs. The scope fix changes behaviour in ways that raise no error, so read "Changed" before upgrading.
Added
provider.derive(fn)returns a provider offn(value), wherevalueis whatproviderresolves to. Use it to pass one field of a settings object without writing a creator function for the component. Type checkers inferfn's argument from the provider. A derived provider is never cached, takes its scope from its source and is not registered under any type, so an override of the source reaches it. See Derived values. (#733)
class Dependencies(Group):
settings = providers.Factory(Settings, cache=True)
database = providers.Factory(Database, kwargs={"dsn": settings.derive(lambda s: s.db_dsn)}, cache=True)Changed
A provider written inline in a Factory's kwargs without its own scope= used to resolve at APP scope whatever the scope of the provider using it. It now takes the shallowest scope among the providers that use it, fixed when the container registers them. (#734)
- An inline
Factory(..., cache=True)under a REQUEST-scoped parent used to be one instance shared by every request. It is now one instance per request. To keep a single app-wide instance, passscope=Scope.APPto the inline provider. - An inline provider over a request-scoped source used to fail
validate()withInvalidScopeDependencyError. It now validates and resolves. - An inline
ContextProvidernow reads the context at its parent's scope instead of raisingContextValueNotSetErrorfrom the APP container. - A module-level provider without
scope=, used inline, now takes its parents' scope. If it must be APP-scoped, passscope=Scope.APP. - A group declared after registration that would change an inline provider's scope raises
ProviderScopeFrozenError, as it already did for class attributes.
See Inline providers.
Docs
- The integration guide and the starlette, fastapi, fastmcp, litestar and faststream pages say a child container's close is shielded from cancellation and abandoned with a warning after 10 seconds. (#728, #729, #731, #735)
What's Changed
- docs: say how an integration guarantees release after a cancelled child close by @lesnik512 in #728
- docs: say the starlette and fastapi child close is shielded and bounded by @lesnik512 in #729
- docs: say the fastmcp, litestar and faststream child close is shielded and bounded by @lesnik512 in #731
- feat: add provider.derive(fn) to inject a value computed from another provider by @lesnik512 in #733
- docs: describe the shielded, bounded child close in the integration guide by @lesnik512 in #735
- fix: give inline providers in Factory kwargs the scope of the providers using them by @lesnik512 in #734
Full Changelog: 4.2.2...4.3.0
4.2.2
4.2.2 changes how a close walks its finalizer queue, which makes the per-request close faster than in 4.2.1 and 4.2.0. There are no API changes.
Changed
- A cached instance that finishes building on another thread while a close runs is now finalized by that close. In 4.2.1 it waited for the next close, and before 4.2.1 it was never finalized. One that finishes after the close returns is still finalized by the next close. (#727)
- A close now pops its queue newest first until it is empty, instead of copying it as 4.2.1 did. A request that closes one cached item takes about 1.07 µs in the G7b benchmark, against 1.16 µs on 4.2.1 and 1.11 µs on 4.2.0. With 10 cached items per request it is 1-3% slower than 4.2.0. (#727)
A cancelled or interrupted close keeps the documented behavior: everything not yet finalized, the interrupted item included, stays queued for the next close.
What's Changed
- fix: finalize a build that finishes during a close in that close by @lesnik512 in #727
Full Changelog: 4.2.1...4.2.2
4.2.1
4.2.1 fixes three bugs: a finalizer that never ran, an optional dependency that was injected as None, and a generic class creator that wired nothing. There are no API changes.
Fixed
- A cached instance whose creator finishes on another thread while the container is closing is now finalized by the next close, as the lifecycle docs say. Before, the close dropped it from the finalizer queue: it stayed cached, no later close or reopen finalized it, and whatever it held leaked. A close with cached items now copies its queue, which costs about 40 ns per close. (#719, #726)
- A creator parameter annotated
Annotated[X, ...] | None(or a union of annotated types) and read from the creator's__signature__, as for Pydantic models and msgspecStructs, is now resolved fromX's provider. Before, it was injected asNoneeven withXregistered, and a union of two annotated members failedvalidate(). A common case is the Pydantic fieldAnnotated[X, Field(...)] | None = None. (#720, #724) Factory(Repo[int]), with a parameterized generic class as the creator, now wiresRepo's constructor parameters by type, is checked byvalidate(), and is bound toRepo. CallingRepo[int]still builds the instance. Before, it wired nothing, had no bound type, passedvalidate(), and failed on first resolve with a rawTypeError. Two parameterizations of one class now share the bound typeRepo, so registering both needsbound_type=on them. (#722, #725)
What's Changed
- docs: add shutdown-order tests to the integration checklist by @lesnik512 in #707
- docs: regenerate the performance tables after the per-factory code change by @lesnik512 in #708
- docs: state that the performance tables were measured on 4.2.0 by @lesnik512 in #709
- docs: explain that Litestar closes the request container before a stream body runs by @lesnik512 in #710
- docs: note that modern-di-faststream 4.2.2 drops the request container context key by @lesnik512 in #711
- docs: say Celery tasks resolve from their own app's container by @lesnik512 in #712
- docs: plainer prose in the benchmarks README and AGENTS.md by @lesnik512 in #713
- docs: replace emoji link headings with a Links section (RM3) by @lesnik512 in #714
- docs: say a cancelled sync stream on the grpc aio server is never closed by @lesnik512 in #715
- docs: match AGENTS.md to the standard's canonical justfile sentence by @lesnik512 in #716
- docs: say aiogram error handlers get their own child container by @lesnik512 in #718
- docs: use colons in place of em dashes in gloss bullets by @lesnik512 in #717
- fix: wire an Annotated union member read from a creator's signature by @lesnik512 in #724
- fix: wire a parameterized generic class creator from its origin by @lesnik512 in #725
- fix: finalize a cached instance built during a close at the next close by @lesnik512 in #726
Full Changelog: 4.2.0...4.2.1
4.2.0
4.2.0 lets several apps share one container, rejects async creators where they are declared, and makes validate() and several error messages more accurate. Resolving a chain of factories is about 22% faster. Python 3.11 or newer is required, and modern-di still has no runtime dependencies.
Changes that can affect existing code
Factory(async_fn)raisesAsyncCreatorError(#680). It is raised from theFactory(...)call, for anasync deffunction or method, afunctools.partialaround one, and an instance withasync def __call__. Before, the declaration was accepted andresolve()returned an un-awaited coroutine. Build async resources in the framework's lifespan instead; see Async resources via lifespan.validate()checks reference-only providers (#686). A provider withbound_type=None, or withskip_creator_parsing=Trueand nobound_type, used to be checked only when a typed provider depended on it. A broken one now failsvalidate()withValidationFailedErrorinstead of failing on first resolve.ResolutionStep.scopecan beNone(#693). An alias step whose source is not registered has no known scope. Error chains draw it as?. Code that reads.dependency_path[i].scopeorCircularDependencyError.steps[i].scopemust handleNone.- Error message text changed for
InvalidChildScopeError(#682),DuplicateProviderTypeError(#688) andAliasSourceNotRegisteredError(#692). Code that matches the old text must change.
Fixed
- Registering the same provider object more than once, for example from two groups, registers it once instead of raising
DuplicateProviderTypeError. This also lets several apps share one container: callingsetup_dion a second app with the same container no longer raises in the starlette, fastapi, aiohttp, litestar, flask, faststream, aiogram and taskiq integrations. The container is not reference-counted, so whichever app stops first closes it for the others; give it one owner, or use one container per app when apps stop independently. See Sharing a container. (#681) - A creator whose parameter types are only on its
__signature__, such as a Pydantic model or a msgspecStruct, is now wired. Before, resolving it raisedArgumentResolutionError, and a function creator also lost its return type and was left out of by-type registration. (#685) - A provider whose bound type is a falsy class, for example one whose metaclass defines
__len__returning 0, is registered by type and named correctly in errors. (#687) repr(alias)no longer reports a scope the alias doesn't have, and "did you mean" hints show an alias's effective scope. (#683)
Improvements
DuplicateProviderTypeErrornames each provider by its group attribute and call site, such asDependencies.first: Factory (myapp.deps:12), so two factories with the same creator are told apart. (#688)AliasSourceNotRegisteredErrornames the reference-only provider when the source type is only served withbound_type=None, and says how to fix it. (#692)InvalidChildScopeErrorstates the value rule instead of listing no scopes when the parent is the deepest built-in scope. (#682)
Performance
- Each factory gets its own copy of its resolver code, so CPython's call-site caches stop missing between factories. A six-deep chain resolves 22% faster and a ten-wide one 22% faster. The first resolve of a cold container is about 35% slower, about 6 µs. (#694)
CacheItemis a plain slotted class. A request cycle is about 7% faster and closing a container with finalizers about 11% faster. (#690)
Docs
- Most pages were reworked, including the lifecycle, factories, recipes, troubleshooting and integration pages (#651 to #675).
- New recipe: Sharing a container, with
manage_lifespan=Falseand one owner (#700, #702, #703). - The integration pages describe each plugin's current shutdown order: the root opens before user startup code and closes after user shutdown code (#704, #705, #706).
- Every docs page runs in the test suite against the released integration packages (#654, #658).
What's Changed
- docs: point integration scope-hierarchy links at the scope table by @lesnik512 in #651
- test: run the writing-integrations examples in test-docs by @lesnik512 in #652
- docs: rework the lifecycle page: finalizer forms, close order, and which integrations close the root by @lesnik512 in #653
- docs: say every release from 2.15.0 on has a curated body by @lesnik512 in #655
- test: run the framework integration pages against their packages by @lesnik512 in #654
- docs: rework the factories page: kwargs errors, the skip-parsing warning, and the validate() gap by @lesnik512 in #657
- test: run every docs page in the integration env and drop the last skip markers by @lesnik512 in #658
- docs: declare Litestar handler dependencies with NamedDependency by @lesnik512 in #660
- docs: rework the providers: context, container, alias, advanced api pages by @lesnik512 in #661
- docs: rework the pytest and writing an integration pages by @lesnik512 in #662
- docs: rework the wiring, alias, group and kwargs troubleshooting pages by @lesnik512 in #663
- docs: rework the errors catalog and the scope, container and lifecycle troubleshooting pages by @lesnik512 in #664
- docs: rework the web integrations pages by @lesnik512 in #665
- docs: rework the migration guides and dev pages by @lesnik512 in #666
- docs: rework the task, bot, rpc and cli integrations pages by @lesnik512 in #667
- docs: rework the recipes pages by @lesnik512 in #668
- docs: fix cross-page follow-ups from the docs rework by @lesnik512 in #675
- fix: make _DependencyChainError._message abstract for ty 0.0.87 by @lesnik512 in #677
- fix: drop ty ignore that ty 0.0.88 reports as unused by @lesnik512 in #679
- fix: reject an async creator when a Factory is declared by @lesnik512 in #680
- fix: register a repeated provider object once instead of raising by @lesnik512 in #681
- fix: state the value rule in InvalidChildScopeError instead of an empty list by @lesnik512 in #682
- fix: validate() checks reference-only providers by @lesnik512 in #686
- fix: stop Alias repr from reporting a scope it does not have by @lesnik512 in #683
- fix: name each provider in DuplicateProviderTypeError by its group attribute and call site by @lesnik512 in #688
- fix: wire parameters typed only on the creator's signature by @lesnik512 in #685
- fix: register and name a provider whose bound type is falsy by @lesnik512 in #687
- docs: state that close does not wait for other threads and a finalizer runs at most once by @lesnik512 in #689
- perf: build CacheItem as a plain slotted class by @lesnik512 in #690
- fix: name the reference-only provider when an alias source is unbound by @lesnik512 in #692
- fix: draw an unknown step scope as ? in error chains by @lesnik512 in #693
- docs: document the Litestar, Flask and Celery integration fixes by @lesnik512 in #684
- ci: list the installed modern-di packages before the integration docs run by @lesnik512 in #695
- perf: give each factory its own copy of its shape's resolver code by @lesnik512 in #694
- docs: align README and quickstart with the integration and pytest pages by @lesnik512 in #696
- docs: document the aiogram, aiohttp, Starlette, gRPC, FastStream, Typer, pytest and FastAPI integration fixes by @lesnik512 in #697
- docs: call modern-di-pytest an integration and note mounted apps in writing-integrations by @lesnik512 in #698
- docs: mounted apps with middleware are found since starlette 4.1.1 and fastapi 4.0.2 by @lesnik512 in #699
- docs: warn that apps sharing a root container close it for each other by @lesnik512 in #700
- docs: share a container with manage_lifespan=False and one owner by @lesnik512 in #702
- docs: FastStream start() and stop() skip the app lifespan by @lesnik512 in #703
- docs: ...
4.1.0
4.1.0 rejects two declarations that never worked, makes two error messages more useful, and reworks most of the docs. Python 3.11 or newer is required, as in 4.0, and modern-di still has no runtime dependencies.
Three changes can break code that ran on 4.0.0. The first two only affect declarations that already failed at first use; they now fail at import, with a clearer error. Each has a section in the migration guide.
Breaking changes
Aliasrequiresbound_type(#646, #650).Alias(X)raisesTypeError, and type checkers flag it.Alias(X, bound_type=X)raises the newAliasBoundToSourceError. On 4.0.0 both registered the alias under its own source type, so it resolved to itself:validate()andresolve()reported aCircularDependencyError, or registration raisedDuplicateProviderTypeErrorwhen a provider forXexisted. Pass the type the alias answers for, orbound_type=Noneto use it by reference only. Migration.- Provider subclasses fail when the class is defined (#631). Subclassing
Factory,Aliasor another provider outside modern-di raisesTypeErrorat import. On 4.0.0 aFactorysubclass skipped validation and failed at its first resolve with a bareTypeError. Compose behaviour in a creator function or anAliasinstead. FinalizerErrorhas a new message (#631). It is one line naming each kind of error with its count, such asContainer.close_sync() found 2 finalizer error(s): ConnectionError (1), ValueError (1). Code that matched the oldErrors during sync cleanup: [...]text must change. Each finalizer exception also gets a note naming the type of the cached instance whose finalizer raised it. Migration.
Improvements
DuplicateProviderTypeErrornames both providers, one line each, and exposes them as.first_providerand.second_providerin registration order (#631).AliasBoundToSourceErrorhas its own troubleshooting page (#646).
Docs
- The quickstart is built around a working app (#647), and the scopes page now covers caching, skipped scopes and the dependency rule (#649).
- The comparison (#639), performance (#638, #636) and design decisions (#640) pages are rewritten.
- New: a FastMCP integration page (#637) and a Pydantic AI recipe (#634).
- Troubleshooting pages quote errors the way Python prints them (#648), and the advice on
resolveversusresolve_provideris corrected (#643). - Every Python example in the docs runs in the test suite (#644).
4.0.0
4.0 is a breaking release. It makes thread safety, closing a container and missing context values behave the same way everywhere, and it shrinks the public surface to what is documented. Read the migration guide before upgrading. Every change below that can break code has a section there with before and after examples.
Python 3.11 or newer is required, as in 3.6. modern-di still has no runtime dependencies.
Highlights
- Each cached factory has its own lock.
Container(use_lock=)is removed and every container locks a cache miss. A cache hit takes no lock. On a miss, the factory's dependencies are built under its lock, so threads that miss together build them once. Creators of different cached factories can now run at the same time (#561, #597). - A closed container stays closed. Resolving from it raises
ContainerClosedErrorinstead of warning and reopening it. The container counts as closed while its finalizers run, and a finalizer that raises drops its cached instance. Closing a root container keeps its overrides (#555, #584, #586). - Context values are required unless you say otherwise. A missing value raises
ContextValueNotSetErrornaming the parameter.ContextProvider(T, default=X)is new, and a nullable or defaulted parameter falls back when no value is set. The container copies thecontext=dict, soset_context()no longer leaks between containers (#557, #588, #602). - Scopes match by enum member. A provider resolves only in a container built at the same member. A custom scope that shares an integer value with
Scope.SESSIONno longer resolves in aScope.SESSIONcontainer (#596). FinalizerErrorandValidationFailedErrorareExceptionGroups, soexcept*catches the errors inside them by type (#583, #607).- More creators wire.
NewTypeandtype X = ...annotations are bound types of their own, and classes whose signature comes from__new__, such asNamedTuplesubclasses, wire their parameters. Type checkers accept aNewTypeor type alias asbound_type=(#591, #625).
Breaking changes
Each item links to its migration guide section.
- Container
- Every argument after
scopeis keyword-only, anduse_lock=andparent_container=are removed. - The registries are private; use
find_provider().closed,scopeandparent_containerare read-only. - Resolving on a closed container raises, even for an unregistered type, and closing a root keeps overrides.
- Every argument after
- Context: a missing value raises
ContextValueNotSetError,fetch_context_value()is removed, andcontext=is copied. - Scopes match by enum member.
- Exceptions
- Scope and closed-container errors are also
ResolutionErrors. FinalizerErrorandValidationFailedErrorareExceptionGroups; read.exceptions.- Attributes are renamed across the exception classes, for example
ArgumentResolutionError.arg_nameto.parameter_nameandProviderNotRegisteredError.provider_typeto.dependency_type(list, more). DependencyPathMixinis removed.
- Scope and closed-container errors are also
- Providers
Factory(cache=)takes only a bool or aCacheSettings.- Provider attributes are read-only and
CacheSettingsis frozen. - Undocumented provider methods are private, and
AbstractProvideris a plain class. - A group that already binds a
NewTypeor alias now raisesDuplicateProviderTypeError.
- Only four modules are public:
modern_di,modern_di.providers,modern_di.exceptionsandmodern_di.integrations. - Typing:
ContextProvider(T, default=None)isContextProvider[T | None], andCacheSettingsis contravariant. - The 3.x deprecations are removed, including
Container(validate=)and themodern_di.exceptions.warningsmodule.
Fixes
- Every modern-di exception can be pickled and copied (#600).
- Error chains name the right argument and provider (#587).
ScopeSkippedErrornames the root of the chain it was raised in (#607).validate()no longer reports a scope error for a provider that takes theContaineritself (#620).- The wheel and sdist ship
LICENSE(#624).
Performance
Against 3.x on the published benchmarks (Apple M2, CPython 3.14.7), a request lifecycle (C4) went from 2.29 µs to 1.85 µs, and a request with a context value (C6) from 1.45 µs to 806 ns. Warm resolves got 3 to 4% faster. See Performance for the comparison with the other containers.
#626, #628 and #629 landed after that publication and target cold paths. Against the commit before each: building and validating a container 25% faster, a first resolve 12 to 15% faster, and a request cycle with a sync finalizer 8% faster. Warm resolves did not move.
Integrations
The framework integration packages get 4.0-compatible releases after this one (#579).
Full changelog: 3.6.0...4.0.0
3.6.0
Breaking
- Python 3.10 is no longer supported.
requires-pythonis now>=3.11.
What's Changed
- test: report a bare docs/adr/NNNN citation as unresolved by @lesnik512 in #499
- docs(performance): name the release the snapshot ships in by @lesnik512 in #500
- docs: restore the agent artifacts to the setup skill's templates by @lesnik512 in #501
- ci: run the scheduled dependency check daily by @lesnik512 in #502
- docs: point agents at the humanizer skill for prose by @lesnik512 in #515
- docs: de-market introduction/comparison.md by @lesnik512 in #516
- docs: rewrite introduction/about-di.md by @lesnik512 in #517
- docs: normalize the troubleshooting page template by @lesnik512 in #518
- docs: editorial pass on providers pages by @lesnik512 in #523
- docs(introduction): editorial pass on the remaining pages and the quickstart by @lesnik512 in #519
- docs: editorial pass on recipes/ and dev/ by @lesnik512 in #520
- docs: editorial pass on the migration guides by @lesnik512 in #522
- docs: mechanical consistency pass on integration pages by @lesnik512 in #521
- docs: editorial pass on task, bot, RPC, CLI and testing integrations by @lesnik512 in #524
- docs: editorial pass on troubleshooting/ prose by @lesnik512 in #525
- docs: editorial pass on the web integration pages and the spec by @lesnik512 in #526
- docs: align link text, page titles and nav labels with their targets by @lesnik512 in #527
- docs: replace emoji contrast markers in the recipes code comments by @lesnik512 in #528
- docs: record the docs style conventions and drop the last emoji marker by @lesnik512 in #529
- docs: drop the mid-sentence em dashes from README.md and AGENTS.md by @lesnik512 in #530
- docs: drop the mid-sentence em dashes from comparison.md by @lesnik512 in #531
- chore: align with the org standard (MD3, FL2) by @lesnik512 in #532
- chore: give every coverage pragma a reason (TS6) by @lesnik512 in #533
- perf(compiler): inline the cross-scope hop into the generated resolver by @lesnik512 in #540
- ci: fetch the ADR citation check from .github instead of keeping a copy by @lesnik512 in #539
- perf(container): share the root lock across the container tree by @lesnik512 in #541
- perf(cache): skip the coroutine for finalizer-less items in close_async by @lesnik512 in #542
- docs(performance): republish the comparative tables after #540, #541 and #542 by @lesnik512 in #543
- refactor(graph): move the validation walk into dependency_graph.collect_errors by @lesnik512 in #544
- refactor(factory): name the parsed creator and the cache coercion by @lesnik512 in #545
- docs(agents): run just install before just lint by @lesnik512 in #546
- docs: fix factual errors found in review by @lesnik512 in #547
- fix(errors): drop skip_creator_parsing advice for positional-only params by @lesnik512 in #548
- fix(errors): drop skip_creator_parsing advice for parameterized generic params by @lesnik512 in #549
- docs: plain-prose pass over README and published docs by @lesnik512 in #550
- docs: match the C3 dependency-injector ratio to the table by @lesnik512 in #551
- feat!: drop Python 3.10 by @lesnik512 in #552
Full Changelog: 3.5.0...3.6.0
3.5.0
modern-di 3.5.0 — the resolve hot path is generated source
A Factory resolver is now generated from a source template specialised to the factory's
resolver shape and exec'd with the factory's constants as its globals, replacing the seven
hand-copied closures the compiler used to hold. Overrides are compiled in with it: an override
change drops the compiled resolvers and the next resolve recompiles, so nothing on the resolve
path consults the overrides registry. Warm paths get cheaper across the board, cold first-resolve
pays about 2 µs per provider.
One user-visible fix goes with it: a scope violation found by validate() now draws the chain
that reaches the offending provider instead of naming only the declared dependency. No public
name moved. modern_di.exceptions became a package and re-exports every name it exported before.
Performance
Guard-tier scenarios on CPython 3.14, medians, each measured against main on the same machine
with both halves of the change shipped together.
| Scenario | |
|---|---|
G16/G17 by-type resolve(T) |
−23% |
| G12 chain resolved with an unrelated override active | −30% |
| G4 wide fan-out | −19% |
| G9 context parameter | −16% |
| G18 alias hop | −12% |
| G2 cached resolve | −11% |
| G1 transient resolve | −10% |
| G5 cross-scope | −7% |
| G7 request lifecycle | −6% |
| G3 deep chain | −3% |
| G8 cold first-resolve | +12% |
| G8b cold cached | +1% |
-
One code object per resolver shape, shared by every factory of that shape. Unrolling every
arity removes the intermediate list and theCALL_FUNCTION_EXunpack, which is what pays for the
wide fan-out.Container.resolvememoizes type to resolver directly, which is most of the by-type
number. -
The override front-guard is gone.
has_overrides/fetch_overrideused to run at the top of
every resolver on every resolve so that a test could swap a value under a compiled graph. The
registry already dropped every compiled resolver on mutation, so treating an override as a mutation
reuses that path and removes the guard from all five resolver kinds. That is the −30% on G12, the
path every test usingoverride()takes. -
The duplication could not be removed with functions, and that was measured before this was
written. A folded arity ladder costs +31/+26/+35% on transient/chain/wide, because on CPython
3.12+ a closure's size is paid on every call including the branches never taken. A sharedbuild()
helper costs +66/+47/+47%, one frame per node; adding a sharedcall()makes it +79/+62/+58%.
One code object per provider is 20-30% faster than per shape on chains, since call sites stay
monomorphic, but costs about 70 µs ofcompile()per provider, which a suite building a container
per test pays per test. It stays available as a later opt-in. -
Cold first-resolve is about 2 µs per provider slower, once per registry, and an override change
recompiles lazily at about 2-5 µs per provider actually resolved afterwards.
Reasoning and the full rejected-design table:
docs/adr/0001-resolver-hot-path-generated-source.md.
Fixes
-
InvalidScopeDependencyErrordraws the redirect chain. It named the declared dependency beside
the terminal's scope and discarded the hops between them, so a violation reached through anAlias
never named the provider whose scope was actually at fault, neither in the message nor in the
attributes. It now renders through the same chain renderer as its three siblings.New
.dep_chain, running from the declared dependency to the provider that supplies it;
.dep_provideris still there, now a property returning the first hop, and.dep_terminalthe
last. The two differ only when the dependency is reached through anAlias, which is exactly the
case where the declared type alone cannot tell a reader who owns the offending scope. The
constructor takesdep_chain=in place ofdep_provider=/dep_scope=, and the message text
changed, so anything matching on it needs a second look.Each redirect hop now draws at the scope it resolves at rather than the
Scope.APPdefault an
Aliasreports, which also removes that fiction fromCircularDependencyError.
What to know before upgrading
-
Tracebacks through a creator show generated source. It is registered in
linecacheper shape,
so a creator's exception shows the resolver's line rather than an opaque frame, and
__qualname__isresolve[<display name>]for profilers. -
modern_di.exceptionsis a package. Each error family gets its own submodule over a shared
base.from modern_di import exceptionsandfrom modern_di.exceptions import Xare unchanged:
all 33 public classes andSUGGESTION_HEADERare re-exported, now listed in an explicit
__all__, and the class bodies were a verbatim move with byte-identical ASTs.__module__now
names the family submodule, so it reads that way in tracebacks and in anything matching on a
module path. -
Container.overrideis a test-time operation. It is not coordinated with concurrent resolves
on other threads, which its docstring now states. Overriding drops compiled resolvers; a reset that
changes nothing drops nothing, soclose()on a root does not churn the memo.
What did not change
Every public name, the resolution semantics, sync-only resolution, caching, scope transparency,
validation, and every error type, message and breadcrumb other than InvalidScopeDependencyError
above. All override behaviours (handles, nesting, prior restore, propagation through an alias,
scope bypass) pass unchanged, and the one-resolver-frame-per-node invariant holds by construction,
since generated functions have no free variables at all.
Internals
- 547 tests, 100% line coverage, Python 3.10-3.14 including free-threaded 3.14t;
ruffandty
clean. The 100% gate now measures the generator, not the generated resolver, which coverage
cannot see;test_every_resolver_shape_compiles_and_resolvesenumerates every shape so a
template / namespace mismatch is a test failure rather than aNameErrorin your application. - Three seams moved so data stops leaving its owner:
Factory.wiring_plan(registry)replaces
Factory._plan(container),Factory.can_call_positionally(plan)replaces the compiler-level
helper, andAbstractProvider.mark_registered()replaces the registry writing_registered
directly.Alias.find_sourceandFactory.resolution_steplost their underscore, matching the
public siblings they sit beside. - A benchmark covering concurrent first-resolve in sibling children was added, since the compile
step is now the thing two threads can race into.
Docs
- The performance page was republished against the template resolver, measured at #470's merge on
an Apple M2 / CPython 3.14.7 against dishka 1.10.1, dependency-injector 4.49.1, that-depends
4.1.0 and wireup 2.12.0. By-reference and by-type columns are now within run-to-run noise of
each other. - The ADR set was compressed and the surviving decisions moved into the docs and the code; the
integration pages now name the missing-containerRuntimeError, the FastStream broker created
in anon_startuphook,@injecton StarletteHTTPEndpoint/WebSocketEndpointmethods,
aiohttpweb.Viewmethods, and typer's container lookup. Those fixes ship in the integration
packages, which release separately.
Downstream
No action needed. No public API changed and integrations pick the speedups up on upgrade.
Look only if you construct InvalidScopeDependencyError yourself, match on its message, or read
__module__ off a modern-di exception class.
Full changelog: 3.4.0...3.5.0
3.4.0
modern-di 3.4.0 — one field per question on the scope path
A provider's scope is now derived from what was actually declared instead of being
collapsed at construction and reconstructed afterwards. That is an internal change with
no effect on which scope any provider resolves at, but two small pieces of surface go
with it — see Breaking changes before upgrading if you touch ContextProvider
internals or assign to provider.scope. Resolving a ContextProvider directly also got
16% faster.
Internal refactors
-
AbstractProviderderives its scope rather than storing it. The constructor used
to collapse an omittedscope=toScope.APPon its first line, which erased why a
provider was APP-scoped — precisely the fact the precedence rule needs (explicit
scope=beats aGroupdefault beatsScope.APP). A companion flag existed only to
carry that erased bit back, and had to stay in sync with the field recording which
group had stamped the provider.The sources are kept instead:
_explicit_scopeholds whatscope=gave (Nonewhen
omitted) and_group_claimholds(scope, group name)once aGroupstamps it.
scopeis a property whose body is the documented precedence list line for line. Each
field answers exactly one question, and the effective scope cannot drift from its
provenance because it is computed from it. Slot count is unchanged.Every group-scope precedence, conflict and freeze test passes untouched;
GroupScopeConflictErrorandProviderScopeFrozenErrorfire on exactly the same
inputs as before. -
DependencyPathMixindrops its empty__slots__. The line asserted the class had
no instance attributes while its own__init__set two, whichty0.0.74 began
reporting. Removing it lets the mixing-in classes' declarations stand on their own and
leaves__dict__empty after__init__, at the cost of 16 bytes per instance on the
nine mixin users, on the error path only. No behavior change.
Performance
Measured on an Apple M2 / CPython 3.14.7, min of 11 timing repeats, averaged across
three separate processes.
| Path | 3.3.0 | 3.4.0 | |
|---|---|---|---|
direct ContextProvider resolve |
194.3 ns | 161.6 ns | −16.8% |
-
The compiled
ContextProviderresolver inlines its lookup. It used to delegate to
ContextProvider.resolve, which calledfetch_context_value, which read
provider.scopeon every resolve. The compiled closure now performs the override
guard, scope hop, reopen check and context read itself, with the scope read once at
compile time — the same shape the folded context kwargs have used since 3.3.0, and
consistent with the contract that a registeredContextProvider's scope and
context_typeare fixed. Two delegated frames go with it, which is where most of the
gain comes from.This path is what the
Annotatedmarker injectors hit when a handler asks for a
context type directly, once per marker per request. -
The guard tier is unchanged. All 25 scenarios sit inside run-to-run noise; the
control scenario moved further than any real one. No guard scenario covers a direct
ContextProviderresolve, which is why the number above is measured separately.
Breaking changes
Both are removals of machinery that was never a designed extension point, in the same
class as the provider-type closure shipped in 2.29.0.
-
ContextProvider.resolve(container)is removed. Its only caller was the compiled
resolver branch above, and the polymorphicprovider.resolve(self)dispatch it
belonged to was retired in 2.29.0
(decision).
Nothing in the docs referenced it. Resolve through the container
(container.resolve(SomeType)orcontainer.resolve_provider(provider)), which is
unchanged and raises the sameContextValueNotSetErrorwith the same message.ContextProvider.fetch_context_value(container)is unaffected. Public since
2.18.0, it remains the way to read a context value without raising, returningUNSET
when nothing is set. It now has direct tests covering the same-scope read, the
cross-scope hop, the closed-owner reopen, and the absent value. -
provider.scopeis read-only. It is now a property, so assigning to it raises
AttributeErrorwhere it previously silently succeeded. Reading is unchanged
everywhere, including before any container exists (MyGroup.svc.scopestill reflects
a group default at class-creation time). Mutating a registered provider's scope was
already unsupported: the group path has raisedProviderScopeFrozenErrorsince 2.x,
and 3.3.0 stated the contract that aContextProvider's identity is fixed once
something resolves through it. Set the scope at construction (scope=) or via the
Groupdefault; to change it, construct a second provider.
What did not change
Which scope any provider resolves at, the precedence rule, the freeze, every error type,
message and breadcrumb, and the Alias / container_provider opt-out from group
stamping. Resolution stays sync-only. No public name other than the two above moved, and
no documented statement in docs/providers/scopes.md needed editing.
Docs
docs/architecture/is gone. The living truth about behaviour is the code and its
INVARIANT:-marked tests, and a behaviour change is reviewed with the diff rather than
promoted to a prose page; the invariant census test now enforces that every citation
pointing at a test resolves to a real one.docs/introduction/performance.md's comparative table was republished at 3.3.0, which
it had lagged. It does not yet include this release, whose only measured change is on a
path that table does not cover.
Internals
- 514 tests, 100% line coverage, Python 3.10–3.14 including free-threaded 3.14t;
ruff
andtyclean. - One invariant test added for the compile-time scope capture, carrying a positive
control so it cannot pass vacuously, plus two for the group-default opt-outs onAlias
andcontainer_provider— both written and confirmed passing before the refactor, so
they characterise existing behaviour rather than the new code.
Downstream
No action needed for any integration that resolves through a Container. Check only
if you call ContextProvider.resolve directly or assign to provider.scope; neither
appears in any sibling integration.