January 14, 2022·4 min read·engineeringpythoncpythonmemory

Python 3.10.2 fixes a leak in Cython function calls

Python 3.10.2 arrives early to fix a native allocation leak. The investigation shows how a small mistake in the interpreter can accumulate in extension-heavy applications.

Python 3.10.2 arrives ahead of schedule because certain calls from Cython code leak memory inside the interpreter. The release announcement describes a small constant allocation per affected call, with consequences for long-running applications. The interesting part is how little application code needs to do wrong. An ordinary dependency can exercise the affected interpreter function repeatedly while returning correct results.

The investigation began with an asyncpg memory problem. On January 11, Yury Selivanov reported an allocation inside PyEval_EvalCodeEx that nobody freed. Elvis Pranskevichus confirmed that removing it prevented the reported leak. Their issue discussion connects a symptom in a database library to a defect in CPython's function-call implementation. It also records the decision to accelerate a maintenance release.

I value Python's extension ecosystem more than the language itself. That ecosystem often provides the implementation a problem needs, but this incident illustrates the dependency it creates on interpreter internals. A Python application can inherit a native memory defect through a library without containing any C code of its own.

An allocation with no consumer

Selivanov identified this statement in the interpreter implementation. The variable names and allocation come directly from his January 11 report.

PyObject **kwargs = PyMem_Malloc(sizeof(PyObject *) * kwcount);

The report says the resulting kwargs allocation was neither used nor freed anywhere in the function body. That makes the repair unusually direct. An allocation without a consumer contributes nothing to the call's result, so removing it removes the leak without requiring a new lifetime for useful data. Pranskevichus's confirmation provided evidence that this allocation explained the application symptom under investigation.

The expression also explains why counting calls matters. It requests storage proportional to kwcount, measured in pointer-sized elements. Repeated execution can therefore accumulate allocations even when each individual request is small. There is no need to retain a growing Python collection for this particular defect to consume memory.

Pablo Galindo traced the affected path into Cython's __Pyx_PyCFunction_FastCall implementation. His issue comment identifies repeated calls through that helper as a serious exposure. The release announcement consequently describes the problem in terms of Cython usage, although the faulty allocation belongs to CPython. The library that makes a defect visible and the component that owns its repair can be different projects.

This distinction changes the useful diagnosis. Rewriting an application's data structures cannot remove an allocation that the interpreter performs on its behalf. The issue's confirmation came from changing the interpreter allocation itself. For applications affected through this path, Python 3.10.2 supplies the corresponding runtime repair.

Native allocation and object ownership

The issue also uncovered separate cleanup problems on failure paths. Larry Hastings observed that temporary objects could leak if creating the function object failed. Selivanov followed with another patch, and the thread records further error-path work. These were related ownership defects within the same function, but they required cleanup of values that the function actually used.

Python's 3.10.2 reference-counting documentation describes how extension code manages strong references. Creating a strong reference increases an object's reference count. Releasing that reference requires the corresponding decrement. When the count reaches zero, the object's type deallocation function runs. Native allocation ownership and Python reference ownership therefore require separate attention when reading interpreter code.

The unused pointer array and the error-path references illustrate that difference. Removing a redundant array allocation resolves one ownership problem. Cleaning up temporary Python objects when an operation fails resolves another. A review focused only on the successful return can overlook both the unused storage and the values abandoned during an exceptional return.

There is another constraint on reference cleanup. The documentation warns that decrementing a reference can execute arbitrary Python code through deallocation. Reachable data structures must already be consistent before that happens. Correct cleanup is therefore about ordering as well as pairing reference operations. Even a small interpreter routine can have obligations that extend beyond freeing its own local storage.

Why the release moved forward

Gregory P. Smith asked whether the fix could wait for the already scheduled release, which the discussion placed four weeks away. He also asked when the regression began. Pranskevichus identified its introduction in the fifth alpha of the 3.10 development cycle. The reported history therefore did not limit the problem to the immediately preceding maintenance release.

Galindo argued that leaving Cython calls leaking for that interval was unacceptable. His comments connected the release decision to repeated calls and the memory growth reported in asyncpg. This was an argument about the cost of leaving an affected runtime deployed. The fact that one invocation loses little memory says little about an application that invokes the same operation throughout its lifetime.

I agree with the early release decision on that evidence. A maintenance schedule needs room for defects whose impact accumulates during otherwise successful work. The issue gives a concrete reason for changing that schedule, without requiring a new language feature or an application-visible exception.

The public download page dates Python 3.10.2 to January 14. Its version-specific changelog labels the final release January 13, but the public announcement is the basis for this post's date. The important operational change is the repaired PyEval_EvalCodeEx path. Applications that reach it through Cython can keep the same calling code while running an interpreter that no longer performs the unused allocation.