22 May 2026

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

feedPlanet Python

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

feedPlanet Python

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:

  1. I ask Priya to draft the first module. She writes this in a markdown file.

  2. I read through her draft and leave comments and questions directly within the text.

  3. 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.)

  4. Repeat steps 2 and 3 until I feel I understand the topic.

  5. 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:

  1. 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

  2. The Club - the extra Python posts for premium subscribers

  3. 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.

  4. 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.

Subscribe now

stephengruppetta.com


Photo by detait

21 May 2026 9:23pm GMT

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:

  1. Manually edit pyproject.toml to add upper bounds for every single dependency.
  2. Live in fear that uv lock --upgrade will 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.

  1. Use uv pip list --outdated instead of uv tree --outdated --depth 1. The uv pip command 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.

  2. You can set the --bounds default in pyproject.toml. You don't have to remember to type --bounds major on every uv 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.

  3. 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.

21 May 2026 6:08pm GMT

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

20 May 2026

feedDjango community aggregator: Community blog posts

Weeknotes (2026 week 17)

Weeknotes (2026 week 17)

I published the last entry near the beginning of March. I'm really starting to see a theme in my Weeknotes publishing schedule.

Releases since the first weeks of March

I'm trying out a longer-form version of those notes here than in the past. I think it's worth going into some detail and not just listing releases with half a sentence each.

feincms3-sites and feincms3-language-sites

I released updates to feincms3-sites and feincms3-language-sites fixing the same issue in both projects: When an HTTP client didn't strip the default ports :80 (for HTTP) or :443 (for HTTPS) from a request, finding the correct site would fail. Browsers generally strip the port already, but some other HTTP clients do not.

django-tree-queries

As I wrote elsewhere I closed many issues in the repositories, mostly documentation issues but also some bugs. {% recursetree %} should now work properly and not cache old data anymore, using the primary key in .tree_fields() now raises an intelligible error, and I also fixed a bug with table quoting when using django-tree-queries with the not yet released Django 6.1+.

feincms3-cookiecontrol

feincms3-cookiecontrol not only offers a cookie consent banner (which actually supports only embedding tracking scripts when users give consent) but also a third-party content embedding functionality which allows allowlisting individual services.

The privacy policies of these services are now linked inline instead of with an ugly extra link. This reduces content inside the embed which helps on small screens.

Version 1.7 used a buggy trusted publishing workflow so I immediately published 1.7.1.

django-cabinet and django-prose-editor

django-cabinet can now be used as a media library directly inside django-prose-editor. I'm (ab)using the CKEditor 4 protocol for embedding, which uses window.opener.CKEDITOR.callFunction to send data back from the file manager popup into the editor. It feels icky but works nicely. This is only available if you're installing the alpha prereleases, but I'm already testing the functionality in production somewhere, so I feel quite good about it.

django-prose-editor now also ships brand new ClassLoom and StyleLoom extensions. Both extensions allow adding either classes or inline styles to text spans or nodes. In an ideal world we might not use something like this, but to make the editor more useful in the real world, editors need more flexibility. These two extensions provide that. I already mentioned ClassLoom in December, now it's actually available. I'm not completely sold on how they work yet, but both of them are already solving real issues.

Honorable mentions

django-debug-toolbar 6.3 has been released, I only contributed reviews during this cycle.

20 May 2026 5:00pm 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