Planet Python
Last update: August 22, 2026 09:48 PM UTC
August 22, 2026
Ned Batchelder
Micro language implementation: Calcium
I wrote a tiny language implementation: Calcium. It’s meant as a demonstration of how languages like Python are implemented. It has a tokenizer, a parser, an AST, a compiler, bytecodes, and an execution engine, all in about 300 lines of code.
I did it because I often see the question: isn’t Python interpreted? Why do people say it’s compiled? (BTW, I also answered this in an earlier blog post: Is Python interpreted or compiled? Yes.) It can be hard to explain that your Python program never becomes an explicit sequence of native CPU instructions, which is what people often think “compiled” means.
So I coded up Calcium to have on hand the next time it comes up. I think it will help to be able to show the execution engine code reading bytecodes and doing what they say.
It could also be an interesting starting point for people wanting to play with a language implementation. It has almost nothing, so there’s lots of simple things (comments?) to add.
Robin Wilson
Some recent, and not so recent, talks and activities
There has been a long gap between posts (again!), and a lot of the stuff that I’m going to talk about in this post is fairly old too. Oh well, life keeps getting in the way of blogging.
Anyway, I’ve recently submitted a talk for the FOSS4G UK 2026 conference in Leeds in October, and have just found out that it’s been accepted. That reminded me that I never got round to posting about my workshop at the same conference last year…oops! So, here goes:
FOSS4G UK 2025 workshop: Live Vector Tiles from PostGIS with pg_tileserv

Last year at FOSS4G UK 2025, I ran a workshop on generating vector tiles live from a PostGIS database. The full description of the workshop from the schedule is available below, but I started by giving a bit of a talk about vector tiles, what they are, and the differences between pre-generated and on-the-fly vector tiles. I then gave a number of exercises to work on using a PostGIS cloud server that I (well, my current client) provided for people to play with. People worked through using pg_tileserv to server vector tiles on-the-fly, starting with consuming tiles for a table, and moving on to generating tiles from PostGIS functions.
Vector tiles are a key part of many modern web mapping systems, but they don’t have to be generated ahead of time and stored. Instead, they can be generated ‘on the fly’ directly from the underlying data. Importantly this allows geospatial processing to be performed ‘live’ when the tiles are generated enabling many complex geospatial workflows, as well as reducing the need to pre-generate and store many different tiles.
This workshop will take you through the whole process of generating Mapbox Vector Tiles on the fly from a PostGIS database using a tool called pg_tileserv. We will start with an introduction to vector tiles and PostGIS, then move on to using pg_tileserv to generate tiles for database tables. After demonstrating basic tile serving to a local web map (using MapLibre) and QGIS, we will move on to defining PostGIS functions to integrate with pg_tileserv and do live geospatial processing as tiles are requested. Our demonstrations will focus on assessing flooding in the UK, but there will be plenty of opportunities for participants to try other sorts of processing.
To reduce the burden on participants, we will provide access to a pre-populated cloud PostGIS instance alongside a pg_tileserv deployment for the duration of the workshop, allowing users to participate in the workshop without installing or setting up any software beyond pgAdmin (or an alternative way to run SQL queries), a text editor and (optionally) QGIS. For those who want to run things locally or continue after the workshop, we will also provide a Docker Compose configuration.
It will be helpful for participants to have some experience with PostGIS and basic geospatial SQL, as well as a basic understanding of web mapping, Javascript and HTML.
The workshop materials are all available on the foss4guk_vector_tile_workshop Github repository. The cloud server we provided for the workshop is no longer running, but you can set up your own PostGIS server, add a few geospatial tables and run pg_tileserv yourself (it’s a simple Docker file) if you want to have a play. All the exercises are listed in the README file, and my slides are also available for download.
Electromagnetic Field 2026 talk: Lights, Spectrometer, Action!

The Electromagnetic Field camping festival is a biannual highlight for me and my family. My wife, son and I all attended this year – and we brought my Mum along too – and we had a great time. My wife ran an over-subscribed workshop on lace making, and I gave a talk called Lights, Spectrometer, Action! which was all about spectrometry. That’s the science of measuring light at different wavelengths – and my talk included lots of live demos using a visible and near-infra-red spectrometer that I inherited from my late PhD supervisor, Prof Ted Milton.
The video of my talk is available online. The full talk description from the schedule is below, but in summary I look at light from various different light sources, then at reflected light from materials like leaves and soil, before finishing with a bit of absorption and emission spectrometry.
Full talk descriptionCome and watch me point a spectrometer at a load of things, and teach you about how light interacts with stuff. What’s a spectrometer, you ask? Well it’s a device that measures light and produces a graph of the brightness of the light at each wavelength. I inherited one last year, and this talk will mainly consist of live demos of the spectrometer, along with explanations of what we’re seeing and how it works. The talk will be accessible to almost anyone – you don’t need a lot of science knowledge to be able to enjoy it!
I’ll start with looking at some different light sources (incandescent bulbs, fluorescent bulbs, LEDs, sunlight, LCD displays) and then take a look at how a spectrometer works and what is inside the box. I’ll then introduce some other useful bits of kit, including a very expensive sheet of white plastic and a ‘sphere’ that has a surprisingly cuboid shape.
My background is in satellite imaging – which basically uses spectrometers mounted on satellites – so we’ll do a live look at the reflectance graphs we get for things we might be able to see from a satellite, like vegetation, soil and water and try to understand what causes these specific spectra. We’ll link these to real satellite images and cover some of the problems with doing spectroscopy from space – and even explain why a friend of mine did a whole PhD on the reflectance spectra of a concrete runway in the UK.
Finally we’ll try a few experiments that may or may not work, looking at the uses of spectroscopy in chemistry and measuring aerosols.
The talk took a lot of preparation, and I even ended up developing custom software to drive the spectrometer (see the repo). I’m hoping to add it to my list of talks that I give at local public science groups – though it requires quite a lot more setup and equipment than most of my other talks!
Advance notice: FOSS4G UK 2026 talk

I’ll be giving a talk at FOSS4G UK 2026 called Open cloud-native access to Environment Agency DEM data using STAC and COG. Basically I’ve taken the openly-licensed Environment Agency DEM data and re-encoded it into cloud-native formats, made it far easier to use and re-released it. I’m looking forward to presenting it in a few months time – I just need to finish the development work first!
The Environment Agency provides openly-licensed LiDAR-derived DEM data covering the whole of England, but it is difficult to access this data. The web interface only allows downloads of small areas, manual bulk downloads produce hundreds of small TIFF files and the web map services have limited visualisation options. At Rebalance Earth, we needed easy access to large volumes of DEM data to analyse flood risk and design nature-based restorations to reduce flooding.
To solve this problem, we have converted the whole dataset to Cloud Optimized GeoTIFF (COG) and produced a STAC catalog linking to all these COG files. As the Open Goverment License allows redistributing the data, we’ve been able to make the new files and STAC catalog available online, free for anyone to use.
In this talk I’ll explain the benefits of cloud native geospatial data, and how we did the conversion (including some useful tips for creating COG files). I’ll then show some demos of how this data can be used – for example to easily produce mosaics of DEM data across arbitrary areas, to visualise in custom ways on a web map (tools like titiler and titiler-pgstac) and more. I’ll finish by explaining how you can connect to this dataset and access it yourself.
Python Insider
The Python documentation is now available in Russian!
Bob Belderbos
Unsubscribe links without a login: Django signing
Django has a signing module that makes it easy to build an unsubscribe link that works with no login and no session: token = signing.dumps(recipient.pk, salt=UNSUBSCRIBE_SALT). The token itself is the credential, and it ships with Django out of the box.

