How I Fixed a Tuple Free‑List Bug in CPython 3.12
The article explains CPython's object memory caching, details the structure of the tuple free‑list, reproduces a bug where length‑20 tuples aren't reused, shows the debugging steps, and presents a one‑line source change that corrects the issue in Python 3.12/3.13.
While writing a series dissecting Python 3.12's interpreter source, I noticed an inconsistency in the tuple implementation and decided to investigate.
Python objects are allocated on the heap via malloc. To avoid the overhead of repeated allocations and deallocations, CPython maintains per‑type free‑lists that cache recently freed objects. Lists use a free‑list of default capacity 80, while tuples have a much larger free‑list capacity of 40 000 because tuples are created implicitly in many situations.
The tuple free‑list is implemented as a C array named free_list with 20 entries. Each entry points to the head of a linked list that caches tuples of a specific length: free_list[0] stores length‑1 tuples, free_list[1] stores length‑2 tuples, and so on up to free_list[19] for length‑20 tuples. Only tuples of length 1‑20 are cached, with up to 2 000 tuples per length. The empty tuple is a singleton initialized at interpreter start.
To observe the free‑list, I added a class method tuple.get_free_list_count(length) that returns the number of cached tuples of the given length, recompiled CPython, and ran a series of tests:
Calling get_free_list_count(3) after interpreter start returned 5, indicating five length‑3 tuples were already cached.
Creating a = (1, 2, 3) reduced the count to 4, confirming the tuple was taken from the cache.
Creating b = (4, 5, 6) and c = (7, 8, 9) each further decreased the count to 3, 2, and finally 1.
Deleting a, b, c returned the three tuples to the cache, restoring the count to 5.
The process worked for lengths 1‑19, but length‑20 tuples behaved differently. After creating three length‑20 tuples and deleting them, the cache count rose to 3, yet a subsequent creation with d = tuple(range(20)) did not reuse a cached tuple; a new allocation occurred. Deleting d added it back to the cache, revealing the bug: length‑20 tuples are cached on destruction but never retrieved on creation.
The offending source code checks the tuple size against the macro PyTuple_MAXSAVESIZE (value 20) using a strict "less than" comparison. The condition should be "less than or equal" so that size‑20 tuples are also considered for reuse. The fix is a single added equality operator in the relevant if statement.
Only CPython 3.12 and 3.13 are affected; other versions are unaffected. After applying the change and recompiling, length‑20 tuples are correctly reused, closing the bug.
Below is the illustrative code snippet used in the article (comments translated to English):
# The right‑hand side 1, 2, 3, 4 is equivalent to (1, 2, 3, 4)
a, b, c, d = 1, 2, 3, 4
# *args is a tuple
def foo(x, y, z, *args):
pass
# Multiple return values are essentially a tuple
def bar():
return 1, 2Finally, the author encourages readers interested in Python implementation details to follow his series for deeper insights.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Satori Komeiji's Programming Classroom
Python and Rust developer; I write about any topics you're interested in. Follow me! (#^.^#)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
