Skip to content

3.1.1

Choose a tag to compare

@github-actions github-actions released this 27 Jul 19:14
3.1.1
f373fa1

modern-di 3.1.1 — containers stop being garbage the refcounter can't free

A single-line fix in Container.__init__ with a disproportionate effect. Every
container stored itself inside its own scope map, which made it a reference
cycle: no container — root or child — could be freed by reference counting, so
each one waited for a generational GC pass. An application that builds a
container per request was handing the collector work at its request rate.

No API changes. No integration needs updating.

Fix

  • Containers are no longer reference cycles. Container.__init__ seeded
    _scope_map with {scope: self}, so the container referenced a dict that
    referenced the container. The map is now seeded from the parent instead — it
    holds ancestors only. find_container already returned self for its own
    scope before consulting the map, so the self-entry was never read and
    resolution is unaffected.

    Measured on an Apple M4 / CPython 3.14.6:

    before after
    100 closed REQUEST children 792 objects reclaimable only by the GC 0
    100 closed root containers 2300 0
    100 full APP→SESSION→REQUEST→ACTION→STEP chains 5500 0
    build a child and drop it, GC enabled ~849 ns ~542 ns

    Isolated construction is unchanged within noise (~500 vs ~506 ns with the GC
    off); the gain is the collector no longer reclaiming what reference counting
    now frees. On the comparative request-lifecycle benchmark the median fell from
    232.7 µs to 194.8 µs per 100-request batch, and the standard deviation from
    123.0 µs to 7.9 µs — the tail that scenario carried was GC, and it is gone.

Behavior change

  • Container.scope_map no longer contains the container itself. A root's
    map is now empty and a child's holds only its ancestors. The property is
    documented as an internal surface with no stability guarantee and already
    emits a DeprecationWarning; a survey of all 12 sibling modern-di-*
    integration repositories found no references to it. find_container is
    unaffected on both its own-scope and ancestor paths.

Internal

  • The benchmark suite was rebuilt for measurement fidelity. Nine defects
    found by audit, none of them affecting library code. The largest: the
    request-lifecycle scenarios entered the asyncio event loop once per timed
    iteration, and that entry costs ~27 µs regardless of the body — so ~93% of
    every published figure was a shared constant that compressed the real
    differences between frameworks toward parity. Scenarios are now batched, the
    per-framework setup asymmetries are levelled, iterations is pinned so no
    cell sits on the platform timer's ~42 ns grid, and the published comparison
    table is generated by just bench-report rather than hand-assembled.

    This changed a published claim: modern-di is not level with dishka on the
    request lifecycle, as the old numbers implied. See
    Performance.

  • The reference-cycle fix above was found while investigating why modern-di's
    request-lifecycle measurement was the least stable cell on that page.

Downstream

No action needed. No API changed, and no integration reads the affected
property.

Internals

  • 450 tests, 100% line coverage, Python 3.10–3.14 including the free-threaded
    3.14t build.