Planet Python
Last update: May 24, 2026 04:43 AM UTC
May 24, 2026
Graham Dumpleton
Per-instance lru_cache using wrapt
Following on from the previous post on stateful decorators, there is another small addition in wrapt 2.2.0 worth a closer look. A new wrapt.lru_cache helper has been added that fixes the long-standing issues with using functools.lru_cache on instance methods.
The thing I want to emphasise up front is that wrapt.lru_cache is not a replacement for functools.lru_cache. The actual caching is still done by the standard library implementation, all of its keyword arguments are passed straight through, and the eviction behaviour is identical. What wrapt.lru_cache adds is a thin layer on top, built using wrapt's decorator machinery, that fixes how the underlying functools.lru_cache is applied when the decorated function turns out to be a method on a class.
What lru_cache gives you
functools.lru_cache is a small but very useful decorator. You wrap a function with it and the function's return values are remembered, keyed on the arguments, up to some maximum cache size. Repeat calls with the same arguments skip the function body and return the cached result.
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive(n):
print("computing", n)
return n * n
expensive(2)
expensive(2)
expensive(3)
Running this prints computing 2 and computing 3 once each. For pure functions of their arguments this is exactly what you want.
Where it falls apart
It is when you reach for the same decorator on an instance method that things start to go wrong. The standard library implementation has no concept of the wrapped function being a method, so it treats self as just another argument and includes it in the cache key. That single design choice causes three distinct problems.
Problem 1: instances share a single cache budget
from functools import lru_cache
class Computer:
@lru_cache(maxsize=2)
def compute(self, x):
return x * 2
a = Computer()
b = Computer()
a.compute(1)
a.compute(2)
b.compute(1)
b.compute(2)
print(a.compute.cache_info())
The cache is a single shared structure attached to Computer.compute. With maxsize=2, four distinct (self, x) pairs across the two instances are competing for two cache slots. cache_info() reports hits=0, misses=4, currsize=2. With one hundred instances and the default maxsize=128, each instance ends up with rather close to a single cache slot of its own.
Problem 2: cached instances cannot be garbage collected
Because self is part of the cache key, the cache holds a strong reference to it. The instance can never go out of scope while there is a cached entry for one of its method calls:
import gc, weakref
from functools import lru_cache
class Big:
@lru_cache
def compute(self, x):
return x
b = Big()
ref = weakref.ref(b)
b.compute(1)
del b
gc.collect()
print(ref())
ref() returns the original Big instance rather than None. It is still alive, kept around by the cache, with no easy way to find or release it short of calling Big.compute.cache_clear() and dropping every other instance's cached results along with it.
Problem 3: self must be hashable
Cache keys have to be hashable. The standard library implementation therefore requires that self is hashable too. Any class that defines __eq__ without also defining __hash__ is implicitly unhashable, and the decorator will fail at call time:
from functools import lru_cache
class Record:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return isinstance(other, Record) and self.name == other.name
@lru_cache
def upper(self):
return self.name.upper()
Record("a").upper()
That raises TypeError: unhashable type: 'Record'. None of the function's actual arguments are involved in the failure; it is purely about self.
The wrapt version
The wrapt.lru_cache helper sidesteps all three problems by recognising when the decorated callable is being invoked as a method, and arranging for a separate functools.lru_cache-wrapped helper to exist for each decorated method on each instance. The helper is stored directly on the instance under an attribute named after the wrapped method, so for a method called compute the cache lives at instance._lru_cache_compute. The cache key is built from the genuine arguments only, with self providing the lookup of which cache to use rather than being a participant in the key.
The same three examples now look like:
import wrapt
class Computer:
@wrapt.lru_cache(maxsize=2)
def compute(self, x):
return x * 2
a = Computer()
b = Computer()
a.compute(1)
a.compute(2)
b.compute(1)
b.compute(2)
print(a.compute.cache_info())
Each instance has its own cache for compute with the full maxsize=2 budget. The cache_info() call here returns the stats for the cache attached to a (hits=0, misses=2, currsize=2), not a shared total. Calling b.compute.cache_info() reports its own independent set of numbers. If Computer had several @wrapt.lru_cache methods then each would get its own per-instance cache, stored under a separate attribute (_lru_cache_compute, _lru_cache_other_method, and so on), with no contention between them.
The garbage collection case works correctly because each instance owns its own cache attributes, and when the instance is collected the caches stored on it go with it:
import gc, weakref
import wrapt
class Big:
@wrapt.lru_cache
def compute(self, x):
return x
b = Big()
ref = weakref.ref(b)
b.compute(1)
del b
gc.collect()
print(ref())
ref() now returns None.
And unhashable instances are fine, because self was never part of the cache key in the first place:
import wrapt
class Record:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return isinstance(other, Record) and self.name == other.name
@wrapt.lru_cache
def upper(self):
return self.name.upper()
print(Record("a").upper())
That prints A, with no TypeError.
For plain functions, class methods and static methods (where there is no per-instance state to keep separate) wrapt.lru_cache defers to a single shared functools.lru_cache, so the behaviour is indistinguishable from using functools.lru_cache directly:
@wrapt.lru_cache(maxsize=32)
def factorial(n):
return n * factorial(n - 1) if n else 1
What is and is not new here
To restate the point at the top of the post, none of this is a new caching algorithm. The eviction strategy, the cache statistics, the keyword arguments, the CacheInfo tuple, the cache_info() / cache_clear() / cache_parameters() methods are all functools.lru_cache, untouched. What wrapt.lru_cache adds is the descriptor-protocol-aware machinery to ensure that for instance methods, the right cache is created and consulted, with no global cache pollution, no reference leaks, and no hashability requirement on the instance.
This is the kind of problem wrapt exists to handle. The recommended way to write a decorator with wrapt gives you a uniform wrapper signature that knows whether it has been called as a function, instance method, class method or static method, and the lru_cache helper is essentially a small, focused use of that machinery to delegate to the standard library decorator in a way that respects the calling convention.
The lru_cache helper is documented over on the bundled decorators page, and the full release notes for the rest of wrapt 2.2.0 are in the changelog. The feature is available from 2.2.0 onwards, although as before it is worth grabbing the latest release from PyPi since there have been follow-up releases on the 2.2.x branch. Issues and questions go to the issue tracker on Github.
Stateful decorators in wrapt
A new version of wrapt was released earlier this week. Version 2.2.0 introduces a small helper that makes it noticeably easier to write decorators that need to keep state across calls. It is the kind of thing that does not look like much until you try to write the equivalent code without it, so it is worth a closer look.
The full release notes are in the changelog. What I want to walk through here is the stateful decorator side of the release, because it touches on something that has always been a bit awkward in plain Python.
Why a decorator might need state
The idea of a stateful decorator is straightforward enough. You attach a wrapper to a function, and the wrapper remembers something across invocations. Counting how many times the function has been called is the canonical example. Other examples include accumulating timing statistics, caching results in a way you want to inspect, tracking which arguments have been seen, or maintaining a registry of what the wrapped function has done.
The complication is not the bookkeeping itself, it is exposing the state back to the caller. If a decorator is purely passive and does its work without anyone ever needing to look at the internals, state can live in a closure and nobody is any the wiser. Once you decide that the user of the decorated function should be able to ask "how many times has this been called?", you need a way to reach into that state from the outside.
The closure approach
The simplest pattern in plain Python is to push state onto the wrapper function as an attribute:
import functools
def call_tracker(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
finally:
wrapper.call_count += 1
wrapper.call_count = 0
return wrapper
@call_tracker
def add(x, y):
return x + y
add(1, 2)
add(3, 4)
print(add.call_count)
Running this prints 2. That works fine for a regular function, but the moment you apply the same decorator to an instance method things get more subtle. The wrapper itself is still a function, so the descriptor protocol kicks in and self is passed through correctly. The state however lives on the single wrapper object that was created at class definition time, so it is shared across every instance of the class. Whether that is what you want depends on the use case, but you have no real control over it from the way the decorator is written.
The class approach
If you want to keep both the state and the wrapper logic together, the next natural step is to write the decorator as a class:
import functools
class CallTracker:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.call_count = 0
def __call__(self, *args, **kwargs):
try:
return self.func(*args, **kwargs)
finally:
self.call_count += 1
@CallTracker
def add(x, y):
return x + y
add(1, 2)
print(add.call_count)
This works for plain functions. The problem appears when the same decorator is applied to a method:
class Calculator:
@CallTracker
def add(self, x, y):
return x + y
Calculator().add(1, 2)
That raises TypeError: add() missing 1 required positional argument: 'y'. The reason is that Calculator.add is now a CallTracker instance rather than a function. When the attribute is looked up via an instance, the descriptor protocol does not kick in, because instances of user-defined classes are not descriptors by default. The Calculator instance is therefore never bound to self in the wrapped function, and the call sees x as 1 with no value for y.
You can fix this by adding a __get__ method to CallTracker to make it behave as a descriptor, but then you also need to think about whether each access creates a fresh bound version, how classmethod and staticmethod interact with it, what happens when the descriptor is accessed on the class versus the instance, and so on. There is a real amount of code involved in getting all of this right, and it is exactly the code that wrapt exists to provide.
Doing it with wrapt
wrapt handles the descriptor machinery for you. The recommended way to write a decorator with wrapt is to use @wrapt.decorator, which gives you a uniform wrapper signature across functions, instance methods, class methods and static methods. You always get wrapped, instance, args and kwargs, with instance set appropriately depending on how the call was made.
Before version 2.2.0, layering state on top of that meant a little bit of manual plumbing. You had to construct the state object yourself, write the wrapper to close over it, then explicitly attach the state to the wrapper after the fact so it could be reached from outside. Something like this:
import wrapt
class CallTracker:
def __init__(self):
self.call_count = 0
def __call__(self, func):
tracker = self
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
try:
return wrapped(*args, **kwargs)
finally:
tracker.call_count += 1
wrapped_func = wrapper(func)
wrapped_func.tracker = tracker
return wrapped_func
It is not exactly painful, but it is noisy. You have to remember to assign the state attribute, you have to alias self so the closure captures it rather than something else, and the actual interesting code (the try/finally) is buried under boilerplate.
The new helper
In wrapt 2.2.0 the same decorator can now be written like this:
import wrapt
class CallTracker:
def __init__(self):
self.call_count = 0
@wrapt.bind_state_to_wrapper(name="tracker")
@wrapt.decorator
def __call__(self, wrapped, instance, args, kwargs):
try:
return wrapped(*args, **kwargs)
finally:
self.call_count += 1
The __call__ method is defined directly with the standard wrapt decorator signature, with an extra self at the front so it can reach the state on the CallTracker instance. The @wrapt.bind_state_to_wrapper descriptor sits on top of @wrapt.decorator and takes care of two things. When __call__ is accessed via an instance of CallTracker, it returns a wrapper that knows about the right self. And when that wrapper is applied to a function, the CallTracker instance is automatically attached to the resulting wrapped function under the name supplied in the name argument.
Using it looks like:
@CallTracker()
def add(x, y):
return x + y
add(1, 2)
add(3, 4)
print(add.tracker.call_count)
The output is 2. Where the previous approaches forced a choice between keeping state with the decorator class and supporting methods correctly, wrapt lets you have both. Applied to an instance method, the same decorator just works:
class Calculator:
@CallTracker()
def add(self, x, y):
return x + y
calc = Calculator()
calc.add(1, 2)
calc.add(3, 4)
print(calc.add.tracker.call_count)
This also prints 2. The wrapper handles descriptor binding correctly, self is passed through to the underlying method, and the state attribute remains reachable on the bound version of the wrapper because attribute lookup on a bound function wrapper now falls through to the parent function wrapper. That last bit is another small change in 2.2.0 that I won't dwell on here, but without it the cleaner syntax above would not be reachable through an instance.
A little extra polish
One refinement worth pointing out is what to do when you want the decorator to be usable both with and without arguments. That is, the @CallTracker versus @CallTracker(call_count=100) distinction. Construction can be wrapped up in a static method on the class:
class CallTracker:
def __init__(self, call_count=0):
self.call_count = call_count
@wrapt.bind_state_to_wrapper(name="tracker")
@wrapt.decorator
def __call__(self, wrapped, instance, args, kwargs):
try:
return wrapped(*args, **kwargs)
finally:
self.call_count += 1
@staticmethod
def track(func=None, /, *, call_count=0):
tracker = CallTracker(call_count=call_count)
if func is None:
return tracker
return tracker(func)
You can now write either @CallTracker.track or @CallTracker.track(call_count=100) and get sensible behaviour in both cases. None of that is specific to wrapt, it is just the usual Python trick for optional-argument decorators, but it composes nicely with the rest.
Why this matters
The reason wrapt exists in the first place is that writing decorators that behave correctly across functions, instance methods, class methods and static methods is harder than it looks. The descriptor protocol, functools.wraps, the inspect module, and the time-honoured Python habit of "just stick it on the function as an attribute" all interact in slightly awkward ways once you try to combine them. The uniform wrapper signature in wrapt removes most of that friction.
What bind_state_to_wrapper adds is the last missing piece for the common case of a stateful decorator. The state lives on the decorator class, the wrapper has direct access to it via self, and the state is exposed back to callers through a named attribute on the wrapped object with no extra plumbing. Documentation for both pieces is over in the decorators guide and the examples page if you want to look at the full set of variations.
The feature is available in wrapt from version 2.2.0 onwards, although you should grab whatever the latest release is from PyPi since there have been follow-up releases on the 2.2.x branch since. If you are coming to this from the Wrapt version 2.0.0 announcement last year, it builds on the same BaseObjectProxy reshuffle that release prepared the ground for. As always, if you find any issues there is an issue tracker on Github.
May 23, 2026
EuroPython
Call for Onsite Volunteers: Make EuroPython 2026 Happen
We need volunteers to make EuroPython 2026 happen. And you might be exactly who we&aposre looking for!
Before sharing all the information, here is a personal story from me:
The first time I attended EuroPython in-person was as a volunteer. It was the first year after Covid, and I was nervous about traveling abroad for a conference where I didn&apost know anyone personally; there were only friendly faces from the previous year of volunteering online. When I volunteered online, it was easier. I could stay in my comfort zone. But stepping out of that zone to meet people face-to-face? That changed everything 🐍❤️
Those online faces became really good friends. Now I want to go for every EuroPython because I will get to meet them again. Volunteering with friends became such fun I didn&apost even notice that I was constantly stepping outside my comfort zone 💃
So, if you&aposre thinking of volunteering, just do it! You will meet awesome humans and have fun while helping people surrounded by positive vibes 💖

As a volunteer, you&aposre the face of the conference. Your job is to make sure everyone has a great time. We need volunteers to be welcoming, helpful, and collaborative; making sure everyone (including yourself) is comfortable and happy.
There are lots of different ways to help, depending on your interests and availability:
- Registration Desk: Check in attendees, hand out badges, answer questions
- T-Shirt Handout: Hand out awesome EuroPython merch to attendees
- Room Manager: Keep things running smoothly in talk rooms, ensure speakers are ready
- Session Chair: Introduce speakers, manage transitions, signal time, handle Q&A
- Greeter / Badge Check: Welcome people, check for badges at entry
- Runners: Help with whatever is needed at the moment!
You can sign up for as many or as few slots as you want. Even a couple of hours helps. We&aposd appreciate it if you could do more than one, but no pressure, whatever you can give is valuable.
In the volunteering form, tell us what sounds interesting. Get matched with a role that fits your skills and availability. Show up, help out, and be part of something amazing.
That&aposs it. No experience necessary. You don&apost need to be a Python expert. You just need to care about the community and be willing to help out. Whether that&aposs greeting people at the door, managing the schedule, troubleshooting tech issues, or making sure speakers have what they need - we have a place for you.
What do you get?
- 🎫 Free Ticket if you dedicate 10 hours or more (Tutorials + Conference)
- 👕Volunteer T-Shirt: Awesome EuroPython merch to keep and show off
- ⭐ <3 Forever: Featured on the EuroPython 2026 Team page
Check out this page for all the details, including descriptions of various roles: https://ep2026.europython.eu/volunteering/
And if you have more questions? Just reach out volunteers@europython.eu. We&aposre here to help.
🎁 Sponsor Spotlight
We&aposd like to thank Manychat for sponsoring EuroPython.
Manychat builds AI-powered chat automation for 1M+ creators and brands at real production scale.

👋 Stay Connected
Follow us on social media and subscribe to our newsletter for all the updates:
👉 Sign up for the newsletter: https://blog.europython.eu/portal/signup
- LinkedIn: https://www.linkedin.com/company/europython/
- X/Twitter: https://x.com/europython
- Mastodon: https://fosstodon.org/@europython
- Bluesky: https://bsky.app/profile/europython.eu
- Instagram: https://www.instagram.com/europython/
- YouTube: https://www.youtube.com/@EuroPythonConference
Hopefully, we’ll see you on this side soon 🔜 😉
Cheers,
Sangarshanan Veera, EuroPython 2026 Communications Team
Sign up for EuroPython Blog
The official blog of everything & anything EuroPython! EuroPython 2026 13-19 July, Kraków
No spam. Unsubscribe anytime.
May 22, 2026
Kay Hayen
Nuitka Release 4.1
This is to inform you about the new stable release of Nuitka. It is the extremely compatible Python compiler, “download now”.
This release adds many new features and corrections with a focus on async code compatibility, missing generics features, and Python 3.14 compatibility and Python compilation scalability yet again.
Bug Fixes
Python 3.14: Fix, decorators were breaking when disabling deferred annotations. (Fixed in 4.0.1 already.)
Fix, nested loops could have wrong traces lead to mis-optimization. (Fixed in 4.0.1 already.)
Plugins: Fix, run-time check of package configuration was incorrect. (Fixed in 4.0.1 already.)
Compatibility: Fix,
__builtins__lacked necessary compatibility in compiled functions. (Fixed in 4.0.1 already.)Distutils: Fix, incorrect UTF-8 decoding was used for TOML input file parsing. (Fixed in 4.0.1 already.)
Fix, multiple hard value assignments could cause compile time crashes. (Fixed in 4.0.1 already.)
Fix, string concatenation was not properly annotating exception exits. (Fixed in 4.0.2 already.)
Windows: Fix,
--verbose-outputand--show-modules-outputdid not work with forward slashes. (Fixed in 4.0.2 already.)Python 3.14: Fix, there were various compatibility issues including dictionary watchers and inline values. (Fixed in 4.0.2 already.)
Python 3.14: Fix, stack pointer initialization to
localspluswas incorrect to avoid garbage collection issues. (Fixed in 4.0.2 already.)Python 3.12+: Fix, generic type variable scoping in classes was incorrect. (Fixed in 4.0.2 already.)
Python 3.12+: Fix, there were various issues with function generics. (Fixed in 4.0.2 already.)
Python 3.8+: Fix, names in named expressions were not mangled. (Fixed in 4.0.2 already.)
Plugins: Fix, module checksums were not robust against quoting style of module-name entry in YAML configurations. (Fixed in 4.0.2 already.)
Plugins: Fix, doing imports in queried expressions caused corruption. (Fixed in 4.0.2 already.)
UI: Fix, support for
uv_buildin the--projectoption was broken. (Fixed in 4.0.2 already.)Compatibility: Fix, names assigned in assignment expressions were not mangled. (Fixed in 4.0.2 already.)
Python 3.12+: Fix, there were still various issues with function generics. (Fixed in 4.0.3 already.)
Clang: Fix, debug mode was disabled for clang generally, but only ClangCL and macOS Clang didn’t want it. (Fixed in 4.0.3 already.)
Zig: Fix,
--windows-console-mode=attach|disablewas not working when using Zig. (Fixed in 4.0.3 already.)macOS: Fix, yet another way self dependencies can look like, needed to have support added. (Fixed in 4.0.3 already.)
Python 3.12+: Fix, generic types in classes had bugs with multiple type variables. (Fixed in 4.0.3 already.)
Scons: Fix, repeated builds were not producing binary identical results. (Fixed in 4.0.3 already.)
Scons: Fix, compiling with newer Python versions did not fall back to Zig when the developer prompt MSVC was unusable, and error reporting could crash. (Fixed in 4.0.4 already.)
Zig: Fix, the workaround for Windows console mode
attachordisablewas incorrectly applied on non-Windows platforms. (Fixed in 4.0.4 already.)Standalone: Fix, linking with Python Build Standalone failed because
libHacl_Hash_SHA2was not filtered out unconditionally. (Fixed in 4.0.4 already.)Python 3.6+: Fix, exceptions like
CancelledErrorthrown into an async generator awaiting an inner awaitable could be swallowed, causing crashes. (Fixed in 4.0.4 already.)Fix, not all ordered set modules accepted generators for update. (Fixed in 4.0.5 already.)
Plugins: Disabled warning about rebuilding the
pytokensextension module. (Fixed in 4.0.5 already.)Standalone: Filtered
libHacl_Hash_SHA2from link libs unconditionally. (Fixed in 4.0.5 already.)Debugging: Disabled unusable unicode consistency checks for Python versions 3.4 to 3.6. (Fixed in 4.0.5 already.)
Python3.12+ Avoided cloning call nodes on class level which caused issues with generic functions in combination with decorators. (Added in 4.0.5 already.)
Python 3.12+: Added support for generic type variables in
async deffunctions. (Added in 4.0.5 already.)UI: Fix, flushing outputs for prompts was not working in all cases when progress bars were enabled. (Fixed in 4.0.6 already.)
UI: Fix, unused variable warnings were missing at C compile time when using
zigas a C compiler. (Fixed in 4.0.6 already.)Scons: Fix, forced stdout and stderr paths as a feature was broken. (Fixed in 4.0.6 already.)
Fix, replacing a branch did not accurately track shared active variables causing optimization crashes. (Fixed in 4.0.7 already.)
macOS: Fix, failed to remove extended attributes because files need to be made writable first. (Fixed in 4.0.7 already.)
Fix, dict
popandsetdefaultusing with:=rewrites lacked exception-exit annotations for un-hashable keys. (Fixed in 4.0.8 already.)Python 3.13: Fix, the
__parameters__attribute of generic classes was not working. (Fixed in 4.0.8 already.)Python 3.11+: Fix, starred arguments were not working as type variables. (Fixed in 4.0.8 already.)
Python2: Fix,
FileNotFoundErrorcompatibility fallback handling was not working properly. (Fixed in 4.0.8 already.)Compatibility: Fix, loop ownership check in value traces was missing, causing issues with nested loops.
Windows: Improved
--windows-console-mode=attachto properly handle console handles, enabling cases likeos.systemto work nicely.Python2: Fix, there was a compatibility issue where providing default values to the
mkdtempfunction was failing.Windows: Fix, there were spurious issues with C23 embedding in 32-bit MinGW64 by switching to
coff_objresource mode for it as well.Plugins: Fix, the
post-import-codeexecution could fail because the triggering sub-package was not yet available insys.modules.UI: Fix, listing package DLLs with
--list-package-dllswas broken due to recent plugin lifecycle changes.UI: Fix,
--list-package-exewas not working properly on non-Windows platforms failing to detect executable files correctly.UI: Handled paths starting with
{PROGRAM_DIR}the same as a relative path when parsing the--onefile-tempdir-specoption.Plugins: Followed multiprocessing
forkserverchanges for newer Python versions.Python 3.12+: Fix, generic class type parameters handling was incorrect.
Python 3.12: Fix, deferred evaluation of type aliases was failing.
Python 3.12+: Aligned
sumbuilt-in float summation with CPython’s compensated sum for better accuracy.Python 3.10+: Fix, uncompiled coroutine
throw()return handling was incorrect, restoring completed coroutine results viaStopIteration.valuerather than exposing them as ordinary return values to the outer await chain.Python 3.13+: Fix, uncompiled coroutine
cancel()/awaitsuspension handling was incorrect, improved to ensure integration compatibility.macOS: Made finding
create-dmgmore robustly by also checking the Homebrew path for Intel and fromPATHproperly.Compatibility: Fix, class frames were not exposing frame locals.
UI: Detected
static-libpythonproblems, which affected some forms of Anaconda.Distutils: Rejected
--projectmixed with--mainarguments as it is not useful.macOS: Fix,
zigfromPATHor fromziglangwas not being used.Distutils: Fix, the wrong
module-rootconfig value was being checked foruvbuild backend.macOS: Fix, was attempting to change removed (rejected) DLLs, which of course failed and errored out.
Python 3.14: Fix, tuple reuse was not fully compatible, potentially causing crashes due to outdated hash caches.
Fix, fake modules were still being attempted to located when imported by other code, which could conflict with existing modules.
Python 3.5+: Fix, failed to send uncompiled coroutines the sent in value in
yield from.Fix, older
gcccompilers lacking newer intrinsic methods had compilation issues that needed to be addressed.Standalone: Fix, multiphase module extension modules with post-load code were not working properly.
Fix, Avoid using the non-inline copy of
pkg_resourceswith the inline copy of Jinja2. These could mismatch and cause errors.Fix, loops could make releasing of previous values very unclear, causing optimization errors.
Fix,
incbinresource mode was not working with oldgccC++ fallback.Python 3.4 to 3.6: Fix, bytecode demotion was not working properly for these versions, also bytecode only files not working.
Plugins: Added a check for the broken
patchelfversions 0.10 and 0.11 to prevent breaking Qt plugins.Android: Allowed
patchelfversion 0.18 on Android.Windows: Fix, the header path for self uninstalled Python was not detected correctly.
Release: Fix, inclusion of the
pkg_resourcesinline copy for Python 2 to source distributions was missing.UI: Detected the OBS versions of SUSE Linux better.
Suse: Allowed using
patchelf0.18.0 there too.Python 3.11: Fix, package and module dicts were not aligned close enough to avoid a CPython bug.
Fix, unbound compiled methods could crash when called without an object passed.
Standalone: Fix, multiphase module extension modules with postload. (Fixed in 4.0.8 already.)
Onefile: Fix, while waiting for the child, it may already be terminated.
macOS: Removed existing absolute rpaths for Homebrew and MacPorts.
Python 3.14: Avoided warning in CPython headers.
Python 3.14: Followed allocator changes more closely.
Compatibility: Avoided using
pkg_resourcesfor Jinja2 template location for loading.No-GIL: Applied some bug fixes to get basic things to work.
Package Support
Standalone: Add support for newer
paddleversion. (Added in 4.0.1 already.)Standalone: Add workaround for refcount checks of
pandas. (Fixed in 4.0.1 already.)Standalone: Add support for newer
h5pyversion. (Added in 4.0.2 already.)Standalone: Add support for newer
scipypackage. (Added in 4.0.2 already.)Plugins: Revert accidental
os.getenvoveros.environ.getchanges in anti-bloat configurations that stopped them from working. Affected packages arenetworkx,persistent, andtensorflow. (Fixed in 4.0.5 already.)Standalone: Added missing DLLs for
openvino. (Added in 4.0.7 already.)Enhanced the package configuration YAML schema by adding the
relative_toparameter forfrom_filenamesDLL specification, avoiding error-prone purely relative paths.Standalone: Fix,
flet_desktopapp assets were missing, now preserving the packaged runtime and sidecar DLLs.Standalone: Added support for the
tyropackage.Standalone: Added data files for the
perfettopackage.Standalone: Added support for
anyioprocess forking.Standalone: Added support for the
plotly.graphpackage.Anaconda: Fix, dependencies for the
numpyconda package on Windows were incorrect.Plugins: Enhanced the auto-icon hack in PySide6 to use compatible class names.
Standalone: Fix, Qt libraries were duplicated with
PySide6WebEngine framework support on macOS.Plugins: Fix, automatic detection of
mypycruntime dependencies was including all top level modules of the containing package by accident. (Fixed in 4.0.5 already.)Anaconda: Fix,
delvewheelplugin was not working with Python 3.8+. This enhances compatibility with installed PyPI packages that use it for their DLLs. (Fixed in 4.0.6 already.)Plugins: Fix, our protection workaround could confuse methods used with
PySide6.
New Features
UI: Added the
--recommended-python-versionoption to display recommended Python versions for supported, working, or commercial usage.UI: Add message to inform users about
Nuitka[onefile]if compression is not installed. (Added in 4.0.1 already.)UI: Add support for
uv_buildin the--projectoption. (Added in 4.0.1 already.)Onefile: Allow extra includes as well. (Added in 4.0.2 already.)
UI: Add
nuitka-project-setfeature to define project variables, checking for collisions with reserved runtime variables. (Added in 4.0.2 already.)Scons: Added new option to select
--reproduciblebuilds or not. (Added in 4.0.6 already.)Python 3.10+: Added support for
importlib.metadata.package_distributions(). (Added in 4.0.8 already.)Plugins: Added support for the multiprocessing
forkservercontext. (Added in 4.0.8 already, for 4.1 Python 3.6 and earlier, as well as 3.14 support were added too.)Reports: Added structured resource usage (
rusage) performance information to compilation reports.Reports: Included individual module-level C compiler caching (
ccache/clcache) statistics in compilation reports.Added support for detecting and correctly resolving the Python prefix for the
PyEnv on HomebrewPython flavor.macOS: Added support for
rusageinformation for Scons.UI: Added the
__compiled__.extension_filenameattribute to give the real filename of the containing extension module.Windows: Added support for
--clangor ARM. (Added in 4.0.8 already.)Windows: Added support for resources names as not just integers, important when we copy them from template files.
MacPorts: Added basic support for this Python flavor. More work will be needed to get it to work fully though.
Optimization
Avoid including
importlib._bootstrapandimportlib._bootstrap_external. (Added in 4.0.1 already.)Linux: Cached the
syscallused for time keeping during compilation to avoid loadinglibcfor each trace. (Added in 4.0.8 already.)UI: Output a warning for modules that remain unfinished after the third optimization pass.
Added an extra micro pass trigger when new variables are introduced or variable usage changes severely, ensuring optimizations are fully propagated, avoiding unnecessary extra full passes.
Provided scripts to compile Python statically with PGO tailored for Nuitka on Linux, Windows, and macOS.
Added support for running the Data Composer tool from a compiled Nuitka binary without spawning an uncompiled Python process.
Enhanced the usage of
vectorcallforPyCFunctionobjects by directly checking for its presence instead of relying purely on flags, allowing more frequent use of this faster execution path.Cached frequently used declarations for top-level variables to speed up C code generation.
Sped up trace collection merging by avoiding unnecessary set creation and using a set instead of a list for escaped traces.
Optimized plugin hook execution by tracking overloaded methods and added an option to show plugin usage statistics.
Improved performance of module location by avoiding unnecessary module name reconstruction and redundant filesystem checks for pre-loaded packages.
Improved the caching of distribution name lookups to effectively avoid repeated IO operations across all package types.
Plugins: Cached callback plugin dispatch for
onFunctionBodyParsingandonClassBodyParsingto skip argument computation when no plugin overrides them.Python 3.13: Handled sub-packages of
pathlibas hard modules.Handled hard attributes through merge traces as well.
Made constant blobs more compact by avoiding repeated identifiers and unnecessary fields.
Enhanced Python compilation scripts further. (Fixed in 4.0.8 already.)
Recognized late incomplete variables better. (Fixed in 4.0.8 already.)
Made constant blobs more compact. (Fixed in 4.0.8 already.)
Optimized calls with only constant keywords and variable posargs too.
Anti-Bloat
Fix, memory bloat occurred when C compiling
sqlalchemy. (Fixed in 4.0.2 already.)Avoid using
pydocinPySimpleGUI. (Added in 4.0.2 already.)Avoided using
doctestfromzodbpickle. (Added in 4.0.5 already.)Avoided inclusion of
cythonwhen usingpyav. (Added in 4.0.7 already.)Avoided including
typing_extensionswhen usingnumpy. (Added in 4.0.7 already.)
Organizational
UI: Relocated the warning about the available source code of extension modules to be evaluated at a more appropriate time.
Debian: Remove recommendation for
libfuse2package as it is no longer useful.Debian: Used
platformdirsinstead ofappdirs.Debugging: Removed Python 3.11+ restriction for
clang-formatas it is available everywhere, even Python 2.7, and we still want nicely formatted code when we read things. (Added in 4.0.6 already.)Removed no longer useful inline copy of
wax_off. We have our own stubs generator project.Release: Added missing package to the CI container for building Nuitka Debian packages.
Developer: Updated AI instructions for creating Minimal Reproducible Examples (MRE) to skip unneeded C compilation.
Debugging: Added an internal function for checking if a string is a valid Python identifier.
AI: Added a task in Visual Studio Code to export the currently selected Python interpreter path to a file, making it available as “python” and “pip” matching the selected interpreter. This makes it easier to use a specific version with no instructions needed.
AI: Updated the rules to instruct AI to only generate useful comments that add context not present in the code.
Containers: Added template rendering support for Jinja2 (
.j2) container files in our internal Podman tools.Projects: Clarified the current status and rationale of Python 2.6 support in the developer manual.
Debugging: Added experimental flag
--experimental=ignore-extra-micro-passto allow ignoring extra micro pass detection.Visual Code: Added integration scripts for
bashandzshautocompletion of Nuitka CLI options. These are now also integrated into Visual Studio Code terminal profiles and the Debian package.RPM: Included the Python compile script for Linux.
RPM: Removed the requirement for
distutilsin the spec.
Tests
Install only necessary build tools for test cases.
Avoided spurious failures in reference counting tests due to Python internal caching differences. (Fixed in 4.0.3 already.)
Fix, the parsing of the compilation report for reflected tests was incorrect.
Python 3.14: Ignored a syntax error message change.
Python 3.14: Added test execution support options to the main test runner to use this version as well.
Fix, the runner binary path was mishandled for the third pass of reflected compilations.
Removed the usage of obsolete plugins in reflected compilation tests.
Debugging: Prevented boolean testing of
namedtuplesto avoid unexpected bugs.Added the
Testsuffix to syntax test files and disabled “python” mode and spell checking for them to resolve issues reported in IDEs.Fix, newline handling in diff outputs from the output comparison tool was incorrect.
Covered
post-import-codefunctionality with a new subpackage test case.Prevented the program test suite from running an unnecessary variant to save execution time.
macOS: Ignored differences from GUI framework error traces in headless runs in output comparisons.
Reflected test for Nuitka, where it compiles itself and compares its operation has been restored to functional state.
Used the new method to clear internal caches if available for reference counts.
Disabled running nested loops test with Python 2.6.
Containers: Detected Python 2 defaulting containers in Podman tooling.
Cleanups
UI: Fix, there was a double space in the Windows Runtime DLLs inclusion message. (Fixed in 4.0.1 already.)
Onefile: Separated files and defines for extra includes for onefile boot and Python build.
Scons: Provided nicer errors in case of “unset” variables being used, so we can tell it.
Refactored the process execution results to correctly utilize our
namedtuplesvariant, that makes it easier to understand what code does with the results.Quality: Enabled automatic conversion of em-dashes and en-dashes in code comments to the autoformat tool. AI won’t stop producing them and they can cause
SyntaxErrorfor older Python versions, nor is unnecessarily using UTF-8 welcome.Ensured that cloned outline nodes are assigned their correct names immediately upon creation, that avoids inconsistencies during their creation.
Quality: Updated to the latest versions of
blackand adopted a fasterisortexecution by caching results.Quality: Modified the PyLint wrapper to exit gracefully instead of raising an error when no matching files require checking.
Quality: Avoided checking YAML package configuration files twice, since autoformat already handles them.
Quality: Ensured that YAML package configuration checks output the original filename instead of the temporary one when a failure occurs.
Quality: Prevented pushing of tags from triggering git pre-push quality checks.
Quality: Silenced the output of
optipngandjpegoptimduring image optimization auto-formatting.Visual Code: Added the generated Python alias path file to the ignore list.
Quality: Enabled auto-formatting for the Nuitka devcontainer configuration file.
Watch: Avoided absolute paths in compilation to make reports more comparable across machines.
Quality: Changed
mdformatchecks to run only once and silently.Scons: Disabled format security errors in debug mode and moved Python-related warning disables into common build setup code.
Quality: Updated to the latest
deepdiffversion.Scons: Avoided MSVC telemetry since it can produce outputs that break CI.
Debugging: Enhanced non-deployment handler for importing excluded modules.
Split import module finding functionality into more pieces for enhanced readability.
Debugging: Added more assertions for constants loading and checking.
macOS: Dropped the
universaltarget arch.Debugging: Added more traces for deep hash verification.
Summary
This release builds on the scalability improvements established in 4.0, with enhanced Python 3.14 support, expanded package compatibility, and significant optimization work.
The --project option seems usable now.
Python 3.14 support remains experimental, but only barely made the cut, and probably will get there in hotfixes. Some of the corrections came in so late before the release, that it was just not possible to feel good about declaring it fully supported just yet.
Python Morsels
What types of exceptions should you catch?
The trickiest programming bugs are often caused by catching exceptions that you didn't mean to catch or handling exceptions in ways that **obfuscate the actual error that's occurring. Which exceptions should you catch and which should you leave unhandled?
Catching many exceptions at once
When catching an exception, it's generally considered a good idea to only catch exceptions if you understand their origin.
Here we have some code that catches many exception types at once.
We're catching a ValueError, a TypeError, a KeyError, and a NameError exception:
import csv
import datetime
import sys
def parse_date(date_string):
return datetime.date.fromisoformat(date_string)
[filename] = sys.argv[1:]
with open(filename) as csv_file:
reader = csv.DictReader(csv_file)
for n, row in enumerate(reader, start=1):
name = row["name"]
try:
start, end = parse_date(row["start"]), parse_date(row["end"])
except (ValueError, TypeError, KeyError, NameError) as e:
error = type(e).__name__
print(f"{error}: Invalid date on line {n}", file=sys.stderr)
continue
time = end - start
print(f"{name}: {time.days} days")
It's not entirely clear why it catches each of these types of exceptions.
When will a NameError be raised?
We probably shouldn't be catching …
Read the full article: https://www.pythonmorsels.com/what-types-of-exceptions-should-you-catch/
Real Python
The Real Python Podcast – Episode #296: Managing Polars Schema Issues & Profiling GitHub Users
How can you avoid schema problems in your Polars data pipeline when adding new columns? How can you quickly examine a GitHub user's profile to decide how much to invest in their contributions? Christopher Trudeau is back on the show this week with another batch of PyCoder's Weekly articles and projects.
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Quiz: Build a Tic-Tac-Toe Game With Python and Tkinter
In this quiz, you’ll test your understanding of Build a Tic-Tac-Toe Game With Python and Tkinter.
By working through this quiz, you’ll revisit how to design game logic with Python classes, lay out and update Tkinter widgets, and wire up button clicks to a handler through the event loop.
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
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. ↩
Bob Belderbos
What production AI agents actually require
Most "AI agents" shipping right now are demos wearing production paint. They answer questions fluently and break the moment they touch a workflow with money, state, or consequences.
The agent illusion
Multi-agent frameworks benchmark beautifully. Five specialist LLMs cooperate, the demo plays cleanly, the README has a diagram with arrows. Then someone wires the thing to a real billing system and it issues three refunds for the same chargeback because a tool call retried on a flaky network.
That gap is the actual job most agent tutorials skip.
When I review AI code, the same pattern keeps appearing. The LLM call is fine. The agent loop is fine. What is missing is the layer underneath: state, idempotency, audit, and a tool surface the agent cannot use to hurt you.
The unsexy layer
Systems answer four questions before the agent does anything:
- What did the agent already do? Persisted state, not "look at the conversation history."
- What happens if this action runs twice? Idempotency keys on every external effect.
- Who approved this? An audit log a human can read during a postmortem.
- Can I roll this back? A clear inverse for every irreversible operation, or a freeze before execution.
None of this is glamorous. It is also what separates a system that is a toy demo from one that can run mostly unsupervised in production.
The shape of that contract in code:
class ExpenseAction(BaseModel):
idempotency_key: str
requested_by: str
requested_at: datetime
approval_required: bool = True
dry_run: bool = True
payload: ExpensePayload
def submit(action: ExpenseAction, repo: ExpenseRepo) -> Result:
if repo.find_by_key(action.idempotency_key):
return Result.duplicate()
if action.dry_run:
return Result.preview(action.plan())
if action.approval_required and not action.is_approved():
return Result.pending_approval()
repo.persist(action)
return Result.ok(action.execute())
The agent does not call the side effect. It builds a typed plan. A function decides whether to run it.
State that survives retries
Agents need state management that works across restarts and network failures. The Telegram expense bot we build in our Agentic AI cohort program, uses context.user_data to track multi-step flows:
async def handle_expense_text(self, update, context):
text = update.message.text
result = self._preprocessor.preprocess(text)
if not result.is_valid:
await update.message.reply_text(f"Invalid: {result.error}")
return ConversationHandler.END
response = self._build_service().classify(result.text).response
# Store state for the callback handler
context.user_data["expense_description"] = result.text
context.user_data["classification_response"] = response
keyboard = build_category_confirmation_keyboard(
suggested_category=response.category,
all_categories=[c.value for c in ExpenseCategory],
)
await update.message.reply_text(
f"I categorized this as {response.category} ({response.total_amount} {response.currency}). Confirm or pick another category:",
reply_markup=keyboard,
)
return ConversationState.WAITING_FOR_CATEGORY
async def handle_category_selection(self, update, context):
query = update.callback_query
await query.answer()
# Retrieve state from previous handler
description = context.user_data.get("expense_description")
response = context.user_data.get("classification_response")
if description is None or response is None:
await query.edit_message_text("Session expired. Send expense again.")
return ConversationHandler.END
_, category = query.data.split(":", 1)
self._build_service().persist_with_category(
expense_description=description,
category_name=category,
response=response,
telegram_user_id=update.effective_user.id,
)
await query.edit_message_text(f"Saved as {category}!")
return ConversationHandler.END
The .get() with defensive error handling is what saves you when the bot restarts mid-conversation. No silent corruption, no half-written database rows. The user just has to resend their expense description and pick the category again. This is the work of production agents.
Tools the agent cannot trust
LLMs are undeterministic and hallucinate. Design your tool surface for mistrust:
- Narrow scopes.
read_expenseandflag_expenseare two tools, not one tool with a mode flag the LLM can flip. - Dry-run by default. Every write tool returns a plan first. The agent opts in to execute. You get human-in-the-loop (HITL) for free.
- Schema-validated inputs. Pydantic at the tool boundary so a malformed argument cannot reach your database.
- Explicit confirmation for anything destructive. The agent proposes, a human taps approve.
The agent is not the brain of your application. It is a planner that we acknowledge is fallible. The real logic lives in the tools, and the agent's job is to call them with valid inputs and ask for help when it is unsure.
Input validation before the LLM sees anything
Validate at system boundaries before user input reaches your tools. This prevents XSS, length attacks, and malformed data from consuming tokens:
from dataclasses import dataclass, field
import re
XSS_PATTERNS = ("<script", "javascript:", "onerror=", "onload=")
CURRENCY_SYMBOLS = {"$": "USD", "€": "EUR", "£": "GBP", "¥": "JPY"}
AMOUNT_PATTERN = re.compile(r"\d+([.,]\d+)?")
@dataclass
class PreprocessingResult:
text: str
is_valid: bool
warnings: list[str] = field(default_factory=list)
error: str | None = None
class InputPreprocessor:
def preprocess(self, text: str) -> PreprocessingResult:
text = text.strip()
if len(text) < 3:
return PreprocessingResult(text, False, error="Input too short")
if len(text) > 500:
return PreprocessingResult(text, False, error="Input too long")
if any(pattern in text.lower() for pattern in XSS_PATTERNS):
return PreprocessingResult(text, False, error="Invalid characters")
for symbol, code in CURRENCY_SYMBOLS.items():
text = text.replace(symbol, code)
warnings = []
if not AMOUNT_PATTERN.search(text):
warnings.append("No amount detected")
return PreprocessingResult(text, True, warnings)
This runs before the LLM call, returning error messages without burning tokens or risking injection.
Human-in-the-loop as a design pattern
Production agents are not fully autonomous. They classify, extract, or suggest, then wait for a human to confirm. Confidence scores guide when to ask:
from dataclasses import dataclass
@dataclass(frozen=True)
class ClassificationResult:
response: ExpenseCategorizationResponse
persisted: bool
def process_with_hitl(result: ClassificationResult, threshold: float = 0.8) -> str:
if result.response.confidence >= threshold:
return result.response.category
print(
f"Low confidence ({result.response.confidence:.0%}): '{result.response.category}' — {result.response.reason}"
)
user_input = input(
f"Accept '{result.response.category}'? (Enter to confirm, or type a category): "
).strip()
if not user_input:
return result.response.category
return user_input
In the Telegram bot, this becomes an inline keyboard. The bot states its category guess and asks the human to confirm or pick a different one, with the AI suggestion highlighted.

The pattern: AI proposes, human disposes. This surfaces in the service layer we built in prior weeks:
@dataclass
class ClassificationService:
assistant: Assistant
expense_repo: ExpenseRepository
def classify(self, description: str) -> ClassificationResult:
messages = self._build_messages(description)
response = self.assistant.completion(messages)
return ClassificationResult(response=response, persisted=False)
def persist_with_category(
self,
expense_description: str,
category_name: str,
response: ExpenseCategorizationResponse,
telegram_user_id: int | None = None,
) -> None:
"""Store the user's chosen category, not the AI guess."""
expense = Expense(
amount=response.total_amount,
currency=response.currency,
category=ExpenseCategory(category_name),
description=expense_description,
telegram_user_id=telegram_user_id,
)
self.expense_repo.add(expense)
The persist_with_category method accepts the human's decision. The database stores what the user confirmed, not what the model guessed. As the ExpenseCategorizationResponse captures the AI's original category and confidence, we can analyze overrides later to identify model weaknesses.
Dependency injection for testable agents
The service layer pattern separates business logic from LLM provider details. Inject dependencies rather than hardcoding them:
from unittest.mock import create_autospec
from decimal import Decimal
def test_classify_calls_assistant():
# No real OpenAI call, no .env file, no network
mock_assistant = create_autospec(Assistant)
mock_assistant.completion.return_value = ExpenseCategorizationResponse(
category="Food",
total_amount=Decimal("5.50"),
currency=Currency.USD,
confidence=0.95,
cost=Decimal("0.001"),
)
mock_repo = create_autospec(ExpenseRepository)
service = ClassificationService(assistant=mock_assistant, expense_repo=mock_repo)
result = service.classify("Coffee at Starbucks $5.50")
mock_assistant.completion.assert_called_once()
assert result.response.category == "Food"
assert result.persisted is False
Because the service receives its dependencies rather than creating them, you can test classification logic without burning API credits or waiting on network calls. This is a key strategy to test the interface at the service layer, not the LLM provider.
The same service powers the CLI, Telegram bot, and REST API. Change providers (OpenAI to Anthropic) or add caching by swapping the Assistant implementation. Business logic stays untouched.
Speed vs safety
The tradeoff is iteration speed vs execution safety.
Put the LLM behind a typed service boundary and you can swap models without touching business logic. Store actions as events instead of overwriting state, and your audit log writes itself. I wrote about why event sourcing pays off.
Agentic loops with typed tool results
The tool-use loop needs to handle partial results, retries, and tool failures. Here is the pattern from the warm up exercises you can do on our Agentic Cohort page:
from typing import cast
import anthropic
from anthropic.types import (
MessageParam,
TextBlock,
ToolUseBlock,
ToolResultBlockParam,
)
# TOOLS defined with JSON schema for get_exchange_rate(from_currency, to_currency)
def answer_with_tools(question: str, client: anthropic.Anthropic) -> str:
messages: list[MessageParam] = [{"role": "user", "content": question}]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
tools=TOOLS,
messages=messages,
)
if response.stop_reason == "end_turn":
return cast(TextBlock, response.content[0]).text
if response.stop_reason != "tool_use":
# anything other than tool_use here means no tool calls to process — looping would spin forever
raise RuntimeError(f"Unexpected stop reason: {response.stop_reason}")
tool_uses = [
cast(ToolUseBlock, b) for b in response.content if b.type == "tool_use"
]
tool_results: list[ToolResultBlockParam] = [
{
"type": "tool_result",
"tool_use_id": b.id,
"content": str(get_exchange_rate(**cast(dict[str, str], b.input))),
}
for b in tool_uses
]
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
The loop continues until stop_reason == "end_turn". Tool results are typed, preventing schema drift between the tool definition and implementation.
In production, wrap get_exchange_rate() in a try/except and return error results to the LLM when tools fail. The agent can retry, pick a different tool, or surface the error to the user.
The fix is separation of concerns, typed interfaces, and a well-defined contract between the agent and its tools.
Keep reading
- How an AI expense agent is actually structured
- Build the data layer before you touch the LLM
- Stop prompting, start structuring your AI workflow
May 21, 2026
The Python Coding Stack
How I Learn (2026 Version) • My Tutor Agent
I know how I like to learn new things. Over the years, I figured out what works for me and what doesn’t. If you read my articles or attend my courses, then you know how I like to learn since I teach in the same way.
The challenge when learning something new is finding resources that are just right for me. And that’s not easy. I know I can learn things better and quicker with resources that fit my style well, but you can’t always find these resources.
I recently got particularly annoyed learning about the biomechanics of sprinting – I do have non-Python interests, yes – because all three textbooks I read, and lots of the online writing in this field, are just, let’s say, not great.
But I now found the solution.
After many decades of learning in the same way, I have now upgraded how I learn thanks to my new tutor, Priya.
Yes, I gave her a name. No, she’s not a real person. Priya is my personalised tutor agent. I’ll tell you all about her below.
And you’ll experience her teaching, too (not on the Python articles, though, I’ll keep writing those the old-fashioned way.) I’ll tell you more about this below, too, but let me first tell you why this works for me.
My Tutor, My Style
I’ve been thinking about the way I learn and teach for many years, from way back when I was a young University lecturer faced with 120 students in a lecture hall. I wasn’t that much older than the students, but I learnt fast. And they liked my teaching (I even have awards to prove it!)
More recently, I’ve been writing a lot. I wrote articles here on The Python Coding Stack and elsewhere. I wrote a Python textbook. I even wrote about learning and technical writing in Breaking the Rules: the substack and the book.
All this meant that I could ask my freshly-spawned agent to spend a bit of time reading what I wrote to understand how I teach, which is how I like to learn. Priya analysed the techniques I use in my writing and understood my motivations for doing what I do through my technical writing texts.
Then, Priya and I had a good chat to refine ideas, to make sure she captured the essence of “my style”.
And since Priya is an AI agent, “my style” became her knowledge base. This knowledge now lives in several lengthy markdown files and is summarised in shorter context packs and an index to ensure Priya’s short-term memory (the context window) isn’t overwhelmed.
Then I was ready to go. Any topic I wanted to learn, large or small, I could ask Priya to research it thoroughly, creating a new set of knowledge files, this time specific to the topic she needed to teach rather than my learning style. And then, she’s ready to teach me.
And it worked. The stuff she prepared was exactly the way I like it.
The Tutor-Student Conversation Course
And here’s the format I settled on (for now). Once the agent completes her research about the topic I want to learn, I ask her to plan a course spanning several modules.
But here’s the refinement loop that makes the real difference:
I ask Priya to draft the first module. She writes this in a markdown file.
I read through her draft and leave comments and questions directly within the text.
Priya reads my questions and revises the text to address my questions. (But read on to find out more about the two categories of comments/questions I leave for her.)
Repeat steps 2 and 3 until I feel I understand the topic.
Move on to the next module and repeat steps 1 to 4.
This is a human-in-the-loop approach to creating the learning material. Yes, Priya is trained in my way of learning and teaching and in my writing style. But I’m actively having a conversation with her within the text.
This is equivalent to raising your hand in a lesson and asking the teacher a question. A good teacher will then revise how they present the material to address your question.
Priya’s learning materials are just like that. In fact, I will take credit for her output. Sure, I’m not an expert in the subject matter she’s teaching me – that’s the whole point, right? But the output reflects my views and ideas about teaching and includes my questions and queries as I tried to understand and master the topic.
This is a collaboration. Priya and I are co-authors, even though Priya did most of the “writing”.
I tried this approach on several topics, but there are two I want to share with you. I’m setting up two new sections here on The Python Coding Stack, which I’ll use to learn these two topics in public. I’ll publish the “transcripts” of the conversations Priya and I are having. It’s mostly Priya doing the talking, but my questions are there, too.
The first topic I’m learning in public with Priya’s help is Agentic AI. It’s very meta to use agentic AI to learn about agentic AI! I’ll publish an introduction and the first module in the coming days in the new section here on The Python Coding Stack called Agents Unpacked. You can already see this section in the menu on the homepage.
I’ll set up another section to deal with the second topic in a week or so. No spoilers for now except to say it’s directly related to programming but it’s distinct from the articles I publish in the main section on The Python Coding Stack and in The Club.
By the way, you’ll be able to select which sections you want to receive regularly by email. So if you’re interested in my Python core content but not in these other topics, you can pick and choose what to opt out of. You can always go to The Python Coding Stack to read the other sections, of course.
How Priya and I Create These “Courses”
But let me expand on how Priya – my tutor agent – and I created these courses. [Incidentally, those are my em-dashes – I use them often and have always done. Commas would be ambiguous in that context!]
I provide two types of questions or comments to my agent as I read through the drafts: private and public.
Private Questions and Comments
When Priya reads the private questions or comments, she makes changes to the text, but then she deletes my input. So, you won’t see my intervention explicitly in these cases. However, Priya’s text reflects my thoughts. My interventions guide Priya. This type of intervention is similar to an editor’s role, but I’m intervening as a learner more than as an editor.
Public Questions and Comments
However, when Priya comes across a comment or question I mark as public, she leaves it in the text, acknowledges the question, and answers it directly. So, you’ll see my public questions in the text. Priya and I decided not to include too many of these public questions to keep the text flowing. However, I think it’s beneficial to see some of my interventions. My questions may also be your questions.
More Learning. More Articles. More Fun
As with everything to do with AI, this is all very new. It’s a work in progress. I may refine and revise how I interact with my agent. But it’s been fun learning this way, and I hope you enjoy reading my interactions with Priya and you find it useful, too.
To state the obvious, the posts I’ll publish in these two new sections are mostly AI-generated content. If you read this far, then you won’t be surprised by that statement. A year ago, I would never have thought I’d publish anything written by AI. But a year is a long time in the AI world. And this AI content reflects me and my thinking. The agent is my mentee – someone I trained to teach the way I do, to write the way I do. But she’s also my tutor, teaching me new stuff.
So there’s a lot of “me” in what you read, even if it’s mostly written by Priya!
The posts in the main section of The Python Coding Place and those in The Club (for premium subscribers) won’t change. They’re still my writing from beginning to end. Every word and letter you read in those posts is the result of nerve signals going from my brain to my fingers, which tap keys on a keyboard. In this era of AI doing a lot of work for us, I think it’s more important than ever for me to keep using my pre-AI skills. Otherwise, my brain will atrophy, and I don’t want that!
So, in summary, there will soon be four sections here on The Stack:
The main area in The Python Coding Stack – no change here, you’ll get the same type of Python articles you’ve been reading for the past 3+ years
The Club – the extra Python posts for premium subscribers
Agents Unpacked – the Agentic AI course Priya and I are creating for me to learn all about this agentic stuff. Learn with me (and Priya) if you’re interested.
Mystery Fourth Section – Stay tuned, you won’t have to wait long. This is also a Priya-Stephen collaboration.
Next post will be the introduction and first section in Agents Unpacked. Soon after, I have another Python post I’m planning for you.
Kevin Renskers
uv is fantastic, but its package management UX is a mess
UPDATE
May 22, 2026: This article hit the Hacker News front page. Readers pointed out a couple of things I’d missed and one bit of framing I should have been clearer about. See the Corrections and clarifications section at the bottom.
Astral’s uv has taken the Python world by storm, and for good reason. It is blisteringly fast, handles Python versions with ease, and replaces a half-dozen tools with a single binary. I’ve written multiple articles about it before.
Getting started with a new Python project using uv and adding your first dependencies is very easy. But once you move past the initial setup and into the maintenance phase of a project, i.e. checking for outdated packages and performing routine upgrades, the CLI starts to feel surprisingly clunky compared to its peers like pnpm or Poetry.
Finding outdated packages
In my JavaScript projects, if I want to see what needs an update, I run:
$ pnpm outdated This gives a clean, concise list of outdated packages, their current version, the latest version, and the version allowed by your constraints.
In uv, there is no uv outdated. Instead, you have to memorize the following mouthful:
$ uv tree --outdated --depth 1 The output is also a problem. It doesn’t just show you what is outdated; it shows you your entire top-level dependency tree, with a small annotation next to the ones that have updates available. If you have 50 dependencies and only two are outdated, you still have to scan a 50-line list.
Poetry isn’t much better with its command poetry show --outdated, but at least it only shows actual outdated packages.
Unsafe version constraints by default
This is the most significant philosophical departure uv takes from pnpm and Poetry, and it’s a dangerous one for production stability.
How pnpm/Poetry handle it
When you add a package using pnpm add, it writes it to package.json using the caret requirement (^1.23.4). The caret at the beginning means that any 1.x.x version is allowed, but it will not update to 2.0.0.
Poetry does the same by default, using a format like >=1.23.4,<2.0.0. I find this less readable than ^1.23.4, but the effect is the same.
In both cases, updates are safe by default. You can run pnpm update or poetry update every morning and have high confidence that your build won’t break due to a major API change (assuming the packages you depend on respect SemVer).
How uv handles it
When you run uv add pydantic, it inserts this into your pyproject.toml:
dependencies = [ "pydantic>=2.13.4", ] Note the lack of an upper bound. In the eyes of uv, pydantic version 2, 3, and 100 are all perfectly acceptable.
This means uv updates are unsafe by default. If you run a bulk update, you aren’t just getting bug fixes; you are opting into every breaking change published by every maintainer in your dependency graph.
The bad UX of the upgrade command
The commands to actually perform an update in uv feel like they were designed for machines rather than humans.
If you want to update everything in pnpm or Poetry, it’s a simple pnpm update or poetry update command. In uv, you use:
$ uv lock --upgrade THOUGHTS
Why isn’t this simply uv update or uv upgrade? Who designed this command line interface? It’s not uv lock --add or uv lock --remove either!
Because of the “no upper bounds” issue mentioned above, uv lock --upgrade is a nuclear option. It will upgrade every single package in your lockfile to their absolute latest versions, ignoring SemVer safety. And this includes deep, nested dependencies you’ve never heard of! Good luck, better hope there are no breaking changes anywhere.
Once you realize this is too risky, you’ll want to upgrade only specific packages. After scouring the subpar output of uv tree --outdated --depth 1 to find them, the syntax becomes a repetitive chore.
How pnpm does it:
$ pnpm update pydantic httpx uvicorn How uv does it:
$ uv lock --upgrade-package pydantic --upgrade-package httpx --upgrade-package uvicorn Having to repeat the --upgrade-package flag for every single item is a huge hassle when you want to update a bunch of packages. I don’t understand why the UX of uv’s commands is so poor.
There is hope: the bounds flag
Luckily uv has recently introduced a --bounds option for uv add:
$ uv add pydantic --bounds major This produces the safer pydantic>=2.13.4,<3.0.0 constraint we’ve come to expect. However, this is currently an opt-in feature. You have to remember to type it every time, and as of now, it is considered a preview feature.
Until --bounds major (or a similar configuration) becomes the default behavior, uv users are essentially forced to choose between two bad options:
- Manually edit
pyproject.tomlto add upper bounds for every single dependency. - Live in fear that
uv lock --upgradewill accidentally pull in a breaking major version change.
What I’d like to see
I love uv. Its speed is transformative, and the way it manages Python toolchains is second to none. But as a package manager, the developer experience for maintaining a project is currently a step backward from the tools that came before it.
We need a dedicated uv outdated command that filters noise, a more ergonomic update command that doesn’t require repeating flags, and default version constraints that respect the sanity of Semantic Versioning.
Until then, I’ll be double-checking every single line of my lockfile changes with a healthy dose of suspicion.
Corrections and clarifications
After this article hit Hacker News, readers pointed out two things I’d missed and one bit of framing I should have been clearer about up front.
-
Use
uv pip list --outdatedinstead ofuv tree --outdated --depth 1. Theuv pipcommand actually filters to only outdated packages, which makes the “Finding outdated packages” critique much weaker than I made it out to be. The remaining complaint is that this lives under the pip-compatibility namespace rather than as a first-class top-level command, which is a discoverability issue, not a noisy-output one. -
You can set the
--boundsdefault inpyproject.toml. You don’t have to remember to type--bounds majoron everyuv add. You can set it once:[tool.uv] add-bounds = "major"This invalidates the “two bad options” framing in the bounds-flag section. The actual situation is closer to: set this once in your config, and you get sensible defaults from then on. It’s still a preview feature, and for applications it would be better as the default, but the ergonomics are not nearly as bad as I painted them.
-
Scope: applications vs. libraries. The standard Python packaging advice is that libraries published to PyPI should not pin upper bounds, and that advice is correct. If every library pins upper bounds, downstream consumers end up with dependency trees that can’t resolve. But for applications, where you are the terminal node in the dependency graph and nobody resolves against your constraints, the calculus is reversed: upper bounds cost you nothing and protect you from surprise major version bumps. This article is about maintaining applications (websites, services, internal tools), not publishing libraries. I should have been explicit about that from the start, because the “no upper bounds” default is indeed reasonable for the library case.
Real Python
Quiz: Context Managers and Using Python's with Statement
In this quiz, you’ll test what you learned in the video course Context Managers and Using Python’s with Statement.
By working through this quiz, you’ll revisit how the with statement runs setup and teardown for you, how to use standard-library context managers like open(), and how to write your own context managers as classes or with the @contextmanager decorator.
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
PyCharm
Making software accessible often comes down to removing small but repeated points of friction in everyday workflows. Today, on Global Accessibility Awareness Day, we’re sharing recent improvements in JetBrains IDEs across several areas: compatibility with assistive technologies on various platforms, keyboard navigation, and non-visual feedback. Some of these improvements are already available, and some are coming later this year.
You can use the audio player below to listen to this blog post.
Accessibility Blog Post AudioBetter compatibility with assistive technologies
One of the key areas we’ve been working on is improving how JetBrains IDEs interact with OS-level accessibility tools.
Improved Magnifier support on Windows
Screen magnifiers are among the most commonly used assistive technologies in JetBrains IDEs. Until recently, the built-in Windows Magnifier didn’t reliably follow the text cursor in the editor, making navigation and editing more difficult for low-vision users. We’ve implemented support for cursor tracking so Magnifier follows text as you type, just as it does in other applications.
This builds on earlier work on macOS, where we addressed text cursor tracking with macOS Zoom. Now, the same support is being extended to Windows.
Orca and GNOME Magnifier support on Linux
With version 2026.2, coming this summer, JetBrains IDEs will allow you to use the Orca screen reader and GNOME Magnifier in supported Linux environments.
This is an active area of work, with multiple related tasks already underway. Accessibility shouldn’t depend on your operating system, and we’re continuing to improve support across platforms.
More predictable keyboard navigation
We’ve also been making it easier to move through the IDE without relying on a mouse.
Main menu access with Alt on Windows
In native Windows applications, pressing Alt moves the focus to the main menu, allowing you to navigate it with the keyboard. This behavior was previously missing from JetBrains IDEs, and screen readers, such as NVDA, would sometimes announce the system menu instead.
Now, the main menu behaves in a way that feels familiar and predictable for keyboard-only and screen-reader users, and the bright focus indicator helps low-vision users identify the selected item.
Navigating between major parts of the IDE
Another focus area is the experience of moving between different parts of the IDE interface, such as toolbars, panels, and the editor. We’re working on a more structured model for navigating through the big component groups:
- Tab and Shift+Tab move the focus within the current area.
- A dedicated shortcut lets you jump between larger sections of the IDE.
This reduces the effort required to reach essential controls and makes the overall layout easier to navigate. For the current iteration, we made it possible to bring the main toolbar and status bar into focus, and we fixed the Project and Git toolbar widgets, which were not selectable by screen readers, even though other elements already were.
As the next step, we’ll polish specific controls and include tool window bars on both sides of the IDE frame in the navigation flow.
Exploring richer non-visual feedback with audio cues
Accessibility is not only about reaching controls, but also about understanding what’s happening while you work. We’re exploring ways to provide richer audio feedback in the IDE. Two directions we’re currently investigating:
- Contextual signals when the caret lands on lines with errors, warnings, breakpoints, or version control changes. We want the IDE to provide immediate, non-visual feedback in context.
- More general audio notifications for IDE actions and state changes.
The goal is to reduce the need to rely on visual indicators or switch contexts just to understand what changed. Instead, we want the IDE to provide that information more directly.
Accessibility as an ongoing effort
We’re improving accessibility in JetBrains IDEs across multiple areas at once, including by providing compatibility with assistive technologies like screen readers and magnifiers, as well as by offering more consistent keyboard navigation and clearer feedback for events that are otherwise mostly visual.
These improvements build on earlier updates, such as support for VoiceOver and NVDA, a high-contrast UI theme, and color schemes for red-green vision deficiency. There’s still more to do, and we’ll continue working in this direction.
We’d love to hear from you
We’re eager to hear from developers who rely on accessibility features, as well as from anyone interested in improving the experience of using them.
If you have ideas or feedback about accessibility in JetBrains IDEs, you can reach us directly at accessibility@jetbrains.com. You can also report issues through YouTrack or the support request form.
If you’d like to stay informed about accessibility improvements, you can subscribe to updates here.
Improving Accessibility in JetBrains IDEs: What’s New and What’s Next in 2026
May 20, 2026
Talk Python Blog
Audit Your Python App Like Mozilla Audited Firefox
Earlier this year, Mozilla announced that they had pointed Claude at the Firefox JavaScript runtime. The agent surfaced more than 100 bugs, 14 of them serious enough to become CVEs. That is the kind of result you used to only get from an expensive pen-testing engagement, and even then it would take weeks. Reading that announcement, I kept circling back to one question: could a working Python web developer pull off the same kind of audit on their own app, without a security firm on retainer and without spending pen-testing-firm money? I built a course to answer that, and the short answer is yes.
Paolo Melchiorre
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.
Django Weblog
Django 6.1 alpha 1 released
Django 6.1 alpha 1 is now available. It represents the first stage in the 6.1 release cycle and is an opportunity to try out the changes coming in Django 6.1.
Django 6.1 offers a harmonious mélange of new features and usability improvements, which you can read about in the in-development 6.1 release notes.
This alpha milestone marks the feature freeze. The current release schedule calls for a beta release in about a month and a release candidate roughly a month after that. We'll only be able to keep this schedule with early and frequent testing from the community. Updates on the release schedule are available on the Django forum.
As with all alpha and beta packages, this release is not for production use. However, if you'd like to take some of the new features for a spin, or help find and fix bugs (which should be reported to the issue tracker), you can grab a copy of the alpha package from our downloads page or on PyPI.
The PGP key ID used for this release is Jacob Walls: 131403F4D16D8DC7
death and gravity
reader 3.24 released – help, multi-user updates
Hi there!
I'm happy to announce version 3.24 of reader, a Python feed reader library.
What's new? #
Here are the highlights since reader 3.23.
Context-sensitive help #
In lieu of a tutorial mode, the web app now offers guidance to new users, and has a basic context-sensitive help system. Here's some screenshots:
new user / empty state
context-sensitive help
also help
Structured logging #
reader now uses structured logging internally, through structlog.
By default, output goes to stdlib logging, but you can opt into structlog-native logging:
import reader, structlog
reader.enable_structlog()
structlog.configure(...)
This was relatively challenging to do, since as a library, you cannot configure logging, nor change any global state. I hope I can contribute a variant of the solution upstream, but meanwhile here's a recipe you can use in your library (warning: brittle code).
Make update_feeds() parallel again #
It turns out the "extensive rework of the parser internal API" from 3.15 caused update_feeds() to retrieve feeds in the main thread regardless of the worker count.
Protip
If you have a parallel map() that returns @contextmanagers,
make sure the work you need to do in parallel
doesn't happen in __enter__. 😅
New contributors #
Thank you to the new contributors that submitted pull requests to this release!
Want to contribute? Check out the docs and the roadmap.
Hosted reader status update #
As I said last time, I'm working on a hosted version of reader. Background: Why another feed reader web app?, Why not just self-host it?.
Multi-user feed updates #
One of the bigger changes for hosted reader was handling multi-user feed updates.
For intentional but questionable reasons, users have their own dedicated databases, with the web app routing to the appropriate one based on session information.
However, updating feeds should happen in a single, shared database; this allows:
- retrieving feeds once, not once per user
- per-host rate limiting
- preserving a longer history for public feeds
This is now done, complete with a design document (to be published). As a teaser, here's a neat architecture / data flow diagram:
... user@2.sqlite user nginx Flask auth app auth.sqlite user@1.sqlite public shared.sqlite feeds public private email yes, it's web scale ಠ_ಠOK, so what now? #
Since I'm rapidly running out of technical things to do, a launch is imminent.
This is what is finished so far:
- multi-user version of the web app
- authentication via email
- infrastructure deployments using pyinfra
- (new) multi-user feed updates
- (new) tutorial mode – context-sensitive help should do
Remaining work to an MVP:
- public demo
- landing page
- give it a good name
- launch announcement + roadmap
Meanwhile, if this sounds like something you'd like to use, get in touch.
That's it for now. For more details, see the full changelog.
Learned something new today? Share it with others, it really helps!
What is reader? #
reader takes care of the core functionality required by a feed reader, so you can focus on what makes yours different.
reader allows you to:
- retrieve, store, and manage Atom, RSS, and JSON feeds
- mark articles as read or important
- add arbitrary tags/metadata to feeds and articles
- filter feeds and articles
- full-text search articles
- get statistics on feed and user activity
- import / export feeds as OPML
- write plugins to extend its functionality
...all these with:
- a stable, clearly documented API
- excellent test coverage
- fully typed Python
To find out more, check out the GitHub repo and the docs, or give the tutorial a try.
Why use a feed reader library? #
Have you been unhappy with existing feed readers and wanted to make your own, but:
- never knew where to start?
- it seemed like too much work?
- you don't like writing backend code?
Are you already working with feedparser, but:
- want an easier way to store, filter, sort and search feeds and entries?
- want to get back type-annotated objects instead of dicts?
- want to restrict or deny file-system access?
- want to change the way feeds are retrieved by using Requests?
- want to also support JSON Feed?
- want to support custom information sources?
... while still supporting all the feed types feedparser does?
If you answered yes to any of the above, reader can help.
The reader philosophy #
- reader is a library
- reader is for the long term
- reader is extensible
- reader is stable (within reason)
- reader is simple to use; API matters
- reader features work well together
- reader is tested
- reader is documented
- reader has minimal dependencies
Real Python
How to Use the Claude API in Python
The fastest way to use the Claude API in Python is to install anthropic, set your API key, and call client.messages.create(). You’ll have a working response in under a minute:
Example of Using the Claude API in Python
Claude is Anthropic’s large language model, accessible via a clean REST API with an official Python SDK. Unlike heavier AI frameworks that require you to wire up multiple components before you see any output, the anthropic package gets you to a working response in a handful of lines.
In the following steps, you’ll install the anthropic SDK, call Claude from Python, shape Claude’s behavior with a system prompt, and then return structured JSON output using a schema or Pydantic.
Note: Claude’s responses are non-deterministic, so the same prompt produces different output each time, which is expected for a large language model. Also, API calls cost money based on the number of tokens processed. Keep an eye on your usage in the Claude Console as you follow along.
Each step builds on the last, and the final script is short enough to read in one sitting but complete enough to extend into a real application of your own.
Get Your Code: Click here to download the free sample code that shows you how to use the Claude API in Python.
Take the Quiz: Test your knowledge with our interactive “How to Use the Claude API in Python” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
How to Use the Claude API in PythonTest your understanding of using the Claude API in Python. Send prompts, set system instructions, and return structured JSON with a schema.
Prerequisites
Before diving in, make sure you have the following in place:
-
Python knowledge: You should be comfortable with Python basics, like defining functions, running scripts from the terminal, and working with virtual environments. If virtual environments are new to you, Python Virtual Environments: A Primer has you covered before you continue.
-
Python 3.9 or higher: The
anthropicSDK requires Python 3.9 as a minimum. If you’re not sure which version you have, runpython --versionin your terminal. If you need to install or upgrade, follow the steps in the guide on installing Python. -
An Anthropic account: You’ll need an Anthropic account to generate an API key in the Claude Console. Step 1 will show you how to find and secure your key once you’re in.
Don’t worry if you’ve never worked with an API before. This tutorial will walk you through authentication and help you make your first request from scratch.
Step 1: Set Up the Claude API in Python
Before you can call Claude from Python, you need an API key and the anthropic package installed. By the end of this step, you’ll have both, and Claude will be responding to your first prompt.
Get Your API Key and Install anthropic
Log in to the Claude Console or create a new account. If you’re starting fresh, you can begin using the API after adding $5 of credits.
Then navigate to the API Keys section. Click Create Key, give it a descriptive name like real-python-tutorial, and copy it immediately. You won’t see it again after you close the dialog.
Note: Never paste your API key directly into your code. Instead, store it as an environment variable. The anthropic SDK automatically reads it from ANTHROPIC_API_KEY at runtime, so you never need to reference it explicitly in your scripts.
Storing your key as an environment variable means it never touches your source code or version control history. The exact command depends on your operating system:
With your API key stored safely, you’re ready to install the SDK. Create a fresh virtual environment and activate it before installing anything. This isolation prevents the anthropic package from conflicting with your system-level tools.
Send Your First Prompt
Read the full article at https://realpython.com/claude-api-python/ »
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Quiz: How to Use the Claude API in Python
In this quiz, you’ll test your knowledge of How to Use the Claude API in Python.
By working through this quiz, you’ll revisit how to install the anthropic SDK, send prompts to Claude with client.messages.create(), shape responses with a system parameter, and return structured JSON output using a schema or Pydantic.
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Python GUIs
Adding QComboBox to a QTableView and getting/setting values after creation — Use QItemDelegate to embed combo boxes in your table views, with per-row data and value tracking
I'm using a QTableView to display data, and would like to limit the choices in some of the fields using a drop-down. I can use
QComboBoxto provide a list of choices in a normal UI, but how can I do that in a table view?
When you're working with QTableView in PyQt6, you'll sometimes want cells that offer a dropdown selection instead of plain text. A QComboBox is the natural fit here — but embedding one inside a table view takes a bit of wiring up.
In this tutorial, we'll walk through how to use a QItemDelegate to place a QComboBox into specific cells of a QTableView. We'll also cover how to populate each combo box with different items per row, and how to retrieve the selected value so you can use it elsewhere in your application.
How delegates work in Qt's Model/View framework
Qt's Model/View architecture separates your data (the model) from how it's displayed (the view). Between these two sits the delegate, which controls how individual cells are rendered and edited. When you want a cell to use a widget like a combo box instead of a plain text editor, you create a custom delegate.
The delegate has a few methods you'll override:
createEditor()— creates the widget (in our case, aQComboBox) when the user starts editing a cell.setEditorData()— populates the editor widget with the current data from the model.setModelData()— writes the user's selection back into the model.updateEditorGeometry()— makes sure the widget is sized and positioned correctly inside the cell.
Let's build this up step by step.
Setting up the model and view
First, let's create a simple application with a QTableView and a QStandardItemModel. Each row will represent a software package, and one of the columns will hold a list of available versions. We'll store those version lists directly in the model data, so each row can have its own set of options.
import sys
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QTableView, QComboBox, QItemDelegate,
)
from PyQt6.QtGui import QStandardItemModel, QStandardItem
from PyQt6.QtCore import Qt, QItemDataRole
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QComboBox in QTableView")
self.table = QTableView()
self.setCentralWidget(self.table)
# Create a model with 3 rows and 2 columns.
self.model = QStandardItemModel(3, 2)
self.model.setHorizontalHeaderLabels(["Package", "Version"])
# Each row has a package name and a list of available versions.
packages = [
("Widget Library", ["1.0", "1.1", "2.0", "2.1"]),
("Data Toolkit", ["0.9", "1.0"]),
("Render Engine", ["3.0", "3.1", "3.2", "4.0"]),
]
for row, (name, versions) in enumerate(packages):
# Column 0: package name (plain text).
self.model.setItem(row, 0, QStandardItem(name))
# Column 1: store the version list in the item's data.
# We use Qt.ItemDataRole.UserRole to keep the full list alongside the display text.
item = QStandardItem(versions[-1]) # Display the latest version by default.
item.setData(versions, Qt.ItemDataRole.UserRole)
self.model.setItem(row, 1, item)
self.table.setModel(self.model)
# Apply our custom delegate to column 1.
delegate = ComboDelegate(self.table)
self.table.setItemDelegateForColumn(1, delegate)
self.resize(400, 200)
Notice how we store the list of versions using Qt.ItemDataRole.UserRole. This is a custom data role — it lets us attach extra information to a model item without interfering with the text that's displayed (which uses Qt.ItemDataRole.DisplayRole). Each row gets its own version list, so when the combo box opens, it will show only the versions relevant to that row.
Creating the combo box delegate
Now let's write the ComboDelegate class. This is where the combo box gets created and connected to the model.
class ComboDelegate(QItemDelegate):
"""
A delegate that places a QComboBox in cells of the assigned column.
"""
def createEditor(self, parent, option, index):
# Create the combo box and populate it with the version list for this row.
combo = QComboBox(parent)
versions = index.data(Qt.ItemDataRole.UserRole)
if versions:
combo.addItems(versions)
return combo
def setEditorData(self, editor, index):
# Set the combo box to show the currently selected value.
current_text = index.data(Qt.ItemDataRole.DisplayRole)
idx = editor.findText(current_text)
if idx >= 0:
editor.setCurrentIndex(idx)
def setModelData(self, editor, model, index):
# Write the selected value back into the model.
model.setData(index, editor.currentText(), Qt.ItemDataRole.DisplayRole)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
Let's walk through each method:
createEditor() is called when the user double-clicks (or otherwise activates) a cell in column 1. We create a fresh QComboBox, pull the version list from Qt.ItemDataRole.UserRole for that specific row, and add those items to the combo box. Because each row stores its own list, different rows will show different options.
setEditorData() makes sure the combo box starts with the right item selected. We read the current display text from the model and find the matching entry in the combo box.
setModelData() fires when the user finishes editing (for example, by clicking away from the cell). It takes whatever the user selected in the combo box and writes it back into the model's DisplayRole.
updateEditorGeometry() simply ensures the combo box fills the cell neatly.
Running the application
Add the standard entry point at the bottom of your script:
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Run the script and double-click any cell in the "Version" column. You'll see a combo box appear with the version options for that specific row. Select a value, click away, and the cell updates.

