February 4, 2025·4 min read·engineeringpythonasynciomaintenance

Python 3.13.2 repairs asyncio flow control and cancellation

Python 3.13.2 fixes a missing write-buffer notification and removes a task-group optimization that could miss cancellation. Both repairs concern the resource contracts beneath asynchronous application code.

An asynchronous function can return promptly while leaving the application with more work than it can drain. Correct scheduling therefore depends on signals that tell producers when to stop and tell tasks when their work has ended. Python 3.13.2 repairs failures in both kinds of signal, making its asyncio changes a useful way to understand this maintenance release.

The February 4 release is the second maintenance release of the 3.13 series. Its announcement counts almost 250 bug fixes, build improvements, and documentation changes since 3.13.1. The versioned changelog includes an asyncio socket-transport write-buffer fix and removal of an incorrect optimization for eager tasks in task groups. These repairs affect different operations, but both restore behavior that other code needs to manage resources.

Python's protocol model is one of the parts of the language I value. A library can participate in established behavior through a small set of methods. That benefit depends on the implementation honoring the protocol even when an optimization takes a less common execution path.

A write call does not wait for the peer

The transport documentation shipped with 3.13.2 defines write() as nonblocking. It buffers bytes and arranges for asynchronous transmission. A successful return therefore does not mean that the peer has consumed those bytes. If production continues faster than transmission, buffered data can accumulate.

Write transports expose high and low watermarks to coordinate this situation with the protocol using the transport. Crossing the high watermark calls pause_writing(). Once a paused buffer drains to the low watermark, the transport calls resume_writing(). The different thresholds allow some buffering without repeatedly pausing and resuming around one boundary.

These callbacks express a cooperative contract. They tell the producer when the transport needs it to stop submitting data. Their usefulness depends on all relevant write paths issuing the notification. Buffering data through a different entry point must not silently bypass the mechanism.

The changelog's gh-127655 entry records exactly that defect. The selector socket transport failed to pause writing for its protocol when writelines() brought the buffer to the high watermark. Python 3.13.2 fixes that missing notification. It does not introduce a new application-level queue limit or change every transport implementation. The named failure concerns this transport and this write method.

The method is documented as functionally equivalent to calling write() for each element, although its implementation may be more efficient. That qualification explains the design tension. Combining work can reduce overhead, but callers still need the observable flow-control behavior that individual writes would provide. An optimized batch path cannot treat notification as incidental.

Eager tasks change when execution begins

A separate repair concerns task execution that starts before the normal scheduling step. The 3.13.2 task documentation describes asyncio.eager_task_factory, introduced in 3.12. With this factory installed, a coroutine starts synchronously during task construction. The event loop schedules it only if it blocks.

That can avoid scheduling overhead when a coroutine completes immediately. The documentation gives cached or memoized operations as an example. A cache hit may finish without actual I/O, so constructing a task need not imply a later event-loop turn.

Immediate execution also changes semantics. A coroutine that returns or raises synchronously never enters the event loop's scheduling sequence. Execution order can therefore differ from an application using the ordinary factory. Code coordinating tasks must account for completion that has already happened when task construction returns.

The gh-128550 changelog entry says Python removed an incorrect optimization related to eager tasks in asyncio.TaskGroup because it caused cancellations to be missed. That description supports a specific conclusion about the repair. It does not support claiming that ordinary task groups always lost cancellation, or that eager execution itself has been removed.

For application reasoning, the significant point is that an optimization cannot assume every task follows the same deferred lifecycle. A task group has to coordinate work even when a child has already completed through the eager path. Missing cancellation changes the control flow visible to callers, so preserving that behavior takes priority over retaining the optimization.

Maintenance beneath unchanged APIs

The same changelog records other lifecycle repairs, including cyclic references involving eager tasks created through the loop and task-group APIs. It also fixes a task leak and unhandled exception in asyncio.staggered.staggered_race(). These entries describe concrete cleanup work beneath APIs that application code may continue to call unchanged.

I choose Python largely because its ecosystem has already solved substantial parts of the problems that bring me to it. Maintenance of these underlying contracts matters to that choice. An application using an asynchronous library depends on the runtime's handling of buffers and task lifetime even when its own code never touches a low-level transport.

The release announcement still describes free threading and the JIT as experimental parts of the 3.13 series. Neither changes status in this maintenance release. The asyncio fixes have a narrower scope, correcting behavior within existing execution paths. For a program depending on selector-transport flow control or eager task coordination, that narrower scope includes the signals through which it controls ongoing work.