24 May 2026

feedPlanet Python

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.

24 May 2026 4:30am GMT

Graham Dumpleton: 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.

24 May 2026 1:55am GMT

23 May 2026

feedPlanet Python

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 πŸ’–

alt

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:

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?

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.

alt

πŸ‘‹ 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

Hopefully, we'll see you on this side soon πŸ”œ πŸ˜‰

Cheers,

Sangarshanan Veera, EuroPython 2026 Communications Team

23 May 2026 7:00am GMT

22 May 2026

feedDjango community aggregator: Community blog posts

Issue 338: Django 6.1 alpha 1 released

News

Django 6.1 alpha 1 released

Django 6.1 alpha 1 has been released, signaling the next round of framework updates headed your way. Plan a quick test run in a staging environment so you can catch compatibility issues early as 6.1 develops.


Wagtail CMS News

Wagtail accessibility statistics for GAAD 2026

Wagtail accessibility statistics for GAAD 2026 give a focused look at how well your CMS setup supports real accessibility needs. Use the figures to spot gaps and prioritize the most impactful improvements.


Updates to Django

Today, "Updates to Django" is presented by Pradhvan from Djangonaut Space! πŸš€

Last week we had 16 pull requests merged into Django by 11 different contributors - including 2 first-time contributors!

Congratulations to somi and Kasey for having their first commits merged into Django - welcome on board! πŸ₯³

This week's Django highlights: πŸ¦„

That's all for this week in Django development! πŸπŸ¦„


Sponsored Link

Middleware, but for AI agents

Django middleware composes request handlers. Harnesses do the same for AI agents - Claude Code, Codex, Gemini in one coordinated system. Learn what a harness actually is, why it's a new primitive, and how to engineer one that holds in production. Apache 2.0, open source.


Articles

My experience at PyCon US 2026

A first-person look at PyCon US 2026 with takeaways for developers who care about Python and the community around it. Expect practical impressions from talks and the conference vibe, not a generic recap.

PyCon US 2026 Recap

Will Vincent from PyCharm (and this newsletter!) shares seven days of talks, sprints, and hallway track conversations from this year's event.

My First PyConUS Experience

Jon Gould from Foxley Talent relates his first experience, takeaways, and comparisons to DjangoCons.

PostgreSQL 19 Beta: The Four Features You'll Actually Feel

PostgreSQL 19 Beta brings four changes highlighted for real-world impact, with a focus on what developers will actually notice. Expect a practical walkthrough rather than a long list of release notes.

Core Dispatch #4

Core Dispatch recaps a packed few weeks in the Python core world, including the arrival of Python 3.15 beta 1, free-threading improvements, PEP 788 landing in CPython, and a wave of new core developer activity.

Anything that could go wrong, will. The excuse is optional.

A thoughtful take on Murphy's Law in software engineering: resilient teams don't avoid risk or ignore it, they design systems assuming failure will happen and plan accordingly.

My PyCon US 2026

A chronological recap of PyCon US 2026 in Long Beach, with live notes ranging from the first AI track talk on AI-assisted contributions and maintainer load to security updates, community building, and Djangonaut Space. Expect practical takeaways about how AI affects review and conflict in open source, plus plenty of Django community moments including "Django on the Med."


Events

Organizing DjangoCon Europe 2026: The Afterthoughts | Blog with LOGIC

Find practical after-the-fact takeaways from organizing DjangoCon Europe 2026, focused on the details people usually only notice after the event. A useful read for anyone planning Django community events or sharpening their conference workflow.


Videos

Tech Hiring has got a FRAUD problem!

Tech hiring can attract fraud, from fake postings to misleading recruiting signals. Keep an eye on red flags in job listings and interview processes so you can spot scams early and protect candidates.


Podcasts

Django Chat #204:How France Ditched Microsoft with Samuel Paccoud

France's shift away from Microsoft is tied to decisions and experiences Samuel Paccoud discusses. The focus is on what prompted the move and what it meant operationally for organizations involved.


Django Job Board

Founding Engineer at MyDataValue

Junior Software Developer (Apprentice) at UCS Assist

Technical Lead at UCS Assist

Web Developer at Crossway

PyPI Sustainability Engineer at Python Software Foundation


Projects

mliezun/caddy-snake

Caddy plugin to serve Python apps

AvaCodeSolutions/django-email-learning

An open source Django app for creating email-based learning platforms with IMAP integration and React frontend components.

ehmatthes/gh-profiler

Examine a GitHub user's profile, to help quickly decide how much to invest in their contributions. Was discussed by many maintainers at PyCon US sprints.

22 May 2026 2:00pm GMT

feedPlanet Twisted

Glyph Lefkowitz: Opaque Types in Python

Let's say you're writing a Python library.