Getting the selected value
After the user makes a selection, the value is stored in the model. You can read it at any time:
# Read the selected version for row 0.
selected = self.model.item(0, 1).text()
print(f"Row 0 selected version: {selected}")
If you want to react immediately when a selection changes, you can connect to the model's dataChanged signal. If you're new to how signals work in Qt, see our guide on signals, slots and events:
self.model.dataChanged.connect(self.on_data_changed)
def on_data_changed(self, top_left, bottom_right, roles):
if top_left.column() == 1:
row = top_left.row()
value = top_left.data(Qt.ItemDataRole.DisplayRole)
print(f"Row {row} version changed to: {value}")
This approach keeps things nicely separate — you're working through the model rather than trying to hold references to individual combo box widgets. The combo boxes are created and destroyed as the user interacts with cells.
Setting a value programmatically
To change a cell's value from code, update the model directly:
# Set row 2's version to "3.1".
self.model.item(2, 1).setText("3.1")
The next time the user opens the combo box on that row, the delegate's setEditorData() will position the combo box on "3.1".
You can also update the list of available versions for a row:
# Add a new version to row 1's options.
item = self.model.item(1, 1)
versions = item.data(Qt.ItemDataRole.UserRole)
versions.append("1.1")
item.setData(versions, Qt.ItemDataRole.UserRole)
Why each row gets its own combo box items
A common stumbling block is ending up with the same items in every combo box across the column. This happens when you store the item list on the delegate itself (as a single shared list) rather than on the model. Since the delegate is shared across all rows, any list stored on it will be the same everywhere.
The solution, as we've done here, is to store per-row data in the model using Qt.ItemDataRole.UserRole. Each call to createEditor() reads from the specific index it's given, so each row naturally gets its own set of options. This is a pattern you'll use often when different rows need different editor configurations.
Complete code
Here's the full working example in one block:
import sys
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QTableView, QComboBox, QItemDelegate,
)
from PyQt6.QtGui import QStandardItemModel, QStandardItem
from PyQt6.QtCore import Qt
class ComboDelegate(QItemDelegate):
"""
A delegate that places a QComboBox in cells of the assigned column.
"""
def createEditor(self, parent, option, index):
combo = QComboBox(parent)
versions = index.data(Qt.ItemDataRole.UserRole)
if versions:
combo.addItems(versions)
return combo
def setEditorData(self, editor, index):
current_text = index.data(Qt.ItemDataRole.DisplayRole)
idx = editor.findText(current_text)
if idx >= 0:
editor.setCurrentIndex(idx)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentText(), Qt.ItemDataRole.DisplayRole)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QComboBox in QTableView")
self.table = QTableView()
self.setCentralWidget(self.table)
self.model = QStandardItemModel(3, 2)
self.model.setHorizontalHeaderLabels(["Package", "Version"])
packages = [
("Widget Library", ["1.0", "1.1", "2.0", "2.1"]),
("Data Toolkit", ["0.9", "1.0"]),
("Render Engine", ["3.0", "3.1", "3.2", "4.0"]),
]
for row, (name, versions) in enumerate(packages):
self.model.setItem(row, 0, QStandardItem(name))
item = QStandardItem(versions[-1])
item.setData(versions, Qt.ItemDataRole.UserRole)
self.model.setItem(row, 1, item)
self.table.setModel(self.model)
delegate = ComboDelegate(self.table)
self.table.setItemDelegateForColumn(1, delegate)
# React to changes.
self.model.dataChanged.connect(self.on_data_changed)
self.resize(400, 200)
def on_data_changed(self, top_left, bottom_right, roles):
if top_left.column() == 1:
row = top_left.row()
value = top_left.data(Qt.ItemDataRole.DisplayRole)
print(f"Row {row} version changed to: {value}")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Wrapping up
Using a custom QItemDelegate gives you full control over how cells in a QTableView are edited. By storing per-row data in the model with Qt.ItemDataRole.UserRole, you can give each combo box its own set of items — solving the common problem of all combo boxes showing the same options.
The pattern here — store data in the model, read it in the delegate, write changes back to the model — works well beyond combo boxes. You can use the same approach to embed spin boxes, date pickers, or any other widget into your table cells. Once you're comfortable with this flow, you'll find Qt's Model/View framework surprisingly flexible. For a deeper dive into using QTableView with real-world data sources like NumPy and Pandas, see our QTableView with numpy and pandas tutorial. You can also explore how to make table cells editable for other common editing patterns.
For an in-depth guide to building Python GUIs with PyQt6 see my book, Create GUI Applications with Python & Qt6.
May 19, 2026
PyCoder’s Weekly
Issue #735: Agentic Architecture, Python is Weird, 3.15, and More (2026-05-19)
#735 – MAY 19, 2026
View in Browser »
Agentic Architecture: Why Files Aren’t Always Enough
What are the limitations of using a file-based agent workflow? Why do massive context windows tend to collapse? This week on the show, Mikiko Bazeley from MongoDB joins us to discuss agentic architecture and context engineering.
REAL PYTHON podcast
Python Is Weird
Here is a collection of things that surprised Maciej about Python. Some you might know and some that might surprise you too.
MACIEJ KOWALSKI
Harness Orchestration: The Next Primitive for AI Agents
A Python SDK that lets you compose Claude Code, Codex, and Gemini as one autonomous harness - agents become FastAPI-style routes you can wire, version, and deploy. Open source. Fork SWE-AF (a 100+ agent software factory) or our cloud-security harness as starter kits. Clone a Recipe →
AGENTFIELD sponsor
Python 3.15: Features That Didn’t Make the Headlines
Every release there are changes that don’t make the headlines, here are a few in the upcoming Python 3.15 release
CHANGS.CO.UK • Shared by Jamie Chang
DjangoCon US 2026 Tickets Available
DJANGOCON.US • Shared by Aayush Gauba
Articles & Tutorials
PyCon US 2026 Typing Summit Recap
Per-talk notes from the PyCon US 2026 Typing Summit. Includes info on: Pyrefly and AI agents, ty constraint sets, Lean formalization, tensor shape types, intersection types, PEP 827, Guido on the direction of typing, and the Typing Council Q&A.
BERNÁT GÁBOR
Event Sourcing Design Pattern
Talk Python interviews Chris May and they discuss the event sourcing design pattern: a mechanism for databases to work like git with immutable, replayable events. Learn what libraries help you do this in Python and when to use the pattern.
TALK PYTHON podcast
Strategic Planning at the PSF
The Python Software Foundation Board has been developing a strategic plan to guide the foundation’s direction over the next five years. This post describes the process and future goals.
PYTHON SOFTWARE FOUNDATION
How Python’s GIL Actually Works (And When It Bites You)
This post explains how Python’s GIL limits the amount of concurrency you can get through threading alone, why it is there, and how it is changing as Python evolves.
ATHREYA AKA MANESHWAR
Concurrency: A Deep Dive Into Multithreading With Python
“This article explains concurrency in Python including topics like multithreading, multiprocessing, race conditions, and synchronization mechanisms such as locks.”
NIKOS VAGGALIS
Shipping Django as a Desktop App
This is a summary of Jochen Wersdörfer’s talk at DjangoCon EU where he outlined how his team used Electron to turn a Django project into an installable app.
REINOUT VAN REES
Pydantic Forks httpx
The Pydantic team has forked httpx and named it httpx2. The folks who created httpxyz have decided to let the larger organization take the reins.
MICHIEL BEIJEN
How to Flatten a List of Lists in Python
Learn how to flatten a list of lists in Python using for loops, list comprehensions, itertools, functools, NumPy, and recursion.
REAL PYTHON
Building Type-Safe LLM Agents With Pydantic AI
Build type-safe LLM agents in Python with Pydantic AI using structured outputs, function calling, and dependency injection.
REAL PYTHON course
Pyrefly v1.0 Is Here!
Pyrefly has reached stable version 1.0 status, read about the new features and how to get started.
PYREFLY.ORG
Projects & Code
Events
PyData Bristol Meetup
May 21, 2026
MEETUP.COM
PyLadies Dublin
May 21, 2026
PYLADIES.COM
Python Sheffield
May 26, 2026
GOOGLE.COM
PyCon Italia 2026
May 27 to May 31, 2026
PYCON.IT
Python Southwest Florida (PySWFL)
May 27, 2026
MEETUP.COM
Happy Pythoning!
This was PyCoder’s Weekly Issue #735.
View in Browser »
[ Subscribe to 🐍 PyCoder’s Weekly 💌 – Get the best Python news, articles, and tutorials delivered to your inbox once a week >> Click here to learn more ]
Real Python
Tapping Into the Zen of Python
The Zen of Python is a collection of 19 aphorisms that capture the guiding principles behind Python’s design. You can display them anytime by running import this in a Python REPL. Tim Peters wrote them in 1999 as a joke, but they became an iconic part of Python culture that was even formalized as PEP 20.
By the end of this video course, you’ll understand:
- The Zen of Python is a humorous poem of 19 aphorisms describing Python’s design philosophy
- Running
import thisin a Python interpreter displays the complete text of the Zen of Python - Tim Peters wrote the Zen of Python in 1999 as a tongue-in-cheek comment on a mailing list
- The aphorisms are guidelines, not strict rules, and some intentionally contradict each other
- The principles promote readability, simplicity, and explicitness while acknowledging that practicality matters
Experienced Pythonistas often refer to the Zen of Python as a source of wisdom and guidance, especially when they want to settle an argument about certain design decisions in a piece of code. In this video course, you’ll explore the origins of the Zen of Python, learn how to interpret its mysterious aphorisms, and discover the Easter eggs hidden within it.
You don’t need to be a Python master to understand the Zen of Python! But you do need to answer an important question: What exactly is the Zen of Python?
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Quiz: Absolute vs Relative Imports in Python
In this quiz, you’ll test your understanding of Absolute vs Relative Imports in Python.
By working through this quiz, you’ll revisit how Python’s import system resolves modules, the differences between absolute and relative imports, and the PEP 8 conventions for styling import statements.
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Quiz: Tapping Into the Zen of Python
In this quiz, you’ll test your understanding of Tapping Into the Zen of Python.
By working through this quiz, you’ll revisit the origins of the poem, the meaning of several aphorisms, and the inside jokes hidden throughout.
The questions explore how the principles apply in practice and when it’s okay to bend the rules in the name of practicality.
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]


