9 merged pull requests · 5 upstream projects
Fixes in the libraries I build on: Django, redis-py, matplotlib, OpenTelemetry, and Black. The main one here is a connection-pool bug in redis-py's async cluster client: a topology refresh during a transaction sent connections back to the wrong server's pool. It merged after a maintainer review that changed my approach, and that review is most of the story.
Python · asyncio · Redis Cluster · SQLAlchemy · OpenTelemetry semantic conventions · pytest
redis-py #4254: connections returned to the wrong pool
While reviewing my earlier redis-py PR (#4206, EVALSHA in cluster pipelines), the maintainer found two places where the async cluster client behaved differently from the sync one and filed them as issue #4253. I took the first one.
The problem
A Redis Cluster transaction that uses WATCH has to send every command on the same socket, because the watch lives on that connection. The async TransactionStrategy held on to its connection correctly, but on every command it looked the slot up again and overwrote _transaction_node with whatever node the slot map named right now. If another task refreshed the cluster topology in the meantime, the pipeline ended up holding a socket from node A while believing it belonged to node B. Then reset() released that socket into node B's free queue, so the next caller on B got a connection to A.
WATCH on node A
pipeline holds a socket to A
Topology refresh elsewhere
slot now maps to node B
Before
node := B · socket still A · reset() frees A into B's pool
After
node stays A · reset() frees A into A's pool
My first version
The sync client has an ownership check: if the pool doesn't own the held connection, release it to its real owner and acquire a new one. I ported that to async with a new ClusterNode.owns_connection() and NodesManager.find_connection_owner(), and argued in the review threads that this was sync parity. Automated reviewers flagged that swapping sockets mid-WATCH was risky. I answered that sync did the same thing. A community reviewer approved it.
The review
Petya Slavova, the maintainer, requested changes. In sync, ConnectionPool.owns_connection() only compares process IDs, so in a single process it always returns true and the swap branch never runs. Sync keeps the watching socket. My async check tested real membership, so async would actually switch sockets, run EXEC without the WATCH, and put a connection that was still watching back in the pool. I had fixed the wrong-pool bug by adding a worse one.
I read redis/connection.py to confirm it, said on the PR that my parity argument was wrong, and took the smaller fix from the review.
The fix that merged
Set the node only when a new connection is acquired. Then the node always names the owner of the held connection, and the existing release paths in reset() and _reinitialize_on_error() are correct without changes. The ownership helpers I'd added were deleted.
if not self._transaction_connection:
node = self._pipe.cluster_client.nodes_manager.get_node_from_slot(
list(self._pipeline_slots)[0], False
)
self._transaction_connection = node.acquire_connection()
self._transaction_node = node
return self._transaction_node, self._transaction_connectionThe regression test seeds two nodes with mock connections, starts a watched transaction on the first, retargets the slot to the second mid-transaction, and asserts that the pipeline keeps the original socket and that reset() returns it to the first node's pool and not the second's. The approval: "This is exactly the change I had in mind." Merged Oct 20, 2026, about two months after I opened it.
Shorter ones
Django's parallel test runner gives each worker its own clone of the test database. Worker IDs came from a shared counter that only went up, so when multiprocessing replaced a worker that had exited (a crash, an out-of-memory kill), the new one got an ID past the last clone and tried to open a database that was never created. The run died with database "test_db_5" does not exist on Postgres, or unable to open database file on SQLite. The ticket (#27734) had been open since 2017.
Following the approach a maintainer had sketched on the ticket, I turned the clones into a pool of slots. A worker claims a free slot when it starts and records its PID. On each pass of the existing result-polling loop, the parent frees the slots of workers that have exited, so their replacements reuse those clones. If no slot is free, the worker fails with a clear error. I reproduced the crash on Windows by forcing a respawn after every subsuite (maxtasksperchild=1), and a second reviewer reproduced it on macOS.
Review found two races. I took the snapshot of live processes outside the lock, so a replacement could claim a slot in between, and a worker with a recycled PID could end up owning two slots. Both are handled in the version that merged on July 31, 2026. The reviewer opened with "an impressive first PR."
In the metrics tracking issue I pointed out that the SQLAlchemy instrumentation still reported pool usage as the deprecated db.client.connections.usage. A reviewer opened #5060 from that comment, and I took it. I first put the new metric behind OTEL_SEMCONV_STABILITY_OPT_IN, the way span attributes were migrated. The maintainers decided that flag is for spans only, so I reworked it to always emit db.client.connection.count with the current state and pool-name attributes. Because existing dashboards lose the old metric, it shipped with a breaking-change changelog entry, docs, and tests that check the old metric is gone, run against SQLAlchemy 1 and 2.
A Celery task request carries a hostname (the worker that ran it) and an origin (the process that sent it). The instrumentation aliased origin to hostname, so on Celery 5 the sender's node name overwrote celery.hostname and traces pointed at the wrong worker while the metrics had it right (#3096). An earlier PR (#3097) had the fix but stalled waiting on a changelog entry and a test. I picked it up, linked the original, and added both. A reviewer pointed out that my test set both fields, so it missed the case the bug lives in, and I added an origin-only test. Merged Oct 26, 2026.
An image made with figimage() disappeared if you removed it and added it back with add_artist() (#31730). figimage() never set a transform, and its pixel offsets only drew correctly because an unset transform falls back to identity. add_artist() sees no transform and assigns the figure's, which draws the image in the wrong place. Setting IdentityTransform() in FigureImage.__init__, as a maintainer suggested in the issue, fixes it for both entry points and still lets transform= override. Milestoned for 3.12.
Everything merged
redis/redis-py #4254
Async cluster transactions keep the node that owns the held connection
merged Oct 2026 · maintainer follow-up
redis/redis-py #4206
EVALSHA in cluster pipelines with hash-slot routing
merged Aug 2026
open-telemetry/opentelemetry-python-contrib #4827
Celery spans keep the worker hostname and report origin as its own attribute
merged Oct 2026 · revived a stalled fix
open-telemetry/opentelemetry-python-contrib #5063
SQLAlchemy emits the current db.client.connection.count metric
merged Oct 2026 · breaking change
open-telemetry/opentelemetry-python-contrib #4828
Redis instrumentation: explicit db=None no longer emits an invalid span attribute
merged
matplotlib/matplotlib #32380
FigureImage keeps its transform when re-added via add_artist
merged Oct 2026 · 3.12
matplotlib/matplotlib #32147
Nested-brace mathtext parsing
merged · backported to 3.11.x
django/django #21629
Nine-year-old parallel test runner crash
merged
psf/black #4611
Release pipeline moved to PyPI Trusted Publishing
merged
What I took from it