• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aio-libs / multidict / 36018545646
87%

Build:
DEFAULT BRANCH: master
Ran 24 Sep 2026 03:14PM UTC
Jobs 1
Files 43
Run time 1min
Badge
Embed ▾
README BADGES
x

If you need to use a raster PNG badge, change the '.svg' to '.png' in the link

Markdown

Textile

RDoc

HTML

Rst

24 Sep 2026 03:13PM UTC coverage: 87.672% (+0.04%) from 87.629%
36018545646

push

github

web-flow
Recycle the items iterator's result tuple (#1569)

<!-- Thank you for your contribution! -->

## What do these changes do?

The C items iterator built a fresh two-tuple on every step with
`PyTuple_Pack()`, and the caller freed it a moment later: the tuple
allocation, its deallocation and the freelist bookkeeping were about
220 of the 380 instructions a step of `for k, v in d.items()` cost,
against 88 for `dict`. The iterator now owns one result tuple, created
with it, and, when the caller has already dropped it (checked with
`PyUnstable_Object_IsUniquelyReferenced()`, which
`pythoncapi_compat.h` provides on every supported version), stores the
next pair into it and returns it again, the way
`dictiter_iternextitem()`
does. A tuple the caller still holds is left alone and a fresh one is
returned instead. The field is never replaced after creation: on the
free-threaded build the uniqueness check is only ever true for the
thread that owns the tuple, so two threads sharing one iterator either
reuse from the owner or allocate, and neither can free the tuple under
the other. The tuple is re-tracked by the collector when reused, since
the collector may have untracked it while it held only atomic items,
and on 3.14 and later its cached hash is reset.

`keys()` and `values()` iterators do not touch the new field. The
pooled iterator shells release the tuple before going back to the pool,
and `tp_traverse` and `tp_clear` cover it.

Only the C extension changes; the pure-Python iterator yields a fresh
tuple each step, and the new test passes on both.

Instruction counts per step of `for k, v in d.items()` over 200 items,
`benchmarks/callgrind_driver.py`, CPython 3.14.7, `PYTHONHASHSEED=0`:

| `for k, v in d.items()` | before | after | | `dict` |
| --- | ---: | ---: | ---: | ---: |
| `MultiDict`, GIL | 378 | 132 | -65% | 88 |
| `CIMultiDict`, GIL | 382 | 136 | -64% | |
| `MultiDict`, free-threaded | 486 | 224 | -54% | 114 |
| `CIMultiDict`, free-threaded | 490 | 2... (continued)

777 of 1554 branches covered (50.0%)

Branch coverage included in aggregate %.

46 of 46 new or added lines in 2 files covered. (100.0%)

7693 of 8107 relevant lines covered (94.89%)

1.9 hits per line

Jobs
ID Job ID Ran Files Coverage
1 MyPy - 36018545646.1 24 Sep 2026 03:14PM UTC 86
87.67
GitHub Action Run
Source Files on build 36018545646
  • Tree
  • List 43
  • Changed 4
  • Source Changed 2
  • Coverage Changed 4
Coverage ∆ File Lines Relevant Covered Missed Hits/Line Branch Hits Branch Misses
  • Back to Repo
  • Github Actions Build #36018545646
  • 3277c2a8 on github
  • Prev Build on master (#36012420435)
  • Next Build on master (#36020212647)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc