24 May 2026
Planet Python
Graham Dumpleton: Async support for wrapt.synchronized
Continuing the tour through the wrapt 2.2.0 release, the last piece worth a closer look is the new async support in wrapt.synchronized. The decorator has been part of wrapt from the start, but until 2.2.0 it only really did the right thing for synchronous code. Applying it to an async def function used to give the appearance of working without actually serialising anything, and the context manager form had no async variant at all. Both are now fixed.
A quick recap of synchronized
wrapt.synchronized is the bundled decorator for ensuring that a callable is only executed by one caller at a time. The lock it acquires is created lazily and attached to the right object depending on what is being decorated: a per-function lock for plain functions, a per-instance lock for instance methods, a per-class lock for class methods or when the decorator is applied to a class body, and so on. None of that bookkeeping is the caller's responsibility.
import wrapt
class Counter:
def __init__(self):
self.value = 0
@wrapt.synchronized
def increment(self):
self.value += 1
There is also a context manager form, where you supply the object that should own the lock. The decorator form and the context manager form share the same auto-created lock when they name the same object, so they can be mixed freely:
counter = Counter()
with wrapt.synchronized(counter):
counter.value += 1
The lock used in both cases is a threading.RLock. That choice matters and I will come back to it.
Where it fell apart on async
Applying the same decorator to an async def method in wrapt 2.1.x looked promising at first glance. The call returned a coroutine, awaiting it ran the body, and nothing raised. It was only when you tried it under contention that the problem became visible:
import asyncio
import wrapt
class Counter:
def __init__(self):
self.value = 0
@wrapt.synchronized
async def inc(self):
cur = self.value
await asyncio.sleep(0.01)
self.value = cur + 1
async def main():
c = Counter()
await asyncio.gather(*(c.inc() for _ in range(10)))
print(c.value)
asyncio.run(main())
Run under wrapt 2.1.2, this prints 1. Ten tasks all read cur = 0, all sleep, all write 1 back. The lock attached to the instance was a threading.RLock, and it was acquired and released around the construction of the coroutine, not around the awaited body. By the time anything interesting happened, the lock was gone.
The context manager form did not help either. There was no async with support, so writing:
async with wrapt.synchronized(counter):
...
failed with an AttributeError complaining about a missing __aenter__. If you wanted serialised access to a shared resource from async code, you were on your own.
What 2.2.0 changes
In 2.2.0 the decorator inspects the wrapped function and picks a different locking primitive when it sees a coroutine function:
import asyncio
import wrapt
class Counter:
def __init__(self):
self.value = 0
@wrapt.synchronized
async def inc(self):
cur = self.value
await asyncio.sleep(0.01)
self.value = cur + 1
async def main():
c = Counter()
await asyncio.gather(*(c.inc() for _ in range(10)))
print(c.value)
asyncio.run(main())
This now prints 10. The wrapper still returns a coroutine, but the lock acquisition and release happen inside that coroutine using await, so the awaited body is actually serialised across tasks.
The lock attached to the instance in this case is an asyncio.Lock, stored under a different attribute (_synchronized_async_lock) than the synchronous version (_synchronized_lock). A class that mixes synchronous and asynchronous synchronized methods on the same instance therefore gets two distinct locks, which is what you want, because mixing threading and asyncio primitives on the same lock would not work anyway.
The context manager form has gained an async variant alongside the synchronous one. The same call now supports both spellings, picking the right behaviour based on whether you write with or async with:
async with wrapt.synchronized(counter):
counter.value += 1
For plain async functions, async classmethods, and any other shape that wrapt's decorator machinery already knew how to dispatch on, the same rule applies. If the wrapped callable is async def, you get an asyncio.Lock and an async wrapper. If it is not, you get a threading.RLock and a synchronous wrapper. The choice between them is automatic.
The reentrancy difference
There is one place where the synchronous and asynchronous paths deliberately do not match up: the synchronous lock is reentrant and the asynchronous lock is not. Calling a synchronized method from inside another synchronized method on the same instance is fine in the synchronous case, because threading.RLock allows the same thread to acquire the lock more than once. The async equivalent deadlocks:
import asyncio
import wrapt
class A:
@wrapt.synchronized
async def outer(self):
return await self.inner()
@wrapt.synchronized
async def inner(self):
return "done"
async def main():
a = A()
try:
result = await asyncio.wait_for(a.outer(), timeout=0.5)
print(result)
except asyncio.TimeoutError:
print("deadlocked")
asyncio.run(main())
This prints deadlocked. The same outer then inner chain on synchronous methods would print done and move on.
The reason the async case behaves this way is that the standard library does not provide a reentrant async lock. There is no asyncio.RLock, only asyncio.Lock. Whether one ought to exist has been a recurring discussion on the Python issue tracker and on discuss.python.org for the better part of a decade, and the short version is that there is no agreement.
The case for adding one is the obvious one. Code being ported from a synchronous codebase often relies on the reentrancy of threading.RLock to allow public methods that take a lock to call other public methods that take the same lock. Without a reentrant async equivalent, the same restructuring work has to be done by hand.
The case against is partly about scope (every primitive in the standard library carries a maintenance cost) and partly about the conceptual mismatch between threads and tasks. threading.RLock is reentrant per thread, and a thread is a long-lived identity that a function can simply ask about. The analogous identity in async code is the current task, which is well defined but feels less stable to reason about: tasks are cheap, can be created mid-call, and suspend at every await. A reentrant lock keyed on the current task can paper over genuine design problems where one task ends up holding a lock across an await that gives another piece of code a chance to re-enter, in ways that are much easier to spot when the lock simply refuses to be acquired twice.
There are third-party packages that implement reentrant async locks for people who want them, but wrapt deliberately stays in step with the standard library here. The synchronous side uses threading.RLock because that is what the standard library provides; the async side uses asyncio.Lock for the same reason.
The practical consequence is that the usual workaround for non-reentrant locks applies on the async side. Public methods that acquire the lock should delegate to private helpers that assume the lock is already held:
import asyncio
import wrapt
class Counter:
def __init__(self):
self.value = 0
@wrapt.synchronized
async def add_two(self):
await self._incr()
await self._incr()
return self.value
async def _incr(self):
cur = self.value
await asyncio.sleep(0.001)
self.value = cur + 1
async def main():
c = Counter()
await asyncio.gather(*(c.add_two() for _ in range(5)))
print(c.value)
asyncio.run(main())
That prints 10, with the lock acquired exactly once per call to add_two. The pattern is a bit more disciplined than relying on reentrancy, but it makes the locking boundaries explicit, which is no bad thing in async code.
Wrapping up
The full set of changes to wrapt.synchronized is in the changelog, and the decorator itself is documented on the bundled decorators page. The feature is in wrapt from 2.2.0 onwards, with the usual recommendation to grab the latest release from PyPi since there have been follow-up releases on the 2.2.x branch. Issues and questions, as ever, go to the issue tracker on Github.
24 May 2026 10:00am GMT
Graham Dumpleton: Reshaping decorated functions with wrapt
Most decorators leave the function's outward shape alone. The same parameters go in, the same return type comes out, and inspect.signature and inspect.iscoroutinefunction give the same answers they would have given for the undecorated function.
Sometimes you want a decorator that actively changes that shape. Adds or removes a parameter. Changes the return annotation. Turns a sync function into something that should be awaited, or runs an async function to completion so it can be called from sync code. The mechanics of doing the work in the wrapper body are usually straightforward. The harder part is making sure that downstream tools, which decide how to call or treat the function based on what introspection tells them, see the shape of the wrapper rather than the shape of the wrapped target.
wrapt has had a partial answer to this for a long time via the adapter argument on @wrapt.decorator. The 2.2.0 release replaced that with a cleaner standalone with_signature decorator and added a new piece, mark_as_sync / mark_as_async, for the calling-convention side that the existing API did not address at all. There are also a couple of convenience bridges, async_to_sync and sync_to_async, that do the bridging and the marking together for the common cases.
What introspection is for
When this post talks about introspection, it means runtime introspection. Specifically, the answers given by inspect.signature, inspect.iscoroutinefunction, inspect.isasyncgenfunction, inspect.isgeneratorfunction and their friends, computed from the function object after the program has started running.
This is distinct from static type checking. mypy and pyright work from source-level type hints before the program runs and rely on different mechanisms (typing.ParamSpec, typing.Concatenate, properly annotated wrapper signatures and so on). The wrapt decorators in this post fix up runtime introspection. They do not, in general, satisfy a static type checker. That is a separate problem with separate tools.
The runtime side matters because a noticeable amount of modern Python ecosystem behaviour is driven by it. FastAPI inspects function signatures to build request parsing and parameter validation. ASGI frameworks ask iscoroutinefunction to decide whether to await a handler directly or dispatch it to a threadpool. pytest-asyncio decides whether to treat a test as async based on the same check. Click and Typer build their CLIs from inspect.signature of the command function. Sphinx and similar doc tooling pull signatures the same way. Each of these is making a real decision based on what introspection says, and if a decorator silently lies about its shape the decision goes wrong.
The signature side: the old way
wrapt shipped a way to handle this from very early on, via the adapter argument on @wrapt.decorator. The argument takes a prototype function whose signature is borrowed and presented as the decorated function's:
import wrapt
def _prototype(payload): pass
@wrapt.decorator(adapter=_prototype)
def inject_session(wrapped, instance, args, kwargs):
return wrapped("session#1", *args, **kwargs)
@inject_session
def handle(session, payload):
return f"{session} processing {payload}"
import inspect
print("call result :", handle("hello"))
print("introspected sig :", inspect.signature(handle))
Output:
call result : session#1 processing hello
introspected sig : (payload)
The user's handle function is defined with (session, payload). The decorator hides the session parameter and presents the result as (payload), while internally still calling the real handle with a session it provides. inspect.signature reports the shape the user actually sees, not the underlying.
The adapter argument also accepts an inspect.getfullargspec() tuple or a formatted argspec string, and there is a wrapt.adapter_factory(...) helper for cases where the prototype has to be generated lazily.
The catch is twofold. First, the prototype has to be specified on the decorator factory call, which separates it from the wrapper body and makes the whole thing a little harder to read. Second, the adapter argument is being deprecated in favour of the standalone wrapt.with_signature decorator described below.
The signature side: with_signature
The 2.2.0 replacement is wrapt.with_signature. It is a standalone decorator that overrides what inspect.signature reports for a callable, without changing what the callable actually accepts when called. It takes exactly one of three keyword arguments.
The simplest form is prototype=, which borrows the signature from a dummy function:
import inspect, wrapt
def _prototype(payload: str) -> str: pass
@wrapt.with_signature(prototype=_prototype)
def handle(*args, **kwargs):
return f"session#1 a:{args[0]}"
print("call result :", handle("hello"))
print("signature :", inspect.signature(handle))
Output:
call result : session#1 a:hello
signature : (payload: str) -> str
The body of handle accepts *args, **kwargs because the implementation needs to be flexible (or in the decorator case, because that is what the wrapper signature is). Introspection sees the prototype's (payload: str) -> str instead.
The second form is signature=, which takes a prebuilt inspect.Signature object. This is useful when the parameter list has to be assembled programmatically:
sig = inspect.Signature(
parameters=[
inspect.Parameter("payload",
inspect.Parameter.POSITIONAL_OR_KEYWORD,
annotation=str)
],
return_annotation=str,
)
@wrapt.with_signature(signature=sig)
def handle(*args, **kwargs):
return f"session#2 b:{args[0]}"
The third form is factory=, which takes a callable that receives the wrapped function and returns either a Signature or a prototype. This is the right choice when the presented signature is derived from the wrapped function's own signature. A factory that strips the first parameter, for instance, would let an "inject the first argument" decorator present whatever the underlying function had with the first slot removed:
def strip_first(wrapped):
s = inspect.signature(wrapped)
return s.replace(parameters=list(s.parameters.values())[1:])
@wrapt.with_signature(factory=strip_first)
def handle(*args, **kwargs):
return f"session#3 c:{args[0]}"
When used together with @wrapt.decorator, the stacking matters. with_signature applies to whatever it directly decorates, so the right place for it is at the use site (above the decorator-built decorator applied to the user's function), or baked into a custom decorator factory that applies it to the wrapped result. Stacking it above a @wrapt.decorator-built decorator definition does not propagate the signature override to the final wrapped result.
A clean pattern for an "inject this argument" decorator that uses with_signature looks like:
def _prototype(payload): pass
@wrapt.decorator
def _wrap(wrapped, instance, args, kwargs):
return wrapped("session", *args, **kwargs)
def inject_session(fn):
return wrapt.with_signature(prototype=_prototype)(_wrap(fn))
@inject_session
def handle(session, payload):
return f"{session} processing {payload}"
inspect.signature(handle) reports (payload). The decorator is a regular function that composes wrapt.decorator and wrapt.with_signature explicitly. A bit more code than the adapter= form, but the pieces are more visible, and wrapt.with_signature is itself reusable in plenty of cases that have nothing to do with @wrapt.decorator.
The important property that with_signature only touches argument-shape code-object flags. The calling-convention bits are left alone. That cleanly separates this concern from the next one.
The calling-convention side: the problem
Suppose a third-party decorator (or one you wrote a while back) takes an async def function and produces a sync callable that runs the coroutine to completion. The body of the decorated function is async, but the result is something you call without await. What does inspect.iscoroutinefunction say?
import asyncio, inspect, wrapt
@wrapt.decorator
def run_to_completion(wrapped, instance, args, kwargs):
return asyncio.run(wrapped(*args, **kwargs))
@run_to_completion
async def fetch():
return 42
print("call result :", fetch())
print("iscoroutinefunction:", inspect.iscoroutinefunction(fetch))
Output:
call result : 42
iscoroutinefunction: True
Calling fetch() returns 42 because the decorator collapsed the async work to a sync call. But inspect.iscoroutinefunction(fetch) still returns True, because introspection sees the underlying async def. Anything that asks "is this a coroutine function?" to decide what to do with fetch will pick the wrong path. An ASGI framework would await it. pytest-asyncio would treat it as async. Each of those is now making a decision that does not match the actual calling convention.
The mirror case is the same shape in reverse. A plain def function wrapped by something that returns a coroutine reads as not-a-coroutine through introspection, but actually requires await.
with_signature does not help here. It is the wrong tool. The calling-convention bits live in different co_flags slots.
mark_as_sync and mark_as_async
The 2.2.0 answer is a pair of small pass-through decorators that adjust the calling-convention bits and nothing else. They do not bridge anything. They only correct what introspection reports about a stack whose effective convention has already been changed by something else.
The async-to-sync case becomes:
import asyncio, inspect, wrapt
@wrapt.decorator
def run_to_completion(wrapped, instance, args, kwargs):
return asyncio.run(wrapped(*args, **kwargs))
@wrapt.mark_as_sync
@run_to_completion
async def fetch():
return 42
print("call result :", fetch())
print("iscoroutinefunction:", inspect.iscoroutinefunction(fetch))
Output:
call result : 42
iscoroutinefunction: False
And the symmetric sync-to-async case:
import asyncio, inspect, wrapt
@wrapt.decorator
def schedule(wrapped, instance, args, kwargs):
async def runner():
return wrapped(*args, **kwargs)
return runner()
@wrapt.mark_as_async
@schedule
def compute(x, y):
return x * y
print("iscoroutinefunction:", inspect.iscoroutinefunction(compute))
print("awaited result :", asyncio.run(compute(6, 7)))
Output:
iscoroutinefunction: True
awaited result : 42
It is worth being explicit about what the markers do not do. Putting @wrapt.mark_as_sync directly on an async def does not magically make it sync-callable. It only changes what iscoroutinefunction reports. The bridging from one convention to the other has to be done by some other piece of code in the stack. The markers exist so that introspection can be made to match the reality that something else has already established.
Generator nuance
There are four kinds of callable when you consider the generator/coroutine axes together: plain function, sync generator, coroutine function, async generator. The markers handle all four via an optional generator= keyword that takes None (default, preserve), True (mark as the generator variant of the chosen convention), or False (mark as the non-generator variant).
For example, if an inner decorator drains an async generator into a list and presents the result as a plain sync function returning that list, mark_as_sync(generator=False) makes introspection see "plain sync function" rather than the underlying "async generator function". The mirror case for async iterables produced from a sync generator uses mark_as_async(generator=True). Most code only needs the default form, but the option is there when both axes need to move at once.
async_to_sync and sync_to_async
For the common case of actually bridging between conventions, 2.2.0 bundles two convenience decorators. They do the bridging and apply the right marker, so introspection lines up without a separate mark_as_* step.
wrapt.async_to_sync runs the coroutine to completion via asyncio.run on each call, and marks the result as sync:
import inspect, wrapt
@wrapt.async_to_sync
async def add(a, b):
return a + b
print("iscoroutinefunction:", inspect.iscoroutinefunction(add))
print("call result :", add(2, 3))
Output:
iscoroutinefunction: False
call result : 5
wrapt.sync_to_async is the mirror. It schedules a sync function onto the default executor via loop.run_in_executor, and marks the result as async:
import asyncio, inspect, wrapt
@wrapt.sync_to_async
def mul(a, b):
return a * b
print("iscoroutinefunction:", inspect.iscoroutinefunction(mul))
print("awaited result :", asyncio.run(mul(4, 5)))
Output:
iscoroutinefunction: True
awaited result : 20
These are the same family of utility as asgiref.sync.sync_to_async and asgiref.sync.async_to_sync, which Django leans on heavily for mixing sync and async code. The wrapt versions are smaller and pre-marked, which is the convenient thing. If you need richer behaviour, like explicit executor selection or structured concurrency through anyio, the third-party tools are still the right call. In that case you can apply wrapt.mark_as_sync or wrapt.mark_as_async after the third-party bridge to bring introspection into line:
@wrapt.mark_as_sync
@third_party_async_to_sync
async def work(...):
...
Composing both axes
The whole point of keeping with_signature and the markers as separate decorators is that they touch different parts of the function's code-object flags and can be combined freely. A decorator that changes both the parameter shape and the calling convention works by stacking them in the natural order:
import inspect, wrapt
def _prototype(payload: str) -> int: ...
@wrapt.async_to_sync
@wrapt.with_signature(prototype=_prototype)
async def handler(*args, **kwargs):
return len(args[0])
print("signature :", inspect.signature(handler))
print("iscoroutinefunction :", inspect.iscoroutinefunction(handler))
print("call result :", handler("hello"))
Output:
signature : (payload: str) -> int
iscoroutinefunction : False
call result : 5
with_signature overrides the signature presented by introspection. async_to_sync bridges the async body to a sync call and marks the result accordingly. The two concerns are completely independent and the stacking just falls out of which one needs to be closer to the function (with_signature) and which produces the outer wrapper (async_to_sync).
Wrap up
The summary of where to reach for each piece is short.
For signature changes, use wrapt.with_signature for new code. The adapter= argument to wrapt.decorator still works and is not going away tomorrow, but it is being deprecated in favour of with_signature, which is the cleaner option going forward.
For calling-convention changes, where introspection needs to report a different sync/async/generator answer than the underlying function would suggest, use wrapt.mark_as_sync or wrapt.mark_as_async to correct what introspection reports. Remember that the markers do not bridge anything. They annotate a stack whose effective convention has already been changed.
For the common bridging cases, wrapt.async_to_sync and wrapt.sync_to_async do the bridging and the marking together. For more sophisticated async runtime needs, keep using asgiref or anyio and apply the markers afterwards.
The full set of these tools is documented across signature changing decorators, signature override and calling convention markers and adapters. Release notes are in the changelog, the latest release is on PyPi, and issues go to the issue tracker on Github.
24 May 2026 8:00am GMT
Graham Dumpleton: Lazy monkey patching with wrapt
This post is for the people who write APM agents, tracers, profilers, debuggers, and anything else that instruments Python code without asking the user to change it. Everyone else is welcome along.
The reason I want to call out the audience up front is that wrapt was created for this kind of work, and the original purpose is sometimes obscured by how widely the project has been adopted for its decorator API. The decorator side of wrapt (which the recent posts on stateful decorators and per-instance lru_cache have covered) grew out of needing reliable building blocks for monkey patching, not the other way around.
There is a side of wrapt that, until April 2026, had no dedicated page in the official documentation. I have covered it in various conference talks over the years, but that is not the same thing as having proper docs. The mechanism for deferred monkey patching, registering a patch against a module that has not been imported yet, with the patch only applied when the module is later imported, has been part of wrapt from day one. The monkey patching documentation page finally landed in the lead-up to the 2.2.0 release, which also added a small ergonomic piece. A new ? modifier on module names closes the last awkward gap in how the deferred form composes with the convenient decorator syntax.
So this post is amplification of a pattern that has been there all along, not breaking news. The new modifier is just polish.
With Python 3.15 about to ship PEP 810 explicit lazy import syntax, the timing matters. Any instrumentation library that force-imports its target modules at agent startup is now actively undoing user-level lazy imports. That has always been a little impolite for cold-start performance. With 3.15 it becomes a direct conflict with how users want to write their code.
A monkey patching primer
The smallest useful piece of wrapt's monkey patching API is wrap_function_wrapper. You give it a module, the dotted name of an attribute on that module, and a wrapper function. It replaces that attribute with a FunctionWrapper that calls your wrapper around the original.
A timing wrapper on json.dumps looks like this:
import json
import time
import wrapt
def time_call(wrapped, instance, args, kwargs):
start = time.perf_counter()
try:
return wrapped(*args, **kwargs)
finally:
elapsed = (time.perf_counter() - start) * 1e6
print(f"json.dumps took {elapsed:.0f} us")
wrapt.wrap_function_wrapper("json", "dumps", time_call)
print(json.dumps({"a": 1, "b": [2, 3]}))
Output:
json.dumps took 16 us
{"a": 1, "b": [2, 3]}
The wrapped, instance, args, kwargs signature of time_call is the same uniform wrapper signature that @wrapt.decorator uses, and that the stateful decorators post has already shown in the decorator context. That is not a coincidence. The decorator API in wrapt is built on top of this same wrapper mechanism, not the other way around, so the body you would write for a @wrapt.decorator-style decorator is the same body you would write for a monkey patch. Whatever you have learned about writing wrappers in the decorator context carries straight over.
The user code that calls json.dumps does not change. The instrumentation is added entirely by wrap_function_wrapper. That is the whole point. APM agents and similar tools want to add visibility to third-party code without asking the user to modify it.
The forced-import problem
wrap_function_wrapper takes the module name as a string and imports the module to find the attribute to wrap. The act of registering the patch loads the target.
import sys
import wrapt
def trace(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
print("before:", "xml.etree.ElementTree" in sys.modules)
wrapt.wrap_function_wrapper("xml.etree.ElementTree", "fromstring", trace)
print("after :", "xml.etree.ElementTree" in sys.modules)
Running this prints:
before: False
after : True
For a single module that is mildly wasteful. For an APM agent that supports, say, requests, httpx, urllib3, aiohttp, django, flask, fastapi, sqlalchemy, psycopg, redis, pymongo and kafka-python, importing the agent loads every one of those modules at agent startup, regardless of which the user's app actually uses.
The price shows up in three places. Cold start time gets a noticeable chunk added, which matters disproportionately in serverless and short-lived worker environments where the process lifetime is measured in seconds. Memory holds code that is never going to be called. And the user's own lazy import statements get silently undone, because by the time their code runs the modules are already loaded.
The long-standing answer
The mechanism that solves all three problems has been in wrapt from the early days. The idea originally came from PEP 369, which proposed post-import hooks for the Python standard library. That PEP was withdrawn, but wrapt provides its own implementation via a sys.meta_path finder.
The low-level entry point is register_post_import_hook(hook, name). The hook is a callback that takes the module as its argument and runs once the named module is imported. If the module is already imported when the hook is registered, the hook fires immediately.
The decorator form, when_imported(name), is the one most code uses:
import sys
import wrapt
def trace_reader(wrapped, instance, args, kwargs):
print("[traced csv.reader]")
return wrapped(*args, **kwargs)
@wrapt.when_imported("csv")
def install(module):
wrapt.wrap_function_wrapper(module, "reader", trace_reader)
print("after register:", "csv" in sys.modules)
import csv
print("after import :", "csv" in sys.modules)
for row in csv.reader(["a,b,c"]):
print("row:", row)
Output:
after register: False
after import : True
[traced csv.reader]
row: ['a', 'b', 'c']
Two things to notice. Registering the hook does not touch sys.modules. The module is only loaded when the user's code does import csv. And the wrapping happens automatically as a side effect of that import, so the patched csv.reader is what the user code sees.
This is the mechanism that every reputable APM agent already uses one way or another, because they had to. It just was not very visible from the outside.
The decorator-form gap
wrap_function_wrapper has a more convenient cousin called patch_function_wrapper which is the decorator form. It lets you keep the wrapper definition at module top level rather than nested inside a callback:
@wrapt.patch_function_wrapper("html.parser", "HTMLParser.feed")
def trace_feed(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
This is the form you really want for a patch registry. One decorated wrapper function per supported third-party target, all at the top level of one file. Easy to read, easy to grep, no nested closures.
The catch, before wrapt 2.2.0, was that this decorator form force-imported its target the same way wrap_function_wrapper did:
import sys
import wrapt
print("before:", "html.parser" in sys.modules)
@wrapt.patch_function_wrapper("html.parser", "HTMLParser.feed")
def trace_feed(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
print("after :", "html.parser" in sys.modules)
before: False
after : True
The lazy alternative meant restructuring into a when_imported callback with the wrapper defined inside it. Workable but ugly, especially repeated across a dozen targets, and you lose the clean "one decorated function per target" layout that makes a patch registry readable.
The ? modifier in 2.2.0
wrapt 2.2.0 closes the gap by recognising a trailing ? on a module name. With the ?, both wrap_function_wrapper and patch_function_wrapper defer registration via a post-import hook when the target module is not yet loaded. If the module is already in sys.modules, the patch is applied immediately. Same behaviour as before, just without the side effect of forcing the import.
import sys
import wrapt
def trace(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
wrapt.wrap_function_wrapper("gzip?", "compress", trace)
print("after register (with ?):", "gzip" in sys.modules)
import gzip
print("after import :", "gzip" in sys.modules)
after register (with ?): False
after import : True
And the decorator form, which is the case that actually motivated the change:
import sys
import wrapt
@wrapt.patch_function_wrapper("tempfile?", "mkdtemp")
def trace_mkdtemp(wrapped, instance, args, kwargs):
print("[traced tempfile.mkdtemp]")
return wrapped(*args, **kwargs)
print("after register (with ?):", "tempfile" in sys.modules)
import tempfile
print("after import :", "tempfile" in sys.modules)
print("mkdtemp:", tempfile.mkdtemp())
after register (with ?): False
after import : True
[traced tempfile.mkdtemp]
mkdtemp: /var/folders/.../tmpktve96ix
Under the hood, the ? form is genuinely just shorthand. The implementation in wrapt's patches.py is roughly:
if target.endswith("?"):
target = target[:-1]
if target in sys.modules:
return wrap_object(sys.modules[target], name, FunctionWrapper, (wrapper,))
def callback(module):
wrap_object(module, name, FunctionWrapper, (wrapper,))
register_post_import_hook(callback, target)
return None
No new mechanism, no new dispatch path. The work is still done by the same register_post_import_hook that has been in wrapt for years. The benefit is purely the authoring style. @patch_function_wrapper("...?", "...") at top level is now an option that previously was not.
Composition with PEP 810 lazy imports
Python 3.15 ships PEP 810 explicit lazy imports. The user can write:
lazy import requests
and the import is deferred until the name requests is first used. The discussion in Lazy imports using wrapt covers the PEP's motivation in more detail.
This raises a question that was not quite so sharp before. If a user's code uses lazy import for a module, and an APM agent registers a non-lazy wrap_function_wrapper for that module, what happens?
# apm_eager.py - simulated APM patches
import wrapt
def trace(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
wrapt.wrap_function_wrapper("gzip", "compress", trace)
# user_code.py - user's app
import sys
import apm_eager # APM agent loaded at process startup
lazy import gzip
print("after lazy import:", "gzip" in sys.modules)
gzip.compress(b"hello")
print("after first use :", "gzip" in sys.modules)
Output:
after lazy import: True
after first use : True
The user wrote lazy import gzip, but gzip is already in sys.modules by the time their import statement runs. The APM agent loaded it on the user's behalf. Whatever benefit the user expected from lazy import has been quietly undone.
Switching the APM agent to use the ? form fixes it:
# apm_lazy.py
import wrapt
def trace(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
wrapt.wrap_function_wrapper("gzip?", "compress", trace)
With the same user code as before, this now prints:
after lazy import: False
after first use : True
gzip is only loaded at the moment the user's code first touches it, and at that moment the patch fires too. Lazy patching and lazy imports compose correctly.
Putting it together: a patch registry
For an APM agent or similar, the practical pattern looks like this. A single file declares all the patches as a flat list of top-level decorated functions:
# my_apm_patches.py
import wrapt
@wrapt.patch_function_wrapper("xml.etree.ElementTree?", "fromstring")
def trace_fromstring(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
@wrapt.patch_function_wrapper("csv?", "reader")
def trace_reader(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
@wrapt.patch_function_wrapper("gzip?", "compress")
def trace_compress(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
@wrapt.patch_function_wrapper("html.parser?", "HTMLParser.feed")
def trace_feed(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
Importing this module registers all four patches but loads none of the target modules:
import sys
import my_apm_patches
targets = ["xml.etree.ElementTree", "csv", "gzip", "html.parser"]
for m in targets:
print(f" {m:30s} {'loaded' if m in sys.modules else 'not loaded'}")
Output:
xml.etree.ElementTree not loaded
csv not loaded
gzip not loaded
html.parser not loaded
Whichever modules the user's code actually imports is the set that ends up getting patched. The rest stay out of memory entirely. The agent has paid no cold-start cost for the targets the user does not care about, and the user's own lazy imports continue to do what they say on the tin.
As a bonus, the test story is also better. The instrumentation library's test suite no longer needs every supported third-party package installed just to import the library, only the ones it actually exercises.
What changed and what didn't
Strictly speaking, nothing in wrapt 2.2.0 enables any behaviour that was not possible before. The deferred patching mechanism is the same register_post_import_hook it always was. What changed is the authoring ergonomics. The ? modifier lets you write the lazy version of a patch as concisely as the eager version, including in the decorator form that suits patch-registry files best. And the monkey patching docs page that landed in April 2026 finally makes the mechanism easy to discover.
If you maintain instrumentation code that still force-imports its targets, Python 3.15 is a good prompt to refactor. The change is mechanical. Add a ? to the module name in each wrap_function_wrapper and patch_function_wrapper call. The behaviour for already-loaded modules is unchanged, and for not-yet-loaded modules the patch now fires when (and only when) the user's code actually imports them.
The full release notes for wrapt 2.2.0 are in the changelog. The latest release is on PyPi, and issues go to the issue tracker on Github.
24 May 2026 6:30am GMT
22 May 2026
Django community aggregator: Community blog posts
Issue 338: Django 6.1 alpha 1 released
News
Django 6.1 alpha 1 released
Django 6.1 alpha 1 has been released, signaling the next round of framework updates headed your way. Plan a quick test run in a staging environment so you can catch compatibility issues early as 6.1 develops.
Wagtail CMS News
Wagtail accessibility statistics for GAAD 2026
Wagtail accessibility statistics for GAAD 2026 give a focused look at how well your CMS setup supports real accessibility needs. Use the figures to spot gaps and prioritize the most impactful improvements.
Updates to Django
Today, "Updates to Django" is presented by Pradhvan from Djangonaut Space! 🚀
Last week we had 16 pull requests merged into Django by 11 different contributors - including 2 first-time contributors!
Congratulations to somi and Kasey for having their first commits merged into Django - welcome on board! 🥳
This week's Django highlights: 🦄
- Deprecated
QuerySet.select_related()with no arguments, along with the corresponding admin options that relied on this implicit form. (#36593)
RedirectViewnow supports apreserve_requestattribute, letting redirects keep the original HTTP method and body by returning 307 or 308 instead of 302 or 301. (#37062)
- Admin actions are now also shown on the object edit page, allowing bulk actions to be triggered directly from the change form. (#12090)
- Fixed Oracle compound-query compilation by clearing unnecessary ordering from combined query components in unions and
ORDER BYwrappers. (#36938)
That's all for this week in Django development! 🐍🦄
Sponsored Link
Middleware, but for AI agents
Django middleware composes request handlers. Harnesses do the same for AI agents - Claude Code, Codex, Gemini in one coordinated system. Learn what a harness actually is, why it's a new primitive, and how to engineer one that holds in production. Apache 2.0, open source.

Articles
My experience at PyCon US 2026
A first-person look at PyCon US 2026 with takeaways for developers who care about Python and the community around it. Expect practical impressions from talks and the conference vibe, not a generic recap.
PyCon US 2026 Recap
Will Vincent from PyCharm (and this newsletter!) shares seven days of talks, sprints, and hallway track conversations from this year's event.
My First PyConUS Experience
Jon Gould from Foxley Talent relates his first experience, takeaways, and comparisons to DjangoCons.
PostgreSQL 19 Beta: The Four Features You'll Actually Feel
PostgreSQL 19 Beta brings four changes highlighted for real-world impact, with a focus on what developers will actually notice. Expect a practical walkthrough rather than a long list of release notes.
Core Dispatch #4
Core Dispatch recaps a packed few weeks in the Python core world, including the arrival of Python 3.15 beta 1, free-threading improvements, PEP 788 landing in CPython, and a wave of new core developer activity.
Anything that could go wrong, will. The excuse is optional.
A thoughtful take on Murphy's Law in software engineering: resilient teams don't avoid risk or ignore it, they design systems assuming failure will happen and plan accordingly.
My PyCon US 2026
A chronological recap of PyCon US 2026 in Long Beach, with live notes ranging from the first AI track talk on AI-assisted contributions and maintainer load to security updates, community building, and Djangonaut Space. Expect practical takeaways about how AI affects review and conflict in open source, plus plenty of Django community moments including "Django on the Med."
Events
Organizing DjangoCon Europe 2026: The Afterthoughts | Blog with LOGIC
Find practical after-the-fact takeaways from organizing DjangoCon Europe 2026, focused on the details people usually only notice after the event. A useful read for anyone planning Django community events or sharpening their conference workflow.
Videos
Tech Hiring has got a FRAUD problem!
Tech hiring can attract fraud, from fake postings to misleading recruiting signals. Keep an eye on red flags in job listings and interview processes so you can spot scams early and protect candidates.
Podcasts
Django Chat #204:How France Ditched Microsoft with Samuel Paccoud
France's shift away from Microsoft is tied to decisions and experiences Samuel Paccoud discusses. The focus is on what prompted the move and what it meant operationally for organizations involved.
Django Job Board
Founding Engineer at MyDataValue
Junior Software Developer (Apprentice) at UCS Assist
PyPI Sustainability Engineer at Python Software Foundation
Projects
mliezun/caddy-snake
Caddy plugin to serve Python apps
AvaCodeSolutions/django-email-learning
An open source Django app for creating email-based learning platforms with IMAP integration and React frontend components.
ehmatthes/gh-profiler
Examine a GitHub user's profile, to help quickly decide how much to invest in their contributions. Was discussed by many maintainers at PyCon US sprints.
22 May 2026 2:00pm GMT
Planet Twisted
Glyph Lefkowitz: Opaque Types in Python
Let's say you're writing a Python library.
In this library, you have some collection of state that represents "options" or "configuration" for a bunch of operations. Such a set of options is a bundle of potentially ever-increasing complexity. Thus, you will want it to have an extremely minimal compatibility surface, with a very carefully chosen public interface, that is either small, or perhaps nothing at all. Such an object conveys state and might have some private behavior, but all you want consumers to be able to do is build it in very constrained, specific ways, and then pass it along as a parameter to your own APIs.
By way of example, imagine that you're wrapping a library that handles shipping physical packages.
There are a zillion ways to do it ship a package. There are different carriers who can ship it for you. There's air freight, and ground freight, and sea freight. There's overnight shipping. There's the option to require a signature. There's package tracking and certified mail. Suffice it to say, lots of stuff.
If you are starting out to implement such a library, you might need an object called something like ShippingOptions that encapsulates some of this. At the core of your library you might have a function like this:
1 2 3 4 5 |
|
If you are starting out implementing such a library, you know that you're going to get the initial implementation of ShippingOptions wrong; or, at the very least, if not "wrong", then "incomplete". You should not want to commit to an expansive public API with a ton of different attributes until you really understand the problem domain pretty well.
Yet, ShippingOptions is absolutely vital to the rest of your library. You'll need to construct it and pass it to various methods like estimateShippingCost and shipPackage. So you're not going to want a ton of complexity and churn as you evolve it to be more complex.
Worse yet, this object has to hold a ton of state. It's got attributes, maybe even quite complex internal attributes that relate to different shipping services.
Right now, today, you need to add something so you can have "no rush", "standard" and "expedited" options. You can't just put off implementing that indefinitely until you can come up with the perfect shape. What to do?
The tool you want here is the opaque data type design pattern. C is lousy with such things (FILE, pthread_*_t, fd_set, etc). A typedef in a header file can easily achieve this.
But in Python, if you expose a dataclass - or any class, really - even if you keep all your fields private, the constructor is still, inherently, public. You can make it raise an exception or something, but your type checker still won't help your users; it'll still look like it's a normal class.
Luckily, Python typing provides a tool for this: typing.NewType.
Let's review our requirements:
- We need a type that our client code can use in its type annotations; it needs to be public.
- They need to be able to consruct it somehow, even if they shouldn't be able to see its attributes or its internal constructor arguments.
- To express high-level things (like "ship fast") that should stay supported as we add more nuanced and complex configurations in the future (like "ship with the fastest possible option provided by the lowest-cost carrier that supports signature verification").
In order to solve these problems respectively, we will use:
- a public
NewType, which gives us our public name... - which wraps a private class with entirely private attributes, to give us an actual data structure, while not exposing the constructor,
- a set of public constructor functions, which returns our
NewType.
When we put that all together, it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
As a snapshot in time, this is not all that interesting; we could have just exposed _RealShipOpts as a public class and saved ourselves some time. The fact that this exposes a constructor that takes a string is not a big deal for the present moment. For an initial quick and dirty implementation, we can just do checks like if options._speed == "fast" in our shipping and estimation code.
However, the main thing we are doing here is preserving our flexibility to evolve the related APIs into the future, so let's see how we might do that. For example, let's allow the shipping options to contain a concrete and specific carrier and freight method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
As a NewType, our public ShippingOptions type doesn't have a constructor. Since _RealShipOpts is private, and all its attributes are private, we can completely remove the old versions.
Anything within our shipping library can still access the private variables on ShippingOptions; as a NewType, it's the same type as its base at runtime, so it presents minimal1 overhead.
Clients outside our shipping library can still call all of our public constructors: shipFast, shipNormal, and shipSlow all still work with the same (as far as calling code knows) signature and behavior.
If you need to build and convey some state within your public API, while avoiding breakages associated with compatibility churn, hopefully this technique can help you do that!
Acknowledgments
Thanks for reading, and thank you to my patrons who are supporting my writing on this blog. If you like what you've read here and you'd like to read more of it, or you'd like to support my various open-source endeavors, you can support my work as a sponsor.
-
The overhead is minimal, but it is not completely zero. The suggested idiom for converting to a
NewTypeis to call it like a function, as I've done in these examples, but if you are wanting to use this pattern inside of a hot loop, you can use# type: ignore[return-value]comments to avoid that small cost. ↩
22 May 2026 12:33am GMT
21 May 2026
Django community aggregator: Community blog posts
Utrecht (NL) Python meetup summaries
I made summaries at the 4th PyUtrecht meetup (in Nieuwegein, at Qstars this time).
Qstars IT and open source - Derk Weijers
Qstars IT hosted the meeting. It is an infra/programming/consultancy/training company that uses lots of Python.
They also love open source and try to sponsor where possible.
One of the things they are going to open source (next week) is a "cable thermal model", a calculation method to determine the temperature of underground electricity cables. The Netherlands has a lot of net congestion... So if you can have a better grid usage by calculating the real temperature of cables instead of using an estimated temperature, you might be able to increase the load on the cable without hitting the max temperature. Coupled with "measurement tiles" that actually monitor the temperature.
They build it for one of the three big electricity companies in the Netherlands and got permission to open source it so that the other companies can also use it. They hope it will have real impact.
He explained an open source project he started personally: "the space devs". Integrating rocket launch data and providing an API. Now it has five core developers (and got an invitation to the biggest space conference, two years ago!)
Some benefits from writing open source:
- You build your own portfolio.
- You can try new technologies. Always nice to have the skill to learn new things.
- You improve your communication skills (both sending and receiving).
- You can make your own decisions.
- You write in the open.
- Perhaps you help others with your work.
- You could be part of a cummunity.
- It is your code.
How to start?
- Reach out to other communities.
- Read and improve documentation.
- Find good first issues.
- Be proactive.
- Don't be afraid to ask questions (and don't let negative comments discourage you).
When working on open source, make sure you take security serious. People nowadays like to use supply chain attacks via open source software. So use 2FA and look at your deployment procedure.
Learning Python with Karel - EiEi Tun H
What is Karel <https://github.com/alts/karel>)? A teaching tool/robot for learning programming. You need to steer a robot in an area and have it pick up or dump objects. And... in the meantime you learn how to use functions and loops.
Karel only has a turn_left() function. So if you want to have it turn right, it is handy to add a function for it:
def turn_right():
turn_left()
turn_left()
turn_left()
Simple, but you have to learn it sometime!
In her experience, AI can help a lot when learning to code: it explains stuff to you like you're a five-year-old, and that's perfect.
If you want to play with Karel: https://compedu.stanford.edu/karel-reader/docs/python/en/ide.html
JSON freedom or chaos; how to trust your data - Bart Dorlandt
For this talk, I'm pointing at the PyGrunn summary I made three weeks ago. I liked the talk!
Practical software architecture for Python developers - Henk-Jan van Hasselaar
There are several levels of architecture. Organization level. System level. Application, Code.
Cohesion: "the degree to which the elements inside a module belong together". What does it mean? Working towards the same goal or function. Together means something like distance. When two functions are in separate libraries, they're not together. It is also important for cognitive load.
Coupling: loose coupling versus high coupling. You want loose coupling, so that changes in one module don't affect another module.
You don't really have to worry about coupling and cohesion in existing systems that don't need to be changed. But when you start changing or build something new: take coupling/cohesion into account.
Software architecture is a tradeoff. Seperation of concerns is fine, but it creates layers and thus distance, for instance.
Python is one of the most difficult languages when it comes to clean coding and clean architecture. You're allowed to do so many dirty things! Typing isn't even mandatory...
He showed a simple REST API as an example. Database model + view. But when you change the database model, like a field name, that field name automatically changes in the API response. So your internal database structure is coupled to the function at the customer that consumes the API.
What you actually need to do is to have a better "contract". A domain model. In his example code, it was a Pydantic model with a fixed set of fields. A converter modifies the internal database model to the domain model.
You can also have services, generic pieces of code that work on domain models. And adapters to and from domain models, like converting domain models to csv.
Finding the balance is the software architect's job.
What is the least you should do as a software developer? At least to create a domain layer. Including a validator.
There was a question about how to do this with Django: it is hard. Django's models are everywhere. And you really need a clean domain layer...
21 May 2026 4:00am GMT
My PyCon US 2026
A timeline of my PyCon US 2026 journey, in Long Beach (US), told through the Mastodon posts I shared along the way.
21 May 2026 3:00am GMT
04 Apr 2026
Planet Twisted
Donovan Preston: Using osascript with terminal agents on macOS
Here is a useful trick that is unreasonably effective for simple computer use goals using modern terminal agents. On macOS, there has been a terminal osascript command since the original release of Mac OS X. All you have to do is suggest your agent use it and it can perform any application control action available in any AppleScript dictionary for any Mac app. No MCP set up or tools required at all. Agents are much more adapt at using rod terminal commands, especially ones that haven't changed in 30 years. Having a computer control interface that hasn't changed in 30 years and has extensive examples in the Internet corpus makes modern models understand how to use these tools basically Effortlessly. macOS locks down these permissions pretty heavily nowadays though, so you will have to grant the application control permission to terminal. But once you have done that, the range of possibilities for commanding applications using natural language is quite extensive. Also, for both Safari and chrome on Mac, you are going to want to turn on JavaScript over AppleScript permission. This basically allows claude or another agent to debug your web applications live for you as you are using them.In chrome, go to the view menu, developer submenu, and choose "Allow JavaScript from Apple events". In Safari, it's under the safari menu, settings, developer, "Allow JavaScript from Apple events". Then you can do something like "Hey Claude, would you Please use osascript to navigate the front chrome tab to hacker news". Once you suggest using OSA script in a session it will figure out pretty quickly what it can do with it. Of course you can ask it to do casual things like open your mail app or whatever. Then you can figure out what other things will work like please click around my web app or check the JavaScript Console for errors. Another very important tips for using modern agents is to try to practice using speech to text. I think speaking might be something like five times faster than typing. It takes a lot of time to get used to, especially after a lifetime of programming by typing, but it's a very interesting and a different experience and once you have a lot of practice It starts to to feel effortless.
04 Apr 2026 1:31pm GMT
16 Mar 2026
Planet Twisted
Donovan Preston: "Start Drag" and "Drop" to select text with macOS Voice Control
I have been using macOS voice control for about three years. First it was a way to reduce pain from excessive computer use. It has been a real struggle. Decades of computer use habits with typing and the mouse are hard to overcome! Text selection manipulation commands work quite well on macOS native apps like apps written in swift or safari with an accessibly tagged webpage. However, many webpages and electron apps (Visual Studio Code) have serious problems manipulating the selection, not working at all when using "select foo" where foo is a word in the text box to select, or off by one errors when manipulating the cursor position or extending the selection. I only recently expanded my repertoire with the "start drag" and "drop" commands, previously having used "Click and hold mouse", "move cursor to x", and "release mouse". Well, now I have discovered that using "start drag x" and "drop x" makes a fantastic text selection method! This is really going to improve my speed. In the long run, I believe computer voice control in general is going to end up being faster than WIMP, but for now the awkwardly rigid command phrasing and the amount of times it misses commands or misunderstands commands still really holds it back. I've been learning the macOS Voice Control specific command set for years now and I still reach for the keyboard and mouse way too often.
16 Mar 2026 11:04am GMT