The URL is the whole request
Unsubscribing poses a bit of a pickle. A user can click the link in an email without being logged in, and you don't want to force them to log in just to stop receiving emails. So the URL must carry both who they are and what they're allowed to do, all on its own.
The tempting but insecure way to do this:
# don't do this
def unsubscribe(request):
user_id = request.GET["user_id"]
Profile.objects.filter(user_id=user_id).update(opt_out=True)
/unsubscribe/?user_id=42 works, and it also lets anyone loop from 1 upward and unsubscribe your entire user table. It's a plain insecure direct object reference: the URL names a record and nothing proves the clicker is allowed to touch it.
The usual fix is a random token stored in the database, looked up on click. That's fine, but it means a column or a table, a migration, and code to create and expire the tokens. For a boolean flag on a profile, that's a lot of moving parts.
Django already fixes this
django.core.signing gives you a tamper-evident token with no storage at all. It packs your value and a signature made from SECRET_KEY:
from django.core import signing
UNSUBSCRIBE_SALT = "discuss-unsubscribe"
token = signing.dumps(recipient.pk, salt=UNSUBSCRIBE_SALT)
url = f"{settings.DOMAIN}{reverse('discuss_unsubscribe', args=[token])}"
On the way back in, loads verifies the signature and hands you the original value. In this case unsubscribing a user from forum post notifications:
def unsubscribe(request, token):
try:
user_pk = signing.loads(token, salt=UNSUBSCRIBE_SALT)
profile = Profile.objects.get(user_id=user_pk)
# writes on GET for now; see "Don't mutate on a GET" before shipping
profile.opt_out_discussion_emails = True
profile.save(update_fields=["opt_out_discussion_emails"])
success = True
except (signing.BadSignature, Profile.DoesNotExist):
success = False
return render(request, "unsubscribe.html", {"success": success})
Change one character of the token and the signature no longer matches, so loads raises BadSignature and the pk never gets used. Guessing another user's pk is pointless: you can't produce a valid signature for it without SECRET_KEY. No table, no lookup, no cleanup.
If you've ever clicked a thumbs-up in a GitHub Copilot feedback email and seen a URL that's a wall of base64, that's the same idea: a signed token identifying you and your action, no login in the loop.
How to prevent multiple flows from colliding
A salt argument scopes a token to one purpose. This codebase now has two separate unsubscribe flows, and they use different salts:
# bites/management/commands/send_announcement.py
UNSUBSCRIBE_SALT = "announce-unsubscribe"
# discuss/notify.py
UNSUBSCRIBE_SALT = "discuss-unsubscribe"
Both sign a user pk with the same SECRET_KEY. But a token minted for the announcements list will not validate in the forum-reply handler, because the salts differ.
Without that, a single leaked token would work against every flow that signs a pk. The salt keeps a token minted for one purpose from being accepted by another flow. Name it per action and you get that separation for free.
Signed is not secret
One honest point the convenience can hide: signing proves a value wasn't altered, not that it's hidden. The pk is sitting in that token, base64-encoded and readable by anyone who pastes it into a decoder.
That's fine for a user id, which isn't a secret. It is not fine for anything you'd mind a recipient reading. Never sign a value you wouldn't also be willing to print in the email body.
Don't mutate on a GET
There's a subtlety the happy path hides: my handler flips a flag on a plain GET request. Mail security scanners and link-prefetchers routinely follow links inside email before a human ever clicks, which would unsubscribe people who never asked to be. The signing is fine, the HTTP method is the risk.
For unsubscribe the fix is cheap: render a confirmation page on GET and do the write on the POST. The signed token still carries the identity, the button carries the intent.
Mailbox providers formalize the same POST-not-GET rule: RFC 8058 has providers like Gmail and Apple Mail POST straight to the URL in your List-Unsubscribe header (paired with List-Unsubscribe-Post: List-Unsubscribe=One-Click), unsubscribing in one click. That endpoint has to be CSRF-exempt, since the provider sends no token.
When you need expiry
My unsubscribe tokens never expire, and that's a deliberate design choice.
Unsubscribe is low-stakes: a two-year-old link still does the one safe thing, even if the mail gets forwarded and a stranger clicks it. The stakes are what decide this.
Reuse the same pattern for a magic login link or an email verification. Here you do want a lifetime, so set a max_age on the loads call:
# magic login: valid for 15 minutes (single-use needs the state below)
user_pk = signing.loads(token, salt="magic-login", max_age=900)
max_age gives you expiry with still no storage: the deadline is baked into the signed token, so the signature verifies it without a database.
Single-use is different, it needs memory. The signature can't tell you a token was already spent, so you'd have to record each accepted token and check that list. That's the state you're forced to add.
An unsubscribe link is idempotent, clicking it twice does the same safe thing, so it can stay stateless. A login link can't: reusing it opens the account again.
Keep reading
- How to Migrate Users Seamlessly Between Django Apps on Login
- How to Update Multiple Page Elements from One htmx Request
Where else in your app does a URL quietly get trusted? What other pickles have you had to deal with in your Django codebases?
Armin Ronacher
Fast and Hard Code
One of the memes on Twitter is that “programming is solved now.” I’m not sure to what degree it is, but one thing is pretty clear: the act of familiarizing yourself with a language no longer matters and some of the friction that mattered for humans does not matter for agents.
As a result, LLMs make language choice much less consequential than it used to be. If you don’t like the choice, you can seemingly rewrite it in another language and you can make it pick a language that you, as a programmer, are entirely unfamiliar with.
Which in turn means that people can, and do, choose based on the marketing of languages much more. As a long-term Rust programmer I found it quite fascinating to see people now ship Rust code who previously might not have chosen it. I attribute at least one part of this to two recent vibe shifts: there is a lot more talk about wanting fast software, and about LLMs being exceptional at optimizing code without regressing behavior.
Folks like Mitchell Hashimoto, Charlie Marsh, Jarred Sumner, Daniel Lemire and quite a few others always carried a certain level of obsession with fast and performant software and they also all happen to be receptive to agents writing code. Maybe as a result, or unrelated others are now joining in. That’s because with things like autoresearch you don’t even necessarily need to know all the tricks: you just need to put an agent on it — though knowledge greatly helps!
If you look around, there are plenty of projects that want to be fast and small, and they increasingly pick “hard languages”. And it’s not just Rust that is benefiting. Even Zig — despite the fact that the creators and parts of the core community are pretty negative on the whole AI thing — is too. For instance Cloudflare’s new Artifacts service uses a pure-Zig Git-protocol engine, compiled to a roughly 100 KB WebAssembly module and Vercel released fx, a Zig coding agent advertised to be small and fast. From what I can tell, all these projects are largely LLM-assisted.
But it’s not just people picking less common languages but also that they are increasingly working with “much harder” technologies. All of a sudden I have seen people do some really impressive stuff with DWARF files, eBPF, custom network drivers, custom crypto and really old computing hardware. Many of these things were previously off-limits for lots of developers. In some cases (eg: crypto) you were even pushed away because those things were intentionally gatekept by the people in the know.
So maybe the world will have more slop, but it might also have more developers in it, that want things to be fast and small.
August 21, 2026
Caktus Consulting Group
Fuzzy String Matching in Django and PostgreSQL
Gerald Carlton and I will be presenting on fuzzy name search at DjangoCon US 2026 on Monday, August 24, and this is the companion blog post. Searching for a person by name is harder than it looks: names might be typed differently by different operators or change over a person’s lifetime; for example, Smith could be entered as Smyth, Smythe, or Smidt, and Weiss as Weiß. Although names are particularly susceptible to misspelling, these strategies apply to all fuzzy string matching.
Python Software Foundation
Announcing the PSF Board Candidates for 2026!
What an exciting list! Please take a look at the 17 candidates running for the PSF Board this year on the Nominees page. This year there are 4 seats open on the PSF Board. You can see who is currently on the board on the PSF Officers & Directors page. (Cheuk Ting Ho, Christopher Neugebauer, Denny Perez, and Georgi Ker are at the end of their current terms.)
Nomination and supporting statements are the clearest way to see how each candidate actually thinks: their priorities, experience, and vision for the PSF. Rather than just voting on name recognition, we encourage you to read each candidate's nomination and supporting statements. PSF Board members shape real decisions about budget, programs, and the direction of the PSF. Reading these statements helps ensure your vote reflects what the candidates would actually do in the role.
Board Election Timeline:
- Nominations open: Tuesday, July 28th, 2:00 pm UTC
- Nomination cut-off: Tuesday, August 11th, 2:00 pm UTC
- Announce candidates: Thursday, August 13th
- Voter affirmation cut-off: Tuesday, August 25th, 2:00 pm UTC
- Voting start date: Tuesday, September 1st, 2:00 pm UTC
- Voting end date: Tuesday, September 15th, 2:00 pm UTC
Not sure what UTC is for you locally? Check this UTC time converter.
Reminder to affirm your intention to vote!
If you wish to vote in this year’s election, you must affirm your intention to vote no later than Tuesday, August 25th, 2:00 pm UTC, to participate in this year’s election. This year’s Board Election vote begins Tuesday, September 1st, 2:00 pm UTC, and closes on Tuesday, September 15th, 2:00 pm UTC.
Every PSF Voting Member (Supporting, Contributing, and Fellow) needs to affirm their membership to vote in this year’s election. You should have received an email from "psf@psfmember.org <Python Software Foundation>" with the subject "[Action Required] Affirm your PSF Membership voting intention for 2026 PSF Board Election" that contains information on how to affirm your voting status.
Per a recent Bylaw change that allows for simplifying the voter affirmation process by treating past voting activity as intent to continue voting, if you voted last year, you will automatically be added to the 2026 voter roll. Please note: If you removed or changed your email on psfmember.org, you may not automatically be added to this year's voter roll.
Find more information, including step-by-step instructions on voting affirmation, in our ‘Affirm Your PSF Membership Voting Status” blog post. If you run into any issues, or have questions about your membership, please contact psf-elections@pyfound.org.
Voting: what to expect
If you are a voting member of the PSF that affirmed your intention to participate in this year’s election, you will receive an email from “OpaVote Voting Link <noreply@opavote.com>” with your ballot, the subject line will read “Python Software Foundation Board of Directors Election 2026” on September 1st. If you don’t receive a ballot as expected, please first check your spam folder for a message from “noreply@opavote.com”. If you don’t see anything get in touch by emailing psf-elections@pyfound.org so we can look into your account and make sure we have the most up-to-date email for you.
If you have questions about your membership status or the election, please email psf-elections@pyfound.org. You are welcome to join the discussion about the PSF Board election on the Python Discuss forum.
Note: One individual originally announced as a candidate has since withdrawn from the election. The link above reflects the current, updated slate of candidates.
Django Weblog
DSF Membership Open Space at DjangoCon US
If you are at DjangoCon US this year, join us for 45 minutes to talk about the Django Software Foundation.
- What: Django Software Foundation Membership Open Space
- When: Wednesday, August 26th, 1:00 pm to 1:45 pm
- Where: Wolf Point Ballroom, voco Chicago Downtown
- Schedule: Talks: Wednesday, Aug 26
DSF Board Directors Jeff Triplett, Abigail Afi Gbadago, and Paolo Melchiorre will be there, along with Django Fellows, Steering Council members, and other DSF members. This is an open space, not a talk. Pull up a chair and bring your questions.
What We Want to Talk About
- Fundraising and finances. We raised our 2026 goal to $500,000. We'll cover what it funds, most of it the Django Fellows, and how we are tracking against it. If your company depends on Django, or you know one that does, ask us about corporate membership and sponsorship.
- The Executive Director search. The DSF is hiring its first Executive Director, and applications are open through midnight Central Time on September 14, 2026. If you are thinking about applying, or know someone who should, ask us about the role.
- Membership. How individual membership works, how people become members, and what members do. Members vote in DSF elections, and we would like more of you to.
- Working groups and teams. You don't need to be a DSF member to join a working group, and we could use your help. Teams work a little differently, and we can explain the difference.
- The Steering Council. A short update on DEP 19, which rewrites Django's technical governance, and DEP 20, which moves Django to one feature release a year.
This is your time as much as ours. If you have a question about how the Foundation spends its money, how decisions get made, or why something is the way it is, ask it. We would rather answer a hard question in a room in Chicago than in a thread three weeks from now.
One note on scope: this session is about the Foundation, not about code. We will touch on the DEPs above, but patches, ORM internals, and your favorite ticket belong to the rest of the week.
Who Should Come
Everyone. You do not have to be a DSF member to attend: no sign-in sheet and no membership check at the door.
If you are already an Individual Member, meet the others. If you have been curious about joining, see what you would be joining. And if you use Django at work but have never thought much about the Foundation behind it, that is a fine reason to stop by.
Can't Make It?
Send anything you want raised through the Contact the DSF page, or find us on the Django Forum. We will follow up on anything worth sharing more widely.
See you Wednesday.
August 20, 2026
The No Title® Tech Blog
Optimize Images X adds internationalization
Optimize Images X, the multi-platform desktop application that helps you reduce the file size of your images on macOS, Windows and Linux, has just been updated to version 2.2.0. This new version adds full internationalization support with some initial translations, plus a handful of minor UI adjustments.
Python Software Foundation
How AWS Powers PyPI and the PSF
Written by Jacob Coffee, Director of Engineering at the Python Software Foundation
Working on infrastructure at the Python Software Foundation (PSF) as the Director of Engineering is a broad job with many hats. Python turns up everywhere. It's in healthcare systems and government agencies, in research labs and classrooms, in one-person side projects and in infrastructure at companies with six-figure headcounts. Somebody is using it to analyze a genome right now and somebody else is using it to automate a spreadsheet they hate arranging by hand. That range has always been the interesting part of the PSF's work, and it's only broadened in the last couple of years as AI continues to pull people from many disciplines into writing Python.
At the PSF we support and maintain the infrastructure behind PyPI, Python.org, PyCon US, CPython, and a growing number of community services. The majority of the PSF's infrastructure runs on AWS, and nearly all of that bill is covered by credits from the AWS Open Source Credits Program.
The shape of it
PyPI takes over six billion requests a day and about thirteen billion counting file downloads. Package egress runs around 10 petabytes a day.
Almost none of that reaches AWS. Fastly serves it at the edge through their Fast Forward program at a hit ratio just under 99%, so only about one request in seventy makes it to origin. That caching is the core reason our AWS bill is not measured in the millions of dollars a year.
What does reach us is the part that is not cached, including things like user uploads, logins, and account management. That work runs on:
- Amazon EC2 for the application fleet, almost entirely Graviton at this point. We're huge fans of Arm-based hardware at the PSF!
- Amazon RDS for the Postgres database behind Warehouse, which is the application you're looking at when you're on pypi.org.
- Amazon OpenSearch Service for project search.
- Amazon S3 holds ~100 TB of package archives, plus the release history (that nobody ever wants to need).
- Amazon EKS, newly stood up, as the target for migrating the whole fleet.
What changed this year
Our AWS spend is up about 69% comparing July 2026 to July 2025, and 40% across the trailing twelve months. That's new.
For context on how new: last October, Ee Durbin, our former Director of Infrastructure, noted that PSF's AWS credit usage had grown 25% over eight years while daily requests went from millions to billions. Eight years of holding that line. 2026 is the first year it broke.
Some of it is simply more of everything. Compute, search, and load balancing all climbed from 30% to 80%, which is the profile of more people and machines using the service. There are certainly more of both. A lot more Python being written, a lot more agents installing packages on someone's behalf, and a lot more CI runs from all the projects those people and agents are creating. Package repositories were designed around the assumption that a human decides to run pip install some number of times a day. That assumption is well past its expiration.
What we do about it
At the PSF, we take the credits seriously, which means spending real engineering time on utilizing what we have in an optimal way so our usage doesn't increase. Being frugal with the credits entrusted to us is vital.
When a widely used GitHub Action makes requests it doesn't need to make, fixing the Action beats anything we can do on our side. A popular Action can quietly turn a cache hit into a fresh download from PyPI on every run, and that adds up fast across thousands of CI pipelines. astral-sh/setup-uv shipped exactly that kind of fix.
On the PSF side, the EKS migration is the big one. Once we're there, autoscaling lets capacity track demand instead of sitting provisioned for peak at 3 AM (San Francisco time) on a Sunday.
There's just one little (read: major) constraint: Ee left the PSF earlier this year, and I'm currently the only person here working full time on infrastructure. Every item on that list is real and every one is slower than it should be. The PSF is hiring, and that hiring is possible in part because we aren't spending the same money on servers.
That said, hiring has a ceiling set by funding, and the growing gap between the demand placed on PyPI relative to staff operating and securing the service represents a risk to Python packaging. There is a very large amount of the software industry that sits on top of our services. Feature development, a rising volume of security reports, and day to day maintenance are currently being carried by one and a half full-time employees and one full-time employee on PyPI support requests. If your company installs from PyPI, ask internally about what your organization would be willing to fund. That question can be worth much more coming from inside a company than from the PSF. If you are interested in securing a service agreement with PyPI, please fill out our survey.
What the funding actually bought
AWS has backed PyPI two different ways, and both are worth outlining.
A very exciting investment was made in 2023, when AWS became PyPI's inaugural Security Sponsor, putting $144,000 into creating the PyPI Safety & Security Engineer role. That role is now funded by Alpha-Omega, and it's the reason malware comes down off PyPI in hours instead of whenever a volunteer has a spare moment. This investment also followed a pattern AWS had already established, having helped fund the rewrite, internationalization, and 2FA support for PyPI.
The credits do something quieter. They hold the infrastructure bill near zero, which means the PSF's general fund goes to employees, PyCon US, and other operational costs instead of servers. That's what pays for support staff handling the account recovery and project ownership requests that arrive every single day, and what makes it possible to ship things like Trusted Publishing, digital attestations, and organization accounts, instead of just keeping the lights on.
PyPI is free to use and will stay free to use at reasonable levels. Core publishing and installing stay free, permanently. What we are doing is building out real benefits for Organization accounts, and looking at sensible rate limits for the heaviest consumers at the top of the curve.
Which is exactly why in-kind support isn't just a line item to us. A sponsor choosing not to renew would mean an emergency migration or tens of thousands of dollars a month, either of which comes straight out of the work above. AWS has renewed every year since 2018, and the predictability of that is worth as much as the amount. The same goes for Fastly, Google Cloud, Datadog, Sentry, Depot, and PagerDuty, who carry other parts of this mountain of infrastructure.
How you can help
Four things, in order of how much they matter:
- Cache your installs. If your CI pulls from PyPI on every run with a cold cache, you're a meaningful part of the graph. The PSF uses Docker cache mounts and pip's cache on our own builds, and npm and apt caching too, because it's faster for us and cheaper for everyone. Free package repositories aren't a limitless resource, and the fix is usually about six lines of config.
- Sign up for a PyPI Organization if your company publishes to PyPI. Recurring revenue from Organizations is the most sustainable funding base we have, and Community organizations remain free. Look forward to announcements about future features for PyPI organization accounts soon.
- Interested in a PyPI service agreement? Enhanced options like annual bulk seat purchases, higher project size limits, and prioritized support are available. The revenue helps fund the broader PyPI ecosystem while giving your organization benefits at scale. Fill out our service agreement interest survey and we'll follow up.
- Ask your infrastructure vendors for multi-year commitments with open source foundations. Annual renewal cycles carry real risk for projects like ours. The five-year agreement the PSF signed with Fastly in 2024 is a sustainable model, and we'd like more of them.
The open letter the PSF co-signed last year called this a critical inflection point rather than a crisis. A year on, with our first real break in eight years of flat infrastructure costs, that still reads about right.
A big thanks to Mila Zhou and the AWS Open Source team, who have made the credits process about as painless as an annual funding renewal can be, and to everyone at AWS who's kept this program going for so long. PyPI would look very different without it.
This blog was recently edited to more accurately reflect numbers presented and work done.
PyCharm
PyCharm for AI-assisted Django Workflows
The 2026 Django Developers Survey (results coming soon!) found that AI is part of the weekly or daily workflow for 90% of respondents. AI can write code quickly, but Django developers still need to understand the application, evaluate what the agent produces, and be accountable for what ships.
That makes your IDE more important, not less. PyCharm gives you the freedom to choose the agents and models you want, as well as extensive Python and Django support for understanding and reviewing the code they produce.
1. Bring your own agent
Leading agents such as Codex, Claude Agent, Junie, and Gemini run natively in PyCharm, while the ACP registry gives you access to dozens more, installable in a click from the same dropdown menu. You can use a JetBrains AI subscription or bring your own tools – the choice is yours.
Want to use your own model? Bring Your Own Key technology lets you connect existing provider credentials, while Ollama and LM Studio let you work with local models.
You choose the AI tools that fit your workflow. PyCharm doesn’t lock you into one provider.
2. Teach your agent your conventions
Skills give your agent reusable instructions and context. In PyCharm 2026.2, native skill support for Claude Agent and Codex lets you add a skill directly from the AI chat, either for a single project or across your entire codebase.
The curated Skill Repository also gives you a way to add official skills for technologies, including React, Postgres, and Playwright.
Skills let you encode useful context once and reuse it, so you spend less time repeating instructions to your agents.
3. Django 6+ support
PyCharm keeps up with Django’s release cycle, including Django 6.0 template partials. The IDE understands the new partial template tags and completes them as you type.
This matters especially when frameworks evolve. An agent’s training data may not reflect the exact Django version you’re running. PyCharm’s Django support is tied to the version in your project.
4. Review with confidence
AI generates code quickly, which makes reviewing and undoing changes more important than ever.
PyCharm gives you visual diffs, merge tools, integrated conflict resolution, and Git history in the IDE, so you can inspect changes before committing or merging.
Local History works independently of version control, recording changes as you work. You can compare a file with an earlier state and restore a version from before the agent touched it – even if you never committed the change.
5. See your Django architecture
Django applications can get big. Django Logical Structure presents your project from Django’s point of view rather than as a flat collection of Python and HTML files. You can follow a model to its serializer, its views, and the endpoints they serve.
Whether the code you’re reading was written by you six months ago, another developer, or an agent, PyCharm helps you understand how the pieces fit together.
6. Verify your API and your data
The Endpoints tool window gives you a structured view of your Django endpoints, including documentation, examples, generated HTTP requests, and OpenAPI information. You can send those requests directly with the built-in HTTP Client.
The same idea applies to your database. The data editor and viewer let you browse and query application data without leaving the IDE. After an agent generates a migration, you can inspect the resulting data and verify that the change did what you expected.
The IDE outlasts the agent
AI tooling is changing fast, but your developer workflow can stay consistent. PyCharm combines deep Python and Django support with access to the agents and models you choose.
August 19, 2026
Talk Python to Me
#559: 12 Things You Should (and Shouldn't) Do in AWS
Your site is down. It's 3am. Is it a bug, a bill, or a breach? You can't tell yet, and everyone is watching you find out. Matt Lea has spent fifteen years being the person companies call when an outage is costing them real money per hour, and his whole argument is that everything you'd want in that moment gets decided months earlier, on ordinary afternoons, when someone chose the convenient thing. We walk his top twelve dos and don'ts in AWS - infrastructure as code, IAM roles instead of access keys, private subnets, no wildcards, no public buckets - and I push on which of them actually matter if you're one person on a small VPS. Then we get to Cloud War Games, where Matt breaks things on purpose so your team's first real incident isn't their first incident. Let's get into it.<br/> <br/> <strong>Episode sponsors</strong><br/> <br/> <a href='https://talkpython.fm/sentry'>Sentry Error Monitoring, Code talkpython26</a><br> <a href='https://talkpython.fm/course-certifications'>Talk Python Courses</a><br> <a href='https://talkpython.fm/training'>Talk Python Courses</a><br/> <br/> <h2 class="links-heading mb-4">Links from the show</h2> <div><strong>Guest</strong><br/> <strong>Matt Lea</strong>: <a href="https://www.linkedin.com/in/schematical/?featured_on=talkpython" target="_blank" >linkedin.com</a><br/> <br/> <strong>Talk Python Certificates</strong>: <a href="https://training.talkpython.fm/certificates" target="_blank" >training.talkpython.fm/certificates</a><br/> <br/> <strong>Schematical</strong>: <a href="https://schematical.com?featured_on=talkpython" target="_blank" >schematical.com</a><br/> <strong>CloudWarGames.com</strong>: <a href="https://cloudwargames.com?featured_on=talkpython" target="_blank" >cloudwargames.com</a><br/> <strong>Zero to Hero on AWS Security</strong>: <a href="https://www.oreilly.com/videos/zero-to-hero/0642572107789/?featured_on=talkpython" target="_blank" >www.oreilly.com</a><br/> <strong>Repo</strong>: <a href="https://github.com/schematical/sc-terraform/tree/main/_course?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>Custom Wheel Offset</strong>: <a href="https://customwheeloffset.com?featured_on=talkpython" target="_blank" >customwheeloffset.com</a><br/> <strong>2012 TechCrunch Disrupt Hackathon</strong>: <a href="http://techcrunch.com/2012/09/09/meet-the-disrupt-sf-2012-hackathon-winners-livebolt-takes-grand-prize-auctopus-and-heatdata-are-runners-up/?featured_on=talkpython" target="_blank" >techcrunch.com</a><br/> <strong>tech comics</strong>: <a href="https://schematical.com/comics?featured_on=talkpython" target="_blank" >schematical.com</a><br/> <strong>shhgit</strong>: <a href="https://github.com/eth0izzle/shhgit?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>Zero Trust in 200ms: Implementing Identity-Per-Transaction</strong>: <a href="https://us.pycon.org/2026/schedule/presentation/140/?featured_on=talkpython" target="_blank" >us.pycon.org</a><br/> <strong>Coolify</strong>: <a href="https://coolify.io/?featured_on=talkpython" target="_blank" >coolify.io</a><br/> <strong>returned to full GA Nov 2025</strong>: <a href="https://aws.amazon.com/blogs/devops/aws-codecommit-returns-to-general-availability?featured_on=talkpython" target="_blank" >aws.amazon.com</a><br/> <strong>Signed URLs/cookies</strong>: <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-overview.html?featured_on=talkpython" target="_blank" >docs.aws.amazon.com</a><br/> <strong>Cloudflare</strong>: <a href="https://www.cloudflare.com/?featured_on=talkpython" target="_blank" >www.cloudflare.com</a><br/> <strong>Bunny Shield</strong>: <a href="https://bunny.net/shield/?featured_on=talkpython" target="_blank" >bunny.net</a><br/> <strong>Cloud War Games One</strong>: <a href="https://www.youtube.com/watch?v=91z1pn7fEjM" target="_blank" >www.youtube.com</a><br/> <strong>Cloud War Games Two</strong>: <a href="https://www.youtube.com/watch?v=xHXjiTi4nCA" target="_blank" >www.youtube.com</a><br/> <strong>LinkedIn</strong>: <a href="https://linkedin.com/in/schematical?featured_on=talkpython" target="_blank" >linkedin.com</a><br/> <strong>YouTube</strong>: <a href="https://youtube.com/schematical" target="_blank" >youtube.com</a><br/> <strong>KnocKnoc</strong>: <a href="https://knocknoc.io?featured_on=talkpython" target="_blank" >knocknoc.io</a><br/> <br/> <strong>Watch this episode on YouTube</strong>: <a href="https://www.youtube.com/watch?v=NTedSITJyRo" target="_blank" >youtube.com</a><br/> <strong>Episode #559 deep-dive</strong>: <a href="https://talkpython.fm/episodes/show/559/12-things-you-should-and-shouldnt-do-in-aws#takeaways-anchor" target="_blank" >talkpython.fm/559</a><br/> <strong>Episode transcripts</strong>: <a href="https://talkpython.fm/episodes/transcript/559/12-things-you-should-and-shouldnt-do-in-aws" target="_blank" >talkpython.fm</a><br/> <br/> <strong>Theme Song: Developer Rap</strong><br/> <strong>🥁 Served in a Flask 🎸</strong>: <a href="https://talkpython.fm/flasksong" target="_blank" >talkpython.fm/flasksong</a><br/> <br/> <strong>---== Don't be a stranger ==---</strong><br/> <strong>YouTube</strong>: <a href="https://talkpython.fm/youtube" target="_blank" ><i class="fa-brands fa-youtube"></i> youtube.com/@talkpython</a><br/> <br/> <strong>Bluesky</strong>: <a href="https://bsky.app/profile/talkpython.fm" target="_blank" >@talkpython.fm</a><br/> <strong>Mastodon</strong>: <a href="https://fosstodon.org/web/@talkpython" target="_blank" ><i class="fa-brands fa-mastodon"></i> @talkpython@fosstodon.org</a><br/> <strong>X.com</strong>: <a href="https://x.com/talkpython" target="_blank" ><i class="fa-brands fa-twitter"></i> @talkpython</a><br/> <br/> <strong>Michael on Bluesky</strong>: <a href="https://bsky.app/profile/mkennedy.codes?featured_on=talkpython" target="_blank" >@mkennedy.codes</a><br/> <strong>Michael on Mastodon</strong>: <a href="https://fosstodon.org/web/@mkennedy" target="_blank" ><i class="fa-brands fa-mastodon"></i> @mkennedy@fosstodon.org</a><br/> <strong>Michael on X.com</strong>: <a href="https://x.com/mkennedy?featured_on=talkpython" target="_blank" ><i class="fa-brands fa-twitter"></i> @mkennedy</a><br/></div>
Anarcat
The people vs the AI overlords
Previously in this series: The Four Horsemen of the LLM Apocalypse.
In a post to oss-security, my (Debian) co-developer Russ Allbery stated that "open source software [OSS] is coming face to face with a motivation crisis that has been building for a long time". His point is essentially that large language models (LLMs1) are making the existing OSS community crisis worse. For him, it's the flood of code reviews, but he argues that varies according to people's desires, for others it's security issues and so on.
I think Russ is right, but I would argue there's something much bigger than our open communities going on here, and it's about the entire field of computing. This pressure is on all of us, regardless of whether we work on open source software or not.
How people use models
People using LLMs in their workflow have radically changed how programming works, even for people who claim to avoid vibe-coding. And I'm sorry to single out one poor maintainer here: it's not you, Brian, you're just one example among many. But this is typical use of those models nowadays:
Once it’s done, I’ll use
/code-reviewand let Claude spawn sub-agents to do a full review of the new code. This usually finds some problems, even problems that the “main” Claude instance didn’t find during its validation. I usually keep running/code-reviewagain and again after finding and fixing issues, until there aren’t any left.
Think about what that means for a minute. This is automation built to
fire up dozens of agents crunching at a problem for minutes if not
hours of GPU compute time, in parallel. This is essentially a couple
of shelves in a datacenter rack, totally maxed out on power and
cooling, abstracted behind a cute little /code-review command.
The author, here, is rightly concerned that "Anthropic could pull the rug out and require API pricing", which is perhaps a code word for "charging something closer to actual costs". Brian also pays lip service to environmental and societal costs but those are largely abstracted away, so let's keep that conversation aside here as well, as we have discussed it before anyways.
But clearly, this way of working has an (externalized) cost, to say the least.
Paying for non-free tools
For decades my work has been focused on free and open source software. I've long stopped using proprietary operating systems like Windows or Mac, and even before that switch, I was mostly using free software on those platforms, partly out of principle, but also because I was too poor. So the tools of my trade are free, and I build free tools with them.
It feels like we're going backwards: when I was in school, a millennia ago, my classmates didn't have access to a compiler and were wondering how they would scrape the money to buy a compiler like Borland's or Microsoft's. I had a compiler built into my operating system (FreeBSD at the time), so that wasn't a problem for me. For them, it was a significant expense, but at least those expenses (or more shady sourcing of programs) were a one-shot deal.
Fast forward 30 years, and software is rented: you pay monthly for Adobe's Photoshop and Microsoft's office suite just like you pay for Netflix, Disney+ or Spotify2. And now you need to add dozens (if not hundreds of dollars) of monthly credits to access LLMs on top of that.
So, now we have to pay to get anything done? This is peak enshitification of our job: first they steal our work to train their models, and then they sell it back to us at a profit.
Attacking the engineers
AI is coming for our jobs, as engineers, if not everyone, according to the narrative. For a while now, our job market has deteriorated: less jobs, for less pay. Lots of skilled engineers looking for work and finding crap jobs then still looking while working.
This is not by accident.3 We engineers have a lot of power, it is not organized, but that's just a couple of unions away (easy!). Tech overlords know this, so they are attacking our profession, directly, by forcing us to train and use models that they can control.
Even in environments where programmers are not forced to use LLMs, the mere pressure of other people's LLM-generated work is huge. One can be forced to review LLM outputs, or just peer pressured you into producing more.
We're now supposed to accelerate delivery, because models can presumably do things so much better and faster. With supply chain security becoming such a large vector that we now have worms crawling around developers accounts on NPM, increasing the delivery cadence seems like a really bad idea.4
The LLM hype is part of the larger wave of cyberwar against workers, against water, against the Earth, against all the people. This is not a matter of individually "adapting to the reality" or personal choice, but a political, social, hard problem we need to address collectively.
Previously in this series: The Four Horsemen of the LLM Apocalypse.
- I again prefer the term LLM to "AI" because models do not possess intelligence. I did use it in the title because click baiting is apparently important, but I stopped short of calling this one "Rage Against the Machines" because that would be the title of every blog post I have ever made.↩
- Yes, I know that Visual Studio is kind of free now, but I wouldn't be surprised if they turn that into a rental as well, because why not.↩
- Beyond sabotaging the job market, Sam Altman event wants to sell "intelligence as a utility" something that is just a really bad idea but especially shows how megalomaniac those people are.↩
- This brings back memories of another era, walking us back decades in terms of computer security.↩
PyCharm
What’s Fixed and Improved in PyCharm 2026.2
Across the PyCharm 2026.2 release line, we shipped 263 fixes and improvements. Many improve Python code insight directly, with more precise type inference, fewer false positives, smarter completion and imports, and more reliable refactoring. Here are some of the smaller changes you’re likely to notice in everyday Python development.
SQLAlchemy 2.0 support
SQLAlchemy has been a long-standing source of false positives – enough that several duplicate tickets have accumulated over the years. This release resolves a batch of them for the 2.0 style.
String forward-references inside Mapped[...] resolve correctly:
posts: Mapped[list["Post"]] = relationship(back_populates="author") # "Post" now resolves to the model class
PyCharm also correctly infers the mapped type returned by Session.get(), instead of treating the result as the model class itself:
report = session.get(Report, report_id) reveal_type(report) # was: type[Report] | None now: Report | None
Modern hybrid_property setters written as @name.inplace.setter are recognized, so assigning to the property no longer produces a warning. Model class attributes defined via mixins are picked up again, too, clearing the old unexpected argument reports on model constructors.
(PY-78816, PY-65142, PY-59732, PY-51906, PY-28762)
Code insight and type inference
Control-flow narrowing and “unreachable code”
Several false This code is unreachable reports and instances of lost narrowing across loops have been fixed. The common issue: flow analysis either gave up or over-eagerly narrowed to Never in branches it should have kept alive.
isinstance on a numeric union no longer kills the else branch:
def foo(y: int | float) -> None: if isinstance(y, float): pass else: print(y) # was flagged unreachable, y inferred as Never
Narrowing also survives a while loop, so re-narrowing an optional attribute inside the loop body no longer reports a bogus has no attribute error.
(PY-83206, PY-83354, PY-88265)
Strings inside type annotations
A string used as metadata inside Annotated[...] – a Pydantic discriminator field name, for instance – is no longer parsed as a forward reference and flagged as unresolved.
Iterable unpacking and star expressions
PyCharm’s analysis of tuple and star unpacking could lose type information and fall back to Any. Unpacking a starred value into a tuple lost its element types, *-expansion collapsed to Any, and several genuine errors went unreported. Starred expressions preserve their element types:
def a() -> tuple[int, int]: return 2, 3 def b() -> tuple[int, int, int]: return (1, *a()) # no more bogus "Expected tuple[int, int, int]"
(PY-12592, PY-27205, PY-43585, PY-90219)
Augmented assignment
A cluster of false positives came from augmented assignments being misanalyzed. A simple /= on an int produced the wrong type:
foo = 5 foo /= 2 reveal_type(foo) # was: int now: float | int
(PY-80622)
Self and constructor return types
Self binds correctly through classmethod parameters typed as type[Self]:
class A: @classmethod def bar(cls, y: type[Self]) -> Self: ... x = A.bar(A) # was a spurious "Expected type[A], got type[A]" reveal_type(x) # was: Any now: A
Construction also respects __new__, __init__, and metaclass __call__. When __new__ returns something other than an instance, that’s the constructed type – even when an __init__ is present. The same fix covers explicitly parameterized calls like MyClass[int]() and __new__ assigned as a class attribute.
(PY-89296, PY-77611, PY-88644, PY-89571)
Enum members: Literal types for .value and .name
Reading an enum member’s .value or .name yields a precise Literal instead of a widened str or int, so assignments to Literal[...] target type-check. This matches mypy’s inference:
from enum import Enum from typing import Literal class E(Enum): a = "a" b: Literal["a"] = E.a.value # was: Expected 'Literal["a"]', got 'str' n: Literal["a"] = E.a.name # .name is a Literal too
Parameter types inferred from decorators
When a decorator constrains the callable it accepts, the decorated function’s parameters are inferred from that constraint instead of falling back to Any:
from typing import Callable def d(fn: Callable[[int], str]): ... @d def f(a): reveal_type(a) # was: Any now: int
(PY-79204)
Also fixed
- Keyword arguments in a class header are validated against the base class’s
__init_subclass__signature, and offered in completion (PY-79173). - An ellipsis in a
Callableused as a PEP 695 type-parameter bound no longer reports a bogus Invalid type expression (PY-83570). - Type-checker findings are split into granular suppression codes rather than a single
PyTypeCheckerid, and# noinspectiondirectives accept a simplified name form.PyTypeCheckerstill works as a blanket ignore (PY-90265).
Completion and auto-import
Smarter auto-import
Auto-import is now noticeably less noisy. Previously, if a module was already imported, PyCharm would offer to add a second, redundant import instead of qualifying through the one you already had. The quick-fix – and the completion popup – prefer to reuse the existing import.
Given pkg/src.py containing MyClass, and a file that already imports the module, Alt+Enter produces this:
from pkg import src # no longer flagged as unused src.MyClass
instead of adding from pkg.src import MyClass. The same reuse logic applies to plain import pkg.src, and to the auto-import completion on a second Ctrl+Space.
Nested classes can be auto-imported too, which is something PyCharm didn’t previously support:
# mod.py class Outer: class Inner: pass # main.py – Alt+Enter on Inner now offers "Import Outer from mod" from mod import Outer value = Outer.Inner()
(PY-87970, PY-87971, PY-87972, PY-88009, PY-88016)
Completion for unittest.mock.patch() targets
Patching by string target previously offered no code assistance, so dotted paths had to be entered manually. The string argument to mock.patch(...) gets code completion for modules, classes, and their attributes, and it no longer suggests the invalid as keyword mid-path:
from unittest import mock
# sample.py defines: class Foo: my_attr = 42
with mock.patch("sample.Foo.my_attr", 14):
...
# completion now offers `sample`, `Foo`, and `my_attr`
(PY-89189, PY-89191, PY-89192)
Typed signatures when overriding built-in methods
Completing an override of a dunder or built-in method fills in the full annotated signature – and auto-imports the types it needs – instead of bare parameters:
from types import TracebackType class A: def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None): ... # was: def __exit__(self, exc_type, exc_val, exc_tb):
(PY-79218)
Editor and inspections
Type inlay hints
Inferred type arguments are shown inline at the call site, so you can see what a generic resolved to without hovering over it:
class A[T]: def __init__(self, t: T): ... A[int](1) # [int] shown as an inlay hint
Type names rendered inside inlay hints – return types and solved arguments alike – are also clickable, so you can jump straight to a type’s definition from the hint.
f-string format-spec validation
PyCharm already validated the str.format() mini-language. Those checks apply to f-strings too, and PyCharm flags formatting a type that doesn’t implement __format__:
data = 1
f"{data:.2f}" # ok
f"{data:.2q}" # now flagged: unsupported format spec
class A: ...
f"{A():d}" # now flagged: A doesn't support the 'd' format
Refactoring
The Rename refactoring also updates references to a module when the module itself is renamed. Previously, the renaming left importing sites pointing at the old name:
# rename provider/provider_module.py → some_module.py from ..provider import provider_module # this reference is updated too
(PY-53274)
The Refactor | Field action is now Attribute, and the documentation says “instance attributes” to match Python terminology (PY-85828).
Conclusion
Taken together, these changes make PyCharm’s understanding of Python more precise and predictable: fewer false positives, better type inference, smarter completion, and less time spent working around cases where the IDE gets valid code wrong.
Many of these improvements started with real-world examples reported by users. If PyCharm still misunderstands a typing pattern, framework API, or other valid Python code in your project, let us know in YouTrack – a small reproducer can help us turn that friction into the next fix.
Try PyCharm 2026.2 and let us know which improvements make the biggest difference for your workflow.
Thank you for using PyCharm!
Python GUIs
Creating PySide6 UI without .ui / Qt Designer — Build your entire GUI in pure Python code, no .ui files required
Is it possible to write a PySide6 application without using .ui files or Qt Designer? How can I create widgets and lay out a window entirely in Python code?
Yes, absolutely. While Qt Designer is a helpful visual tool for building interfaces, you never need to use it. You can create every part of your GUI — windows, buttons, labels, layouts, and more — directly in Python. Many developers prefer working this way because it keeps everything in one place and gives you full control over your interface.
In this tutorial, we'll walk through building a PySide6 interface entirely in code, starting from a basic window and gradually adding widgets and layouts.
A minimal PySide6 window
Every PySide6 application needs two things: a QApplication instance, and at least one widget to display. Here's the simplest possible window:
import sys
from PySide6.QtWidgets import QApplication, QWidget
app = QApplication(sys.argv)
window = QWidget()
window.setWindowTitle("My App")
window.show()
app.exec()
Run this and you'll see an empty window with the title "My App". QWidget is the base class for all UI elements in Qt, and on its own it gives you a blank window to work with.
Adding a single widget
Let's add a button to the window. You can create any widget and place it inside your window by passing the window as the widget's parent:
import sys
from PySide6.QtWidgets import QApplication, QPushButton, QWidget
app = QApplication(sys.argv)
window = QWidget()
window.setWindowTitle("My App")
button = QPushButton("Click me!", window)
window.show()
app.exec()
The QPushButton is created with two arguments: the text to display, and the parent widget (window). This tells Qt that the button belongs inside the window.
This works, but the button just sits at position (0, 0) in the top-left corner. To arrange multiple widgets neatly, you'll want to use a layout.
Using layouts to arrange widgets
Layouts handle the positioning and resizing of widgets for you. Qt provides several layout types, including QVBoxLayout (vertical), QHBoxLayout (horizontal), and QGridLayout (grid-based).
Here's how to stack a few widgets vertically:
import sys
from PySide6.QtWidgets import (
QApplication, QLabel, QLineEdit, QPushButton,
QVBoxLayout, QWidget,
)
app = QApplication(sys.argv)
window = QWidget()
window.setWindowTitle("My App")
layout = QVBoxLayout()
layout.addWidget(QLabel("Enter your name:"))
layout.addWidget(QLineEdit())
layout.addWidget(QPushButton("Submit"))
window.setLayout(layout)
window.show()
app.exec()
Here we create a QVBoxLayout, add three widgets to it, and then apply the layout to the window with setLayout(). The layout takes care of stacking the label, text input, and button from top to bottom, and it will resize them sensibly if the user resizes the window.
Subclassing QWidget for your own windows
As your interface grows, putting everything at the top level of your script becomes hard to manage. The standard approach is to create your own window class by subclassing QWidget (or QMainWindow for more complex apps). This lets you set up the interface inside the __init__ method:
import sys
from PySide6.QtWidgets import (
QApplication, QLabel, QLineEdit, QPushButton,
QVBoxLayout, QWidget,
)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
layout = QVBoxLayout()
self.label = QLabel("Enter your name:")
layout.addWidget(self.label)
self.name_input = QLineEdit()
layout.addWidget(self.name_input)
self.submit_button = QPushButton("Submit")
layout.addWidget(self.submit_button)
self.setLayout(layout)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
By storing the widgets as attributes on self, you can refer to them later — for example, to read the text from the input field or to connect a button click to a function.
Connecting signals to make it interactive
Widgets in Qt communicate using signals and slots. When something happens — like a button being clicked — the widget emits a signal. You can connect that signal to any Python function to respond to the event.
Let's make the Submit button print a greeting:
import sys
from PySide6.QtWidgets import (
QApplication, QLabel, QLineEdit, QPushButton,
QVBoxLayout, QWidget,
)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
layout = QVBoxLayout()
self.label = QLabel("Enter your name:")
layout.addWidget(self.label)
self.name_input = QLineEdit()
layout.addWidget(self.name_input)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.greet)
layout.addWidget(self.submit_button)
self.greeting_label = QLabel("")
layout.addWidget(self.greeting_label)
self.setLayout(layout)
def greet(self):
name = self.name_input.text()
self.greeting_label.setText(f"Hello, {name}!")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
The line self.submit_button.clicked.connect(self.greet) tells Qt: "When this button is clicked, call the greet method." Inside greet, we read the text from the input and update the greeting label.
Using QMainWindow for menus and status bars
If your application needs a menu bar, toolbars, or a status bar, you can subclass QMainWindow instead of QWidget. QMainWindow provides a built-in structure with these elements ready to use.
With QMainWindow, you place your content inside a central widget rather than setting a layout directly on the window:
import sys
from PySide6.QtWidgets import (
QApplication, QLabel, QLineEdit, QMainWindow,
QPushButton, QVBoxLayout, QWidget,
)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
# Create a central widget and set it
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout()
central_widget.setLayout(layout)
self.label = QLabel("Enter your name:")
layout.addWidget(self.label)
self.name_input = QLineEdit()
layout.addWidget(self.name_input)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.greet)
layout.addWidget(self.submit_button)
self.greeting_label = QLabel("")
layout.addWidget(self.greeting_label)
# Add a status bar message
self.statusBar().showMessage("Ready")
def greet(self):
name = self.name_input.text()
self.greeting_label.setText(f"Hello, {name}!")
self.statusBar().showMessage(f"Greeted {name}")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
The pattern is the same — create widgets, add them to a layout, connect signals — but now you also get a status bar at the bottom of the window for free.
Nesting layouts for complex interfaces
You can combine layouts to create more sophisticated designs. For example, you might have a horizontal layout inside a vertical layout to place two buttons side by side:
import sys
from PySide6.QtWidgets import (
QApplication, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QVBoxLayout, QWidget,
)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
outer_layout = QVBoxLayout()
self.label = QLabel("Enter your name:")
outer_layout.addWidget(self.label)
self.name_input = QLineEdit()
outer_layout.addWidget(self.name_input)
# A horizontal row of buttons
button_layout = QHBoxLayout()
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.greet)
button_layout.addWidget(self.submit_button)
self.clear_button = QPushButton("Clear")
self.clear_button.clicked.connect(self.clear)
button_layout.addWidget(self.clear_button)
outer_layout.addLayout(button_layout)
self.greeting_label = QLabel("")
outer_layout.addWidget(self.greeting_label)
self.setLayout(outer_layout)
def greet(self):
name = self.name_input.text()
self.greeting_label.setText(f"Hello, {name}!")
def clear(self):
self.name_input.clear()
self.greeting_label.clear()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Notice that we use addLayout() (not addWidget()) to nest one layout inside another. You can combine QVBoxLayout, QHBoxLayout, and QGridLayout in any arrangement to build whatever interface you need.
Complete working example
Here's the final version bringing everything together — a QMainWindow with nested layouts, interactive widgets and a status bar.
import sys
from PySide6.QtWidgets import (
QApplication, QHBoxLayout, QLabel, QLineEdit,
QMainWindow, QPushButton, QVBoxLayout, QWidget,
)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Greeter App")
self.setMinimumWidth(300)
central_widget = QWidget()
self.setCentralWidget(central_widget)
outer_layout = QVBoxLayout()
central_widget.setLayout(outer_layout)
self.label = QLabel("Enter your name:")
outer_layout.addWidget(self.label)
self.name_input = QLineEdit()
self.name_input.setPlaceholderText("Your name here...")
outer_layout.addWidget(self.name_input)
button_layout = QHBoxLayout()
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.greet)
button_layout.addWidget(self.submit_button)
self.clear_button = QPushButton("Clear")
self.clear_button.clicked.connect(self.clear)
button_layout.addWidget(self.clear_button)
outer_layout.addLayout(button_layout)
self.greeting_label = QLabel("")
outer_layout.addWidget(self.greeting_label)
self.statusBar().showMessage("Ready")
def greet(self):
name = self.name_input.text()
if name:
self.greeting_label.setText(f"Hello, {name}!")
self.statusBar().showMessage(f"Greeted {name}")
else:
self.greeting_label.setText("Please enter a name.")
self.statusBar().showMessage("No name entered")
def clear(self):
self.name_input.clear()
self.greeting_label.clear()
self.statusBar().showMessage("Cleared")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Copy, paste, and run this file. You'll get a fully functional window with a text input, two buttons, a dynamic greeting label, and a status bar — all created without Qt Designer or any .ui files.
Everything you can do in Qt Designer, you can do in code. As your applications grow, you might find that building the interface programmatically makes it easier to manage, debug, and keep in version control alongside the rest of your Python code. To explore more of the available widgets, take a look at the PySide6 widgets tutorial.
For an in-depth guide to building Python GUIs with PySide6 see my book, Create GUI Applications with Python & Qt6.
Armin Ronacher
What Is Reasoning
A few weeks ago a paper was shared that showed how to extract reasoning traces from closed-weight models. Together with online discussions about tricking models into leaking them, it made me investigate it more out of curiosity. Twitter seems full of half-truths and confusion about how this works, so perhaps this helps some to understand what is happening.
Hiding Traces
Reasoning traces are usually hidden from us. We have lamented this, but mostly have to accept it. Open-weight models thankfully reveal them, and from their behavior you can see that their traces can be long and confusing. This is probably a good reason to separate them from what is normally shown to users.
At minimum, UIs need to detect them. The industry has done a good job at making reasoning traces sound special and exotic, but they really are just text: the model is trained to emit its thinking into a scratchpad as part of its response, before its final answer.
GPT-OSS’s Harmony response format makes this easy to see:
<|channel|>analysis<|message|>
I need to work this out ...
<|end|><|start|>assistant<|channel|>final<|message|>
The answer is ...
<|return|>
The markers are special tokens, but the reasoning between them uses “the same
text” as the final answer (just that GPT chain-of-thought text sounds really
funny). When the model samples the analysis channel token, a parser routes
the following text into a separate stream exposed through the Responses API.
For closed models, presumably a simple model redacts and summarizes it.
Reasoning Effort
How much budget goes to reasoning? Earlier APIs exposed reasoning token budgets, making it seem like a property of the sampling process. In reality, reasoning effort is baked into the system prompt. GPT-OSS puts this into the system prompt:
Reasoning: low
That’s it. Training produces the resulting behavior, such as emitting the
token sequence that switches to the analysis channel. This also explains why
changing the effort invalidates the KV cache. I think closed GPT models call
reasoning effort “juice,” since you can ask most models how much juice they
have.
In DwarfStar for DeepSeek with max reasoning this is added to the system prompt:
Reasoning Effort: Absolute maximum with no shortcuts permitted.
You MUST be very thorough in your thinking and comprehensively decompose the
problem to resolve the root cause, rigorously stress-testing your logic against
all potential paths, edge cases, and adversarial scenarios.
Don’t Think
The destination of reasoning tokens is therefore a learned convention: the
model is trained to keep scratch work out of the final channel. Trick it into
thinking it is in that channel and it may leak tokens. We have even seen older
models, when thinking is disabled, reason into the bash tool and echo their
thoughts to /dev/null.
So in some sense the only “special” behavior for some models is not to think.
That at times is done by “mechanically” removing the model’s usual ways to think.
In DwarfStar, disabled thinking uses the
prefill </think>, while enabled thinking uses <think>, which are the tokens
that close and start thinking. GPT-OSS doesn’t prefill but lets the model
decide either way on its own.
But presumably, some inference APIs prefill the opening token when reasoning is
enabled, so the model never samples it itself and might prevent the sampling of
the reasoning token when disabled since it can be trivially detected. This may
explain why a custom think
tool can
trick models into putting some reasoning where it should not go — but only when
native reasoning is disabled.
Hilariously enough I was unable to use GPT 5.6 terra for spell and grammar checking on this blog post because of safety filters. Had to switch to Kimi.
August 18, 2026
PyCoder’s Weekly
Issue #748: Line Breaks, OpenCode, Segfaults, and More (2026-08-18)
#748 – AUGUST 18, 2026
View in Browser »
Breaking Up (Lines) Is Hard to Do
Here’s a seemingly simple question: given a chunk of multi-line text, how do you split it and return an array? Unicode makes everything harder than it might first seem.
B-LIST.ORG
Coding With OpenCode: AI-Assisted Python
Learn how to use OpenCode for AI-assisted Python coding, using a free Gemini API key to analyze and refactor code right in your terminal.
REAL PYTHON course
The Top Open-Source Code Reviewer on Code-Review-Bench
PR-AF places #2 of 42 overall on Martian’s Code-Review-Bench, ahead of CodeRabbit, Copilot, and Devin. Roughly 3x more valid findings than the commercial tools, at ~10x lower cost per review. Verified findings only. Apache 2.0, self-hosted, runs on any open or closed model. Star & Deploy →
AGENTFIELD sponsor
Two Lines of Python That Segfault the Interpreter
Two lines of plain Python segfaulted every CPython with lazy annotations: PEP 749 gave SET_ADD an operand that user code can rebind.
TIMOFEI IVANKOV • Shared by Timofei Ivankov
Articles & Tutorials
OAuth 2.0 in Python: The Authorization Code Flow Explained
OAuth’s authorization code flow, explained from the four roles up: the redirect, the code-for-token exchange, state, PKCE, and refresh tokens, with a complete runnable Python script and a table of the OAuth errors you’ll actually hit.
ENDPOINT51.COM • Shared by Simon O’Connor
The Problem With pandas Isn’t Performance
This opinion piece argues that pandas’ biggest everyday cost is often cognitive overhead rather than execution time and explores whether a backend-independent data-analysis DSL could reduce it
NEAL HUGHES • Shared by Neal Hughes
🐍 Object-Oriented Python, Taught Live
The Object-Oriented Python live cohort begins August 24. Five 2-hour Zoom sessions Mon to Fri build one growing application end to end, with OOP features introduced as the code starts needing them: classes, the data model, inheritance vs composition, properties, dataclasses. Learn more →
REAL PYTHON sponsor
Security of Everything at PyCon 2026
This year’s PyCon US added a dedicated security track. Talk Python interviews some of the key folks from the center of it: Juanita Gomez, Mike Fiedler, and Seth Michael Larson.
TALK PYTHON podcast
The HTML Representation of the Index API Is Now Frozen
‘PyPI has adopted PEP 833, which “freezes” the HTML representation of the index API, which is also sometimes called the “simple API” or the “simple repository API.”’
PYPI.ORG
How to Integrate OpenTelemetry With a FastAPI App
Learn how to integrate OpenTelemetry with a FastAPI app in Python to instrument endpoints, export traces, add custom spans, and correlate logs.
REAL PYTHON
Qt Designer and Python: Build Your GUI Applications Faster
Learn how to use Qt Designer to build Python GUIs with PyQt6. Create your main windows and dialogs visually, then load them in your app.
REAL PYTHON
Admin Form Injection
Learn about a hidden Django admin feature for passing values between pop-up windows on the web.
TIM SCHILLING
Caller-Specific Coverage
An idea for a new feature for coverage measurement: per-caller coverage
NED BATCHELDER
Projects & Code
pyrig: Aautomate Python Project Setup
GITHUB.COM/WINIPEDIA • Shared by Winipedia
Events
Weekly Real Python Office Hours Q&A (Virtual)
August 19, 2026
REALPYTHON.COM
PyCon Ghana 2026
August 20 to August 23, 2026
PYCON.ORG
PyCon Latam 2026
August 20 to August 24, 2026
PYLATAM.ORG
PyData Bristol Meetup
August 20, 2026
MEETUP.COM
OSS Security Meetup: Mike Fiedler, PyPI Safety and Security Engineer
Wednesday, August 26
LUMA.COM • Shared by Mila Zhou
PyCon JP 2026
August 21 to August 24, 2026
PYCON.JP
DjangoCon US 2026
August 24 to August 29, 2026
DJANGOCON.US
PyCon AU 2026
August 26 to August 31, 2026
PYCON.ORG.AU
PyCon PL 2026
August 27 to August 31, 2026
PYCON.ORG
PyCon Kenya 2026
August 28 to August 30, 2026
PYCON.KE
PyCon Togo 2026
August 28 to August 30, 2026
PYTOGO.ORG
Happy Pythoning!
This was PyCoder’s Weekly Issue #748.
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 ]
Python Bytes
#492 Codeberg Puts Head in Sand
<strong>Topics covered in this episode:</strong><br> <ul> <li><strong>Python 3.12.14, 3.11.16, 3.10.21 - security releases</strong></li> <li><strong><a href="https://lucumr.pocoo.org/2026/7/24/codeberg-divides/?featured_on=pythonbytes">Codeberg’s AI-code ban tests its role as a GitHub alternative</a></strong></li> <li><strong>Brett Cannon: what's missing for reproducible builds on PyPI</strong></li> <li><strong><ul> <li>nothing records the source code a distribution came from. <code>direct_url.json</code> captures it when you install from a repo or archive, so the fix is putting the same info in sdist/wheel metadata.</li> </ul></strong></li> <li><strong><ul> <li>recording the build tools. Wheels can already do this via PEP 770 SBOMs in <code>.dist-info/sboms/</code> - sdists can't, since they're a tarball plus a precalculated <code>PKG-INFO</code> with nowhere to hang extra metadata. Either "don't use sdists" or an sdist v2.</li> </ul></strong></li> <li><strong>Extra extra extra, hear all about it</strong></li> <li><strong>Extras</strong></li> <li><strong>Joke</strong></li> </ul><a href='https://www.youtube.com/watch?v=5wBExYOSx0A' style='font-weight: bold;'data-umami-event="Livestream-Past" data-umami-event-episode="492">Watch on YouTube</a><br> <p>Sponsored by <strong>Logfire from Pydantic</strong> <a href="https://pythonbytes.fm/logfire">pythonbytes.fm/logfire</a></p> <p>This episode is brought to you by Pydantic Logfire. It's observability for AI apps from the team behind Pydantic - agents, LLMs, APIs, database, and infrastructure in a single trace, queried with Postgres-compatible SQL. Your coding agent can query it too, through their MCP server. I'll tell you more later.</p> <p><strong>Connect with the hosts</strong></p> <ul> <li>Michael: <a href="https://fosstodon.org/@mkennedy">Mastodon</a> / <a href="https://bsky.app/profile/mkennedy.codes?featured_on=pythonbytes">BlueSky</a> / <a href="https://x.com/mkennedy?featured_on=pythonbytes">X</a> / <a href="https://www.linkedin.com/in/mkennedy/?featured_on=pythonbytes">LinkedIn</a></li> <li>Calvin: <a href="https://sixfeetup.social/@calvin?featured_on=pythonbytes">Mastodon</a> / <a href="https://bsky.app/profile/calvinhp.com?featured_on=pythonbytes">BlueSky</a> / <a href="https://x.com/calvinhp?featured_on=pythonbytes">X</a> / <a href="https://www.linkedin.com/in/calvinhp/?featured_on=pythonbytes">LinkedIn</a></li> <li>Show: <a href="https://fosstodon.org/@pythonbytes">Mastodon</a> / <a href="https://bsky.app/profile/pythonbytes.fm">BlueSky</a> / <a href="https://x.com/PythonBytes?featured_on=pythonbytes">X</a></li> <li> Join us on YouTube at <a href="https://pythonbytes.fm/stream/live"><strong>pythonbytes.fm/live</strong></a> to be part of the audience. Usually <strong>Tuesday at 7am PT</strong>. Older video versions available there too.</li> </ul> <p>Finally, if you want an artisanal, hand-crafted digest of every week of the show notes in email form? Add your name and email to <a href="https://pythonbytes.fm/friends-of-the-show">our friends of the show list</a>, we'll never share it.</p> <p><strong>Calvin #1: Python 3.12.14, 3.11.16, 3.10.21 - security releases</strong></p> <p>https://blog.python.org/2026/08/python-31214-31116-31021/</p> <ul> <li>Source-only security releases for the three branches now in security-fix-only mode; release team blamed the European solar eclipse for the timing.</li> <li><code>tarfile</code> hardening. Multiple path-traversal bypasses of the <code>data</code> filter closed, including a symlink escape that bypassed the CVE-2025-4330 fix; <code>extract()</code> now applies the filter to link targets too.</li> <li>Four fresh CVEs: CVE-2026-2297 (<code>SourcelessFileLoader</code> not using <code>io.open_code()</code> for <code>.pyc</code>), CVE-2026-4224 (expat crash on deeply nested content models), CVE-2026-3644 (control chars in <code>http.cookies.Morsel</code>), plus the completed CVE-2021-4189 fix in <code>ftplib.ftpcp</code>.</li> <li>Quadratic-complexity DoS cleanup across the stdlib: <code>HTMLParser</code>, <code>configparser</code> regexes, <code>unicodedata.normalize()</code>, <code>csv.Sniffer.sniff()</code>, and ElementTree XPath index predicates.</li> <li>Header/injection fixes: CR/LF rejected in <code>HTTPConnection.set_tunnel()</code>, control chars blocked in <code>wsgiref.handlers</code> status, and <code>webbrowser</code> now rejects leading dashes (plus a <code>%action</code> prefix bypass).</li> <li><code>http.client</code> now caps chunked trailer lines and 1xx interim responses at 100 each - a hostile server could previously hang the client forever despite a socket timeout.</li> <li>Memory-safety odds and ends: stale pointers in lzma/bz2/zlib decompressors after <code>MemoryError</code>, a bz2 stack overflow on reuse-after-error, and bundled libexpat bumped to 2.8.3. If you're still on 3.10, 3.11, or 3.12 - and you extract tarballs from anywhere you don't fully control - this one's not optional.</li> </ul> <p><strong>Michael #2: <a href="https://lucumr.pocoo.org/2026/7/24/codeberg-divides/?featured_on=pythonbytes">Codeberg’s AI-code ban tests its role as a GitHub alternative</a></strong></p> <ul> <li>Armin’s article “<a href="https://lucumr.pocoo.org/2026/7/24/codeberg-divides/?featured_on=pythonbytes">Codeberg Divides</a>”</li> <li>Armin Ronacher argues that Codeberg’s new terms, which prohibit projects mostly written with generative AI, create a vague and difficult-to-enforce boundary. His larger concern is that a democratically governed host can still be unpredictable or ideologically narrow, weakening Codeberg’s potential as a broad European alternative to GitHub. <ul> <li>The strongest question for Python developers is whether repository hosting should judge legal open source by how code was produced, or focus on behavior and resource abuse.</li> <li>“Mostly generated” is hard to measure in modern codebases where developers mix handwritten code, completions, agents, and generated refactors.</li> <li>Ronacher suggests clearer alternatives: ban all LLM involvement, or target autonomous repository spam, abusive resource use, and low-quality generated contributions directly.</li> <li>Codeberg is free to choose a values-driven community, but that may conflict with being predictable, neutral infrastructure and a serious GitHub competitor.</li> <li>Worth discussing: can open-source communities set meaningful AI boundaries without driving maintainers and projects into opposing camps?</li> </ul></li> <li>Very first search for these <a href="https://xn--gckvb8fzb.com/i-regret-migrating-to-codeberg/?featured_on=pythonbytes">terms lands on this page</a>. <ul> <li><em>Codeberg looked like a viable alternative. … Unfortunately, the latest update to its terms of service seems to mark a first step in changing one part I moved there for, namely the “freedom” part.</em></li> </ul></li> </ul> <p><strong>Sponsor</strong>: <a href="https://pythonbytes.fm/logfire">Logfire from Pydantic</a></p> <p>Your AI agent failed at 2am. Was it the model? A tool call? The database? Most observability tools can't tell you, because they only see part of your stack. Pydantic Logfire sees all of it. One trace across your agents, LLMs, APIs, and database. Down to the infrastructure: services, Kubernetes, and hosts. It's built on OpenTelemetry, with SDKs for Python, TypeScript, and Rust, and it works with any OTel-compatible language. Every prompt, token count, and cost, right next to your vector searches and API calls. You query everything with Postgres-compatible SQL. And so can your coding agent, through the Logfire MCP server. Stop guessing. Read the trace. Pydantic Logfire. AI, it's still just engineering. Visit <a href="http://pythonbytes.fm/logfire">pythonbytes.fm/logfire</a> today and sign up today. Get 10M records free every month, no card required. You can even click “Onboard with your coding agent” to copy a prompt to have claude or codex integrate Logfire into your app. Thanks to Pydantic for supporting the show.</p> <p><strong>Calvin #3: Brett Cannon: what's missing for reproducible builds on PyPI</strong></p> <ul> <li>Framing came out of his 2026 Python Packaging Council nomination - the secure-supply-chain gap he found is that Python has no defined way to do reproducible builds at all.</li> <li>Design goal is zero friction: producers uploading to PyPI shouldn't have to do anything. The work lands on build backends and installers.</li> <li>Gap #1: nothing records the source code a distribution came from. <code>direct_url.json</code> captures it when you install from a repo or archive, so the fix is putting the same info in sdist/wheel metadata.</li> <li>Gap #2: recording the build tools. Wheels can already do this via PEP 770 SBOMs in <code>.dist-info/sboms/</code> - sdists can't, since they're a tarball plus a precalculated <code>PKG-INFO</code> with nowhere to hang extra metadata. Either "don't use sdists" or an sdist v2.</li> <li>The replay mechanism already exists: <code>[build-system]</code> in <code>pyproject.toml</code> is a defined entry point, so if backends recorded their own environment, you could reinstall and re-run the build.</li> <li>Payoff idea: trusted third parties report successful reproductions back to PyPI, which displays "independently reproduced by X" - surfaced in the index API so installers could prefer reproduced files.</li> <li>Explicitly framed as a perk, not a requirement - roughly SLSA build level 1, no shaming projects that don't opt in. Verbal kicker option: "And don't think pure-Python wheels are off the hook. Something built that wheel, and if that something was compromised, so is your wheel. SolarWinds was a build-process attack."</li> </ul> <p><strong>Michael #4: Extra extra extra, hear all about it</strong></p> <ul> <li><a href="https://docs.python.org/release/3.14.7/whatsnew/changelog.html?featured_on=pythonbytes">Python 3.14.7</a></li> <li>Upgraded the MCP servers to 2026-07-28 v2 protocols (<a href="https://talkpython.fm/ai-integration?featured_on=pythonbytes">talk python</a>, <a href="https://pythonbytes.fm/ai-integration">python bytes</a>)</li> <li>Got agentsview running <a href="https://www.agentsview.io/pg-sync/?featured_on=pythonbytes">synced via postgres</a></li> <li>Talk Python courses, <a href="https://training.talkpython.fm/team-trial?featured_on=pythonbytes">teams trial offering</a></li> <li>Talk Python courses, <a href="https://training.talkpython.fm/government?featured_on=pythonbytes">government procurement offering</a></li> <li><a href="https://www.audible.com/pd/Lean-TDD-Audiobook/B0HDV6DM5S?eac_link=kbTXxf8oGNDB&ref=web_search_eac_asin_1&eac_selected_type=asin&eac_selected=B0HDV6DM5S&qid=VQkJQUTSmK&eac_id=141-5188421-6797858_VQkJQUTSmK&sr=1-1&featured_on=pythonbytes">Lean TDD audio book is out</a></li> </ul> <p><strong>Extras</strong></p> <p>Calvin:</p> <ul> <li><strong>uv now prefers post-quantum key exchange -</strong> https://github.com/astral-sh/uv/releases/tag/0.12.4</li> </ul> <p><strong>Joke: <a href="https://turnoff.us/geek/beware-of-dog/?featured_on=pythonbytes">Beware of dog</a></strong></p>
Bob Belderbos
Guardrails Protect Your Codebase. What Protects Your Judgment?
Cal Newport's On AI Coding and Its Discontents lands on an uncomfortable point: the speed AI gives you is paid for with the thinking that made us good engineers in the first place.
Read code instead of writing it and you get passive recognition where you used to have an active model of the code.
The vendors sell one answer to this: more AI. The nostalgic answer is the opposite: go back to writing everything by hand. I am searching for a midway.
No right answer, but at least a direction to pursue
When I posted about this recently, I offered 3 options: write more code manually, double down on AI, or find some middle with more friction. I got some valuable comments from developers picking the middle. And that's where I've landed too.
The more I read and think about it, the more the middle stops looking like a compromise. It seems the way forward, but only if you specify what friction to keep.
Skill atrophy isn't caused by using AI per se. It's caused by delegating the part that builds your internal model.
I've argued that the friction is where the learning lives, and nothing about agents changes that. What changes is that it's now easier to bypass friction and it's on us to keep it in.
It helps to separate two problems here, because AI creates both and they aren't the same. One is skill atrophy: stop doing the thinking and your mental model fades. The other is correctness: AI writes plausible code that can be subtly wrong.
Let's look at each in turn.
Do you 'own' the code?
A sharp reply came from someone on Bluesky:
agents draft, but re-derive the hard path before merge. If you can't explain a weird branch without re-reading the whole file, you rewrote too little.
Not all code is created equal. The boundary conditions, error paths, and testing assumptions are where we as engineers pull our weight.
For every diff, can you explain the change? What are the edge cases and risks?
In practice I find that this gets easier when you scope the agent to narrow, well-defined tasks instead of blanket features. Small tasks keep you in the loop, stop the agent from drifting, and prevent giant code reviews nobody wants to read.
I also like how Corey Schafer summarized how he keeps the friction:
- Use detailed prompts, not one-liners: spec the tools, architecture, and reuse saved research prompts that force citing official docs.
- Review everything: read and understand all generated code like a junior's PR you'd never merge blindly.
- Interrogate unfamiliar code / concepts: make the AI explain things in plain English, pull docs, and justify the approach and trade-offs.
- Push back: offer your own approach, ask if the AI's is better/worse/different, and fold what you learn into your own toolkit.
Push the rest into the system
Those four habits are all manual. They keep your judgment in the loop, but they run on your attention, and attention is finite. You can't re-read every branch on every diff forever. So whatever those habits can't cover by hand, you push into the system and let it enforce the rest automatically.
A commenter on Threads put it well:
the real fix is correctness by design, stronger types, compile-time assertions, more "faff in the way." We never fully adopted that stuff because it takes patience humans run out of. AI doesn't. It learns your constraints once, then works inside them on every diff after.
And on Fosstodon:
I feel like all these systems have too many surprises, and making invalid states illegal is the best defensive alternative to not using them, which would be the first choice. Once you get guarantees, if you can type faster with gen ai than your fingers, maybe thats worth the squeeze depending on your morals.
That's why Rust is so appealing to me. Making invalid states unrepresentable and the agent has to comply with this. I discussed this here.
Python gives you less strict guarantees, but we can approximate it. Your type checker (ty, pyrefly, mypy) catches the class of type errors the model would otherwise slip past you. Pydantic models validate at the boundary, so bad data fails loudly instead of drifting three functions deep. And well-thought out test cases can challenge plausible looking code. And for AI generated tests you can use mutation testing (an article on this soon here ...)
So balancing responsible AI use has two sides. Re-derive the decisions that matter, which keeps your judgment sharp. And encode stricter rules to keep your agent accountable to higher standards. One protects your mental model, the other protects the code base. Both protect peace of mind.
As said before: the tooling / way of working has changed, but AI doesn't change what software engineering is; we can automate some of the boring parts, but the majority of the work (including the code-producing part) is a lot of back-and-forth and applying experience and judgment.
Keep reading
- How to Keep Your Developer Instincts When AI Writes the Code
- AI Is an Accelerator, Not a Compass
- From 1,069 to 156 LOC: Design Over Code
Newport's worry is right: outsourced thinking atrophies. The escape isn't more or less AI. It's identifying, every time you build software, the small set of decisions you refuse to let the model make for you.
Can you explain each change? Do you truly own it? Did you keep making the hard decisions yourself? And are strict guardrails in place to protect the codebase when AI writes more of the code?
Reach out to me if you want to chat about this. It feels a bit like the wild west right now, and nobody has final answers. I'm really keen to hear how you're navigating the fast pace of change our industry is going through.
Seth Michael Larson
When str.lower() is a security vulnerability in Python
Some internet standards only support ASCII characters, but the world uses much more than the Latin alphabet. Thus, a mapping from Unicode to ASCII for use in domain names is required.
NamePrep was part of that solution, defined in RFC 3491 as a profile of StringPrep, and is crucially a component of Internationalizing Domain Names in Applications (IDNA), also known as “IDNA 2003”. The StringPrep algorithm is defined in RFC 3454. IDNA 2003 has been obsoleted by IDNA 2008 defined in RFC 5890, 5891, 5892, and 5893.
Python supports IDNA 2003 through the idna codec (str.encode('idna')) and
IDNA 2008 is supported by the idna package on the Python package Index.
Python's implementation of StringPrep is implemented in the stringprep module
in the standard library. In general, you should be using the idna package (IDNA 2008)
and not .encode("idna") (IDNA 2003), but sometimes you
do need the older behavior.
StringPrep defines the “case folding” step (case folding is approximately “how to lowercase/uppercase a codepoint”) in Section 3.2, enabling case-insensitive
comparisons of strings, by mapping all characters through mapping tables B.2 and B.3.
B.2 is effectively str.lower(), lowercasing all characters according to
Unicode rules and B.3 contains the exceptions. The Python code implementing
this (and assuming B.3 table is captured correctly) is the following code below:
def map_table_b3(code):
r = b3_exceptions.get(ord(code))
if r is not None: return r
return code.lower()
And that might seem fine... and the title probably gave it away already.
The str.lower() call in this function is a vulnerability!
Why? Because str uses whatever Unicode data that the particular Python
interpreter is shipped with, you can figure out what Unicode version your
Python interpreter uses by accessing unicodedata.unidata_version:
>>> import unicodedata
>>> unicodedata.unidata_version
'17.0.0'
There's also a database of Unicode 3.2.0 data available on every version
of Python (unicodedata.ucd_3_2_0) specifically for the StringPrep and IDNA algorithms:
$ grep -I "ucd_3_2_0" -R Lib/
Lib/stringprep.py:from unicodedata import ucd_3_2_0 as unicodedata
Lib/encodings/idna.py:from unicodedata import ucd_3_2_0 as unicodedata
This is important! StringPrep depends on this specific version of Unicode
to operate consistently, the B.2 and B.3 tables in RFC 3454 are essentially
Unicode 3.2.0 case-folding rules encoded into
a table. So we need to use Unicode 3.2.0 case-folding rules, not newer
Unicode case-folding rules. This is why calling str.lower() represents
a difference in the implementation and the specification,
and therefore a vulnerability:
# RFC 3454 compliant value ('Ꭰ' is U+13A0)
>>> "ᎠᎠ".encode("idna")
'xn--58da'
# Value if using Unicode 17.0.0 case-folding
>>> "ᎠᎠ".encode("idna")
'xn--kz9aa'
The fix was to create new exceptions so that str.lower() would behave
as if it was using Unicode 3.2.0 for only particular function. So, we
go through each Unicode codepoint and record when the behavior of
str.lower() is different when comparing the Unicode version shipped with Python and Unicode 3.2.0.
And that's all, now IDNA 2003 is consistent with the specification.
Thanks to Bitshift for reporting the vulnerability, Stan Ulbrych for co-developing the remediation, and Marc-Andre Lemburg and Petr Viktorin for reviewing the remediation. See CVE-2026-17084 for more details.
My work as the Security Developer-in-Residence at the Python Software Foundation is sponsored by Alpha-Omega. Thanks to Alpha-Omega for supporting security in the Python ecosystem.
Thanks for reading ♥ I would love to hear your thoughts! Contact me via Mastodon, Bluesky, or email. Browse the blog archive. Check out my blogroll.
August 17, 2026
The Python Coding Stack
Back From Holidays • SOLID Ideas, Agents, Rust
It feels good to be back from holiday. Feeling refreshed, mostly.
The to-do list is longer, the inbox is noisier, and home has thoughtfully provided its own backlog: a replacement car, a new school uniform, and a garden that has staged a minor coup.
I may need another holiday already.
And that’s before even talking about programming and the revolution it is currently going through. I’m a Python educator. Somewhat unusually for Python educators, I’ve been a full-time Python educator for over a decade.
But AI is changing how people learn and what people need to learn. Like many others, I find myself fascinated by what I can now do with AI, while also aware that it is changing the economics of work that has supported me for years.
It’s time to find out how anti-fragile I am. Over the coming weeks and months, I’ll write about using AI, I’ll share my thoughts about programming in 2026 and beyond, and I’ll tell you what I think about how learning and teaching are changing.
So I am going to explore this in public: how we learn, teach, write, and assess code in an AI-shaped world.
Here is some signposting for what else you will read on these pages in the coming months:
SOLID Ideas • I have been wanting to write a series on SOLID programming principles for a while. They matter more than ever when we want AI agents to write robust code for us rather than plausible-looking but brittle code. The series will look not only at what the principles are, but why they matter and what can go wrong when we ignore them.
Agents Unpacked • Before going on holiday, I started this new section on this publication. I am thinking about republishing the first two posts in a significantly updated form, then carrying on at pace. Perhaps it will become a book, too. Agents are here, and I want to understand what they are, how they differ from LLM chat interfaces, and how to use them efficiently. You can join me on that learning journey.
Learning Rust in Public • I also want to learn Rust and will set up another section here to do that in public, as they say these days. I will use a similar format to Agents Unpacked, in which my personal learning agent and I have a discussion as I learn from it.
So there is plenty on the horizon here at The Python Coding Stack: plenty of Python, and plenty of Python-adjacent stuff, too. And some other stuff, too. I hope you will join me for the journey.
Brian Okken
Lean TDD Book Launch
Hey all. I’m super excited to announce the launch of my new book.
It’s Lean TDD: TDD Without the Waste.
That link in the last sentence is a link to leantdd.com.
There you’ll find links to all the places you can read or listen to it.
Right now, it’s available at:
- Amazon as a paperback
- Amazon as an ebook
- Audible as an audio book
- Other platforms to come later …
In the next few months, it will get onto other platforms. I’ve already had requests for Kobo, and just raw files sold through my site, and I’d also like to get it onto library platforms.
August 16, 2026
Brett Cannon
What's missing to have reproducible builds on PyPI
While writing the section of my 2026 Python Packaging Council (PPC) nomination on secure supply chain, I realized that one thing related to having a secure supply chain that we lack is a defined way to perform reproducible builds. The reason I like the idea of making reproducible builds work is that I think it can be done in such a way as to not require any work on the part of the producer of a distribution (which is a technical term for sdists or wheels, i.e., the people who upload stuff to PyPI), and thus make reproducible builds very low-friction for people to opt into supporting.
Why you should care
In terms of secure supply chain, reproducible builds can let independent 3rd parties verify that the bits in a distribution match what&aposs expected based on the source code the distribution was made from. That lets you potentially detect if anyone tampered with the code during the build process. As well, there&aposs a side-effect that from having to record the software involved in the build process means you can more easily detect if some known, compromised tool was used even if you don&apost do everything necessary to verify the bits are exactly the same. And this isn&apost some hypothetical benefit: SolarWinds was compromised due to malicious code being injected into the build process.
And don&apost let the word "build" fool you into thinking that pure Python wheels aren&apost vulnerable. There&aposs a build backend that was used to make that wheel, and if that build back-end was compromised then it may inject some malicious code into the wheel. So there isn&apost some area in Python packaging that gets to ignore these risks.
What&aposs missing from the specs
Where&aposs the source code?
So what do we need in Python packaging to make reproducible builds possible? First, we need to record what source code was used. This isn&apost recorded in sdists or wheels, but if you install directly from a source repository or an archive then it&aposs recorded in a direct_url.json file by your installer. If we were to record the same information in sdists and wheels (such as in the metadata), then we would know the location of the source code used to make the distribution.
What software was used to make the distribution?
But the next tricky bit is recording all the tools used to create the distribution. For wheels, we have support for recording software bill of materials (SBOMs). And as PEP 770, which added that support, says (disclaimer: I was the PEP delegate), SBOMs can be used to record the build tools used to create a distribution. And if you record all the software used to build a distribution, you can hopefully reproduce the exact same bits and show nothing was tampered with.
Unfortunately, sdists don&apost have a similar mechanism to record SBOMs. Since sdists are usually just a tarball with a PKG-INFO file that contains some precalculated metadata, there isn&apost a place to put any other metadata file. So we either have to shrug and say, "don&apost use sdists if you want reproducible builds," or we will have to come up with some sdist v2 format that allows for more structured data.
Who records what software was used?
Now, you might be wondering how you would even go about reproducing a build even if you do have all of this information. Luckily, since we have the [build-system] table in pyproject.toml, we have a defined entry point into the build back-end that produced the distribution. That means once we have all the software required to run the build backend recorded, we can replay the build process by installing the same things and calling the build backend as defined by [build-system]. And if build backends took care of recording what&aposs installed in the environment they are running in, theoretically we could record all the software used for the build back-end and store it in SBOMs today without distribution producers having to do extra work (but it does mean work for the pip maintainers).
Surfacing reproducibility on PyPI
Assuming all of this comes to pass and we record the where the source code is that went into a distribution and the software used to make the distribution, how do we make it useful to people? Does every person who cares about having a secure supply chain have to rebuild everything they use themselves? Is there some way for even people who don&apost care about this stuff to benefit?
One possible way is if there were trusted verifiers who could tell PyPI when they successfully reproduced a distribution. Since various enterprises are going to be doing this anyway, they could feed that information back to PyPI so they can visibly say for a distribution file, "this file was independently reproduced by <name of trusted party>". That means users get to know a distribution is what the creator expected, and the verifier gets a bit of recognition for helping the community out. This could even be surfaced in the index API so you could have installers prefer reproduced distributions.
This should be done in a way not to shame anyone who happens not to use a build backend that can create a distribution that can be reproduced. This should always be viewed as a perk and not a requirement. It&aposs like getting to say your project meets build level 1 of SLSA (which all of this would meet); some people like getting to say that, but it isn&apost a knock against anyone who chooses not to care.
Acknowledgments
Thanks to Seth Larson for listening to my idea and also checking over this blog post.
August 15, 2026
Juri Pakaste
TIL: Typed do in Swift
TL;DR: Swift has do throws(MyError). It's helpful.
This is one of those "I can't believe I had missed that" things.
I've been pretty enthusiastic about adopting typed throws in Swift. If you need to process certain kinds of errors, it just makes sense to me. However, it was always a bit painful. You have this:
do {
// throwing code here
} catch {
// error is precisely typed here, assuming
// the block above throws just one type
}
… and all is fine and good, except that the "throwing code" doesn't need to
grow particularly complex before Swift decides to widen the type to any Error
and your catch block starts producing compilation errors.
When I ran into this, I'd use catch let error as MyError. Fine. Except it
isn't, because now your error-handling code is incomplete: you have to add
an unreachable catch-all block, and you're unhappy with the language and
your life choices.
I did page through the enhancement proposal when it passed through Swift Evolution, but I just never noticed that it doesn't end there. What you do in this situation is this:
do throws(MyError) {
// throwing code here
} catch {
// now the compiler doesn't get confused and
// all is well-typed unicorns and sunshine
}
Now you know too.
August 13, 2026
PyCharm
Open weight models are having a moment, driven by control, choice, and cost. Hybrid and local AI are now getting serious looks, so JetBrains teamed up with DeepLearning.AI on a free AI Coding Workflows: Hybrid to Local course that covers the ideas and options.
The course is now available and uses PyCharm and its AI Chat. Here’s a peek into the course.
Claude Code: Subagents and cheaper models
We start the course with, well, not-local. Instead, we use what you already know – Claude Code and its Anthropic models – to introduce some of the techniques and “levers” that help bring choice, control, and even cost reduction. (Yes, I wrote emdashes.)
We did a previous course on Spec-Driven Development (SDD) so of course, we wanted to start there. Smaller models struggle with big, open-ended “vibe coding.” Dividing and bounding the work keeps smaller models on track. Important note: this course’s example app is really basic. You might say “that’s too easy.” But that’s part of the takeaway: big brain models can do the upfront work, forming right-sized steps for smaller models.
We then illustrate this division with a Claude Code subagent. The main chat prompt implements each roadmap phase in a fresh subagent, to better manage context. This then gives the payoff: a cheaper model for the implementer. Use a “big brain” (Opus) for main conversation thinking and a “little brain” (Haiku) for implementation.
Each lesson finishes with metrics about the change in tokens, turns, cost, and estimated wall time. Which brings us to the main course goal: learning the ideas instead of the specifics, which change weekly.
New agent, inference, and model
That covers the four levers:
- Specs shaped for the model size
- Specialist subagents to divide work
- Cheaper models for the routine work
- Collect metrics as evidence to guide thinking
The course then introduces choice and control:
- New agent: OpenCode
- New inference router: OpenRouter
- New model and inference host: DeepSeek (via OpenRouter) by moving to a new agent (OpenCode) using inference routing (OpenRouter) to inference hosting and models (DeepSeek)
We first move to OpenCode, running in PyCharm. JetBrains wants our IDEs to be open platforms for agents and models. This makes the move from Claude Code to OpenCode straightforward: it’s the same UI. We add OpenRouter (a paid step), connect it to OpenCode, and choose DeepSeek as a model.
Next we repeat our sequence: all in one chat, then context isolation using a subagent. But this time, with a different agent and model.
We finish by making a dedicated implementer subagent in Markdown. This gives quite a number of levers of control: in the frontmatter for mandatory controls, and in the subagent body for “persuasion” guidance. Most importantly, we have the implementer use the smaller DeepSeek v4 Flash model as the “little brain.”
Compared to the Claude Code version, the metrics were, unsurprisingly, a lot cheaper.
Hybrid and Local
Now for the main attraction: for routine development, can we do some – or even all – of the work locally?
We start with a lesson on setting up local AI: LM Studio as the inference server and Gemma 4 12B as the local model, targeting a 32 GB laptop.
We then configure the implementer subagent to use this local Gemma 4 model, promoting DeepSeek v4 Flash from last lesson’s “little brain” up to “big brain.” The results? Quite good, as it turns out.
Then the big test: fully local, with Qwen 3.5 27B as the “big brain.” The results: better than expected, showing that guardrails help.
How did hybrid and local do? Both of these lessons finish with a review of the metrics. That’s one of the big course takeaways: look at the evidence. You can see how small models struggle, and see the effect of helping them succeed.
Hybrid and Local AI Are Heating Up
Much thanks to DeepLearning.AI both for working with us again and for pushing to get this out fast. This topic is now red-hot in the news: Sovereign AI, privacy and security, and of course cost. The innovations are coming really fast and it is important to have a gentle introduction to the fundamentals.
We’ll do more updates here on Local AI for control, choice, and cost. Most of all, we at PyCharm believe in the human-in-the-loop. Stay tuned for more on this.