In this library, you have some collection of state that represents "options" or "configuration" for a bunch of operations. Such a set of options is a bundle of potentially ever-increasing complexity. Thus, you will want it to have an extremely minimal compatibility surface, with a very carefully chosen public interface, that is either small, or perhaps nothing at all. Such an object conveys state and might have some private behavior, but all you want consumers to be able to do is build it in very constrained, specific ways, and then pass it along as a parameter to your own APIs.

By way of example, imagine that you're wrapping a library that handles shipping physical packages.

There are a zillion ways to do it ship a package. There are different carriers who can ship it for you. There's air freight, and ground freight, and sea freight. There's overnight shipping. There's the option to require a signature. There's package tracking and certified mail. Suffice it to say, lots of stuff.

If you are starting out to implement such a library, you might need an object called something like ShippingOptions that encapsulates some of this. At the core of your library you might have a function like this:

1
2
3
4
5
async def shipPackage(
        how: ShippingOptions,
        where: Address,
    ) -> ShippingStatus:
    ...

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:

  1. We need a type that our client code can use in its type annotations; it needs to be public.
  2. 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.
  3. 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:

  1. a public NewType, which gives us our public name...
  2. which wraps a private class with entirely private attributes, to give us an actual data structure, while not exposing the constructor,
  3. 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
from dataclasses import dataclass
from typing import Literal, NewType

@dataclass
class _RealShipOpts:
    _speed: Literal["fast", "normal", "slow"]

ShippingOptions = NewType("ShippingOptions", _RealShipOpts)

def shipFast() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts("fast"))

def shipNormal() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts("normal"))

def shipSlow() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts("slow"))

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
from dataclasses import dataclass
from enum import Enum, auto
from typing import NewType

class Carrier(Enum):
    FedEx = auto()
    USPS = auto()
    DHL = auto()
    UPS = auto()

class Conveyance(Enum):
    air = auto()
    truck = auto()
    train = auto()

@dataclass
class _RealShipOpts:
    _carrier: Carrier
    _freight: Conveyance

ShippingOptions = NewType("ShippingOptions", _RealShipOpts)

def shipFast() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts(Carrier.FedEx, Conveyance.air))

def shipNormal() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts(Carrier.UPS, Conveyance.truck))

def shipSlow() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts(Carrier.USPS, Conveyance.train))

def shippingDetailed(
    carrier: Carrier, conveyance: Conveyance
) -> ShippingOptions:
    return ShippingOptions(_RealShipOpts(carrier, conveyance))

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.


  1. The overhead is minimal, but it is not completely zero. The suggested idiom for converting to a NewType is to call it like a function, as I've done in these examples, but if you are wanting to use this pattern inside of a hot loop, you can use # type: ignore[return-value] comments to avoid that small cost. ↩

22 May 2026 12:33am GMT

21 May 2026

feedDjango community aggregator: Community blog posts

Utrecht (NL) Python meetup summaries

I made summaries at the 4th PyUtrecht meetup (in Nieuwegein, at Qstars this time).

Qstars IT and open source - Derk Weijers

Qstars IT hosted the meeting. It is an infra/programming/consultancy/training company that uses lots of Python.

They also love open source and try to sponsor where possible.

One of the things they are going to open source (next week) is a "cable thermal model", a calculation method to determine the temperature of underground electricity cables. The Netherlands has a lot of net congestion... So if you can have a better grid usage by calculating the real temperature of cables instead of using an estimated temperature, you might be able to increase the load on the cable without hitting the max temperature. Coupled with "measurement tiles" that actually monitor the temperature.

They build it for one of the three big electricity companies in the Netherlands and got permission to open source it so that the other companies can also use it. They hope it will have real impact.

He explained an open source project he started personally: "the space devs". Integrating rocket launch data and providing an API. Now it has five core developers (and got an invitation to the biggest space conference, two years ago!)

Some benefits from writing open source:

  • You build your own portfolio.
  • You can try new technologies. Always nice to have the skill to learn new things.
  • You improve your communication skills (both sending and receiving).
  • You can make your own decisions.
  • You write in the open.
  • Perhaps you help others with your work.
  • You could be part of a cummunity.
  • It is your code.

How to start?

  • Reach out to other communities.
  • Read and improve documentation.
  • Find good first issues.
  • Be proactive.
  • Don't be afraid to ask questions (and don't let negative comments discourage you).

When working on open source, make sure you take security serious. People nowadays like to use supply chain attacks via open source software. So use 2FA and look at your deployment procedure.

Learning Python with Karel - EiEi Tun H

What is Karel <https://github.com/alts/karel>)? A teaching tool/robot for learning programming. You need to steer a robot in an area and have it pick up or dump objects. And... in the meantime you learn how to use functions and loops.

Karel only has a turn_left() function. So if you want to have it turn right, it is handy to add a function for it:

def turn_right():
    turn_left()
    turn_left()
    turn_left()

Simple, but you have to learn it sometime!

In her experience, AI can help a lot when learning to code: it explains stuff to you like you're a five-year-old, and that's perfect.

If you want to play with Karel: https://compedu.stanford.edu/karel-reader/docs/python/en/ide.html

JSON freedom or chaos; how to trust your data - Bart Dorlandt

For this talk, I'm pointing at the PyGrunn summary I made three weeks ago. I liked the talk!

Practical software architecture for Python developers - Henk-Jan van Hasselaar

There are several levels of architecture. Organization level. System level. Application, Code.

Cohesion: "the degree to which the elements inside a module belong together". What does it mean? Working towards the same goal or function. Together means something like distance. When two functions are in separate libraries, they're not together. It is also important for cognitive load.

Coupling: loose coupling versus high coupling. You want loose coupling, so that changes in one module don't affect another module.

You don't really have to worry about coupling and cohesion in existing systems that don't need to be changed. But when you start changing or build something new: take coupling/cohesion into account.

Software architecture is a tradeoff. Seperation of concerns is fine, but it creates layers and thus distance, for instance.

Python is one of the most difficult languages when it comes to clean coding and clean architecture. You're allowed to do so many dirty things! Typing isn't even mandatory...

He showed a simple REST API as an example. Database model + view. But when you change the database model, like a field name, that field name automatically changes in the API response. So your internal database structure is coupled to the function at the customer that consumes the API.

What you actually need to do is to have a better "contract". A domain model. In his example code, it was a Pydantic model with a fixed set of fields. A converter modifies the internal database model to the domain model.

You can also have services, generic pieces of code that work on domain models. And adapters to and from domain models, like converting domain models to csv.

Finding the balance is the software architect's job.

What is the least you should do as a software developer? At least to create a domain layer. Including a validator.

There was a question about how to do this with Django: it is hard. Django's models are everywhere. And you really need a clean domain layer...

21 May 2026 4:00am GMT

My PyCon USΒ 2026

A timeline of my PyCon US 2026 journey, in Long Beach (US), told through the Mastodon posts I shared along the way.

21 May 2026 3:00am GMT

04 Apr 2026

feedPlanet Twisted

Donovan Preston: Using osascript with terminal agents on macOS

Here is a useful trick that is unreasonably effective for simple computer use goals using modern terminal agents. On macOS, there has been a terminal osascript command since the original release of Mac OS X. All you have to do is suggest your agent use it and it can perform any application control action available in any AppleScript dictionary for any Mac app. No MCP set up or tools required at all. Agents are much more adapt at using rod terminal commands, especially ones that haven't changed in 30 years. Having a computer control interface that hasn't changed in 30 years and has extensive examples in the Internet corpus makes modern models understand how to use these tools basically Effortlessly. macOS locks down these permissions pretty heavily nowadays though, so you will have to grant the application control permission to terminal. But once you have done that, the range of possibilities for commanding applications using natural language is quite extensive. Also, for both Safari and chrome on Mac, you are going to want to turn on JavaScript over AppleScript permission. This basically allows claude or another agent to debug your web applications live for you as you are using them.In chrome, go to the view menu, developer submenu, and choose "Allow JavaScript from Apple events". In Safari, it's under the safari menu, settings, developer, "Allow JavaScript from Apple events". Then you can do something like "Hey Claude, would you Please use osascript to navigate the front chrome tab to hacker news". Once you suggest using OSA script in a session it will figure out pretty quickly what it can do with it. Of course you can ask it to do casual things like open your mail app or whatever. Then you can figure out what other things will work like please click around my web app or check the JavaScript Console for errors. Another very important tips for using modern agents is to try to practice using speech to text. I think speaking might be something like five times faster than typing. It takes a lot of time to get used to, especially after a lifetime of programming by typing, but it's a very interesting and a different experience and once you have a lot of practice It starts to to feel effortless.

04 Apr 2026 1:31pm GMT

16 Mar 2026

feedPlanet Twisted

Donovan Preston: "Start Drag" and "Drop" to select text with macOS Voice Control

I have been using macOS voice control for about three years. First it was a way to reduce pain from excessive computer use. It has been a real struggle. Decades of computer use habits with typing and the mouse are hard to overcome! Text selection manipulation commands work quite well on macOS native apps like apps written in swift or safari with an accessibly tagged webpage. However, many webpages and electron apps (Visual Studio Code) have serious problems manipulating the selection, not working at all when using "select foo" where foo is a word in the text box to select, or off by one errors when manipulating the cursor position or extending the selection. I only recently expanded my repertoire with the "start drag" and "drop" commands, previously having used "Click and hold mouse", "move cursor to x", and "release mouse". Well, now I have discovered that using "start drag x" and "drop x" makes a fantastic text selection method! This is really going to improve my speed. In the long run, I believe computer voice control in general is going to end up being faster than WIMP, but for now the awkwardly rigid command phrasing and the amount of times it misses commands or misunderstands commands still really holds it back. I've been learning the macOS Voice Control specific command set for years now and I still reach for the keyboard and mouse way too often.

16 Mar 2026 11:04am GMT