03 Aug 2026
Planet Python
Tryton News: Tryton News August 2026
This month the community focused on the trytond core. The XML data import grows a new way to find existing records, the database schema shrinks oversized character columns automatically, and the test suite for function fields and button methods gets more coverage. On the user-facing side, the accounting menus are tidied up, and the DPD shipping carrier moves to the latest API. All of this builds on our last LTS release 8.0.
For an in depth overview of the Tryton issues please take a look at our issue tracker or see the issues and merge requests filtered by label.
Changes for the User
Accounting, Invoicing and Payments
When a user un-reconciles a line that was created by the receivable rule, Tryton now raises a warning, similar to the warning it already shows for payables. This avoids accidentally leaving too much money in the target account.
The accounting menus are simplified. The "open journal" entry now opens the journal and period form directly instead of going through a wizard. The "close" prefix is dropped from the processing menu-entries, the reconcile wizard becomes the first one, and the redundant "account" prefix is removed from the moves menu entry.
Stock, Production and Shipments
The DPD shipping carrier integration now uses Shipment Service 4.5, the latest version of the DPD API.
New Documentation
The documentation for creating account moves from a template now links the accounting menu-entry in the client, so the workflow is easier to find.
New Releases
We released bug fixes for the currently maintained long term support series 8.0, 7.8, and 7.0.
Changes for Implementers and Developers
When all rows of a char field fit in a smaller column, the database schema is now shrunk automatically. This keeps the database compact without any manual intervention.
The XML data import now supports a search attribute on the <record> tag. When the record with the given id does not exist, Tryton uses this domain to find an existing record before creating a new one. This makes it possible to import data using natural unique keys like the language code.
The test suite for button methods now runs each method as a subtest, so a failure in one method does not prevent the remaining button methods from being tested.
The test suite for column-based function fields is now added to the tests of fields methods.
Initial draft powered by Minimax-M3. Curated and finalized by human hands.
1 post - 1 participant
03 Aug 2026 9:33am GMT
Django community aggregator: Community blog posts
Running Headscale on my own infra (and finally killing my WireGuard setup)
Hello everyone π
This one started in the dumbest possible way: I wanted to check on my 3D printer from outside my house.
I have a Bambu printer running in LAN-only mode, and I use OctoApp to control it from my phone. Works great at home. Useless the moment I leave. The usual answer is OctoEverywhere, which is a lovely project, but it means my printer traffic goes through someone else's servers, and I already run enough infrastructure that this felt silly.
So I went looking for a way to just⦠have my home network with me. And I ended up rebuilding my entire remote access setup in an afternoon.
What I had before
My old setup was, in hindsight, a bit of a Rube Goldberg machine:
- A Hetzner VPS running WireGuard
- A node at home connected to that WireGuard tunnel
- Nginx Proxy Manager on that node
- A Pi-hole for the WireGuard side, resolving things to
10.0.0.xaddresses - Another Pi-hole for my actual home network, resolving to
192.168.0.x
Two Pi-holes. Two address ranges. Two mental models of "where am I right now". It worked, but every time I added a service I had to think about which side of the tunnel it lived on.
What I actually wanted was much simpler: when I'm away, I want my phone to behave exactly like it's sitting on my home WiFi. Same IPs, same DNS, no difference at all.
That's a mesh VPN, and the nicest one is Tailscale.
I don't trust free
Tailscale is excellent. I want to say that first, because what follows is going to sound like criticism and it isn't. The client is great, the free tier is generous, and for most people it's the right answer.
But every time I look at a free tier this good, I catch myself doing the same mental arithmetic: someone is paying for this, and it isn't me. Tailscale is a venture-funded company. Free tiers built on top of venture funding have a well-documented lifecycle, and the last chapter is rarely "and it stayed free and generous forever". The limits shrink, or a device cap appears that you're already over, or the company gets acquired by someone with different ideas.
I'm not predicting that Tailscale does any of this. I have no reason to think they will. But I built my remote access on a WireGuard tunnel I control, and moving to something where a company I don't control holds the keys to my entire home network felt like a downgrade, even if the software is better.
I've also been burned recently enough that I'm not in a trusting mood. I wrote a whole angry post a few months ago about paying $100 a month for a service and getting less than 24 hours of notice before it changed underneath me. That was a paid tier. If that's what happens when I'm a customer, I'm not going to build my house on a free one.
So: I like the idea, I don't trust the arrangement. Which is exactly the situation self-hosting exists for.
Enter Headscale.
What Headscale actually is
Headscale is an open source implementation of the Tailscale control server. Your devices still run the official Tailscale client, they just point at your server instead of Tailscale's.
The important thing to understand is what the control server does and doesn't do. It handles key exchange, device registration, ACLs, and DNS settings. It does not sit in the middle of your traffic. Once two devices know about each other, they talk directly. So Headscale being a small container on a cheap VPS is completely fine.
I put it on the same Hetzner box that was already running WireGuard, because it already has a public IP and I was going to be deleting the WireGuard side anyway.
The Docker Compose setup
Headscale is CLI-first, which is fine, but I got tired of typing docker exec headscale headscale ... within about four minutes. So I also added Headplane, a web UI for it.
Directory structure first:
mkdir -p ~/headscale/config/{headscale,headplane}
cd ~/headscale
wget -O config/headscale/config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml
wget -O config/headplane/config.yaml https://raw.githubusercontent.com/tale/headplane/main/config.example.yaml
And the compose file:
services:
headplane:
image: ghcr.io/tale/headplane:latest
container_name: headplane
restart: unless-stopped
ports:
- "3003:3000"
volumes:
- ./config/headplane/config.yaml:/etc/headplane/config.yaml
- ./config/headplane/lib:/var/lib/headplane
# Shared path to the Headscale config. This has to match
# `headscale.config_path` in the Headplane config.
- ./config/headscale/config.yaml:/etc/headscale/config.yaml
- /var/run/docker.sock:/var/run/docker.sock:ro
headscale:
image: headscale/headscale:latest
container_name: headscale
restart: unless-stopped
command: serve
labels:
# Absolutely necessary for Headplane to find Headscale.
me.tale.headplane.target: headscale
ports:
- "8083:8080"
volumes:
# Same host path in both containers. This matters.
- ./config/headscale/config.yaml:/etc/headscale/config.yaml
- ./config/headscale/lib:/var/lib/headscale
Nginx handles TLS and public exposure, same as every other service on that box. If you're not putting a firewall in front of these, bind the ports to 127.0.0.1 instead ("127.0.0.1:8083:8080"), otherwise Docker happily opens them to the internet and walks around UFW while it's at it.
Note the me.tale.headplane.target label on the Headscale container. That's how Headplane finds it, and it's how Headplane restarts Headscale when you change DNS settings from the UI. Note also that both containers mount the Headscale config from the same host path. Headplane reads it directly, so if the paths drift you get a UI that shows you stale settings.
Headscale config
The parts of config/headscale/config.yaml that matter:
server_url: https://headscale.example.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
database:
type: sqlite3
sqlite:
path: /var/lib/headscale/db.sqlite
dns:
magic_dns: true
base_domain: ts.example.com
override_local_dns: true
nameservers:
global:
- 1.1.1.1
Leave nameservers.global pointing at a public resolver for now. We'll swap it for the Pi-hole later, once the Pi-hole has joined the network and has an address to point at.
base_domain has to be different from your server_url domain, otherwise Headscale refuses to start.
Headplane config
In config/headplane/config.yaml:
server:
host: "0.0.0.0"
port: 3000
base_url: "https://headplane.example.com"
cookie_secret: "<32 char random string>"
cookie_secure: true
data_path: "/var/lib/headplane"
headscale:
url: "http://headscale:8080"
config_path: "/etc/headscale/config.yaml"
api_key: ""
Generate the cookie secret with openssl rand -hex 32.
The url is the internal Docker address: container name, container port. Not whatever port you mapped on the host. The two containers talk over the compose network.
Then start Headscale on its own, create a user and an API key:
docker compose up -d headscale
docker exec headscale headscale users create myuser
docker exec headscale headscale apikeys create --expiration 90d
Paste that key into api_key and bring up Headplane. The key is only shown once, so if you lose it, just make another one. apikeys list only shows the prefix.
Nginx
Two vhosts, nothing exotic. But the Headscale one needs WebSocket passthrough, and this is the first place I lost time (more on that below):
location / {
proxy_pass http://127.0.0.1:8083;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_redirect off;
proxy_read_timeout 5m;
}
Headplane is a boring proxy block, no special headers needed. Certbot for certs, as usual.
Pi-hole as the tailnet DNS
This is my favourite part of the whole setup, and the reason I bothered.
Join your home Pi-hole to the network like any other device:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://headscale.example.com
tailscale ip -4
Take that 100.x.x.x address, put it in Headscale's dns.nameservers.global, and restart the container.
Now every device on the tailnet uses my home Pi-hole for all DNS. Which means:
sonarr.example.casaand every other internal domain resolves from anywhere, because Pi-hole's Local DNS Records answer for them- My phone gets Pi-hole ad blocking on cellular data, not just at home
Neither of those is new, I had both with the old WireGuard setup. The difference is that I now get them from one Pi-hole instead of two, and I didn't have to think about it. The Pi-hole joined the tailnet, I pointed Headscale at it, and every device I add from now on inherits both for free.
override_local_dns: true is what forces this. Without it, your phone keeps using whatever DNS the carrier hands it, the tunnel works fine, and your internal domains silently fail to resolve.
The subnet router
A mesh VPN only connects the devices that are on it. My printer can't run a Tailscale client. Neither can most of the stuff I care about.
The fix is a subnet router: one device on your LAN that advertises the whole network to everyone else. I used the Pi-hole box, since it was already joined:
sudo tailscale up --login-server https://headscale.example.com \
--advertise-routes=192.168.0.0/24
Two things have to happen after this or nothing works, and I hit both.
First, IP forwarding:
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
Second, and this is the one that got me: routes are double opt-in. The node advertises, and then the control server has to approve. Advertising alone does nothing.
docker exec headscale headscale nodes list-routes
docker exec headscale headscale nodes approve-routes --identifier 1 --routes 192.168.0.0/24
The moment I ran that second command, everything worked. Printer, internal domains, random machines on my LAN, all reachable from my phone on cellular.
Clients
You don't need a special app. The official Tailscale clients all support custom control servers.
On Android, install from Play Store or F-Droid, go to "Accounts", tap the kebab menu, and select "Use an alternate server". Enter your Headscale URL and log in.
On iOS it's slightly more buried: install from the App Store, tap the account icon, "Log in", then use the options menu to pick "Use custom coordination server".
macOS is the one with a real trap. Use the standalone build, not the Mac App Store one, because the App Store version is sandboxed and fights you on custom control servers. brew install tailscale works, or grab the standalone package from Tailscale's download page. Then:
sudo tailscale up --login-server https://headscale.example.com
tailscale set --accept-routes
That --accept-routes is easy to forget. Without it the client joins fine and then can't see anything on your LAN.
The gotchas that cost me the most time
Four things ate most of my afternoon. All of them had error messages that pointed somewhere other than the actual problem.
Headscale listening on 127.0.0.1 inside its own container
Headplane kept logging this:
Error while validating API key: [object Object]
I regenerated that API key three times. The key was fine. The real problem was one line above in the Headscale logs:
INF listening and serving HTTP on: 127.0.0.1:8080
127.0.0.1 inside a container means that container only. Not the compose network, not the other container sitting right next to it. Headplane literally could not open a connection, so it couldn't validate anything, and reported that as an auth failure.
Set listen_addr: 0.0.0.0:8080 in the Headscale config. Your host port mapping is a separate concern and keeps working the same.
Nginx eating the WebSocket upgrade
Clients wouldn't connect, and Headscale said:
WRN no upgrade header in TS2021 request. If headscale is behind a reverse proxy,
make sure it is configured to pass WebSockets through.
At least this error tells you exactly what's wrong. My nginx config was a copy-paste of the same reverse proxy block I use for every other service on that box, which has no Upgrade handling because nothing else needs it.
The commonly recommended fix uses a map block in the http context:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
The idea is that plain HTTP requests to the same vhost get Connection: close instead of a bogus Connection: upgrade. If you put that map somewhere nginx doesn't load it into the http context, you get unknown "connection_upgrade" variable and nothing starts.
I'll be honest: I got that error, commented the Connection line out entirely to keep moving, and it worked. It's on my list to go back and do properly, because "it worked when I removed the correctness" is not a state I enjoy leaving things in.
The routes CLI moved
Every guide and blog post out there tells you to run:
headscale routes list
headscale routes enable -r 1
On 0.29 that gives you unknown command "routes". It's now under nodes:
headscale nodes list-routes
headscale nodes approve-routes --identifier 1 --routes 192.168.0.0/24
Related: --user wants a numeric ID now, not a username. headscale users list gives you the number.
When in doubt, headscale --help is more current than anything you'll find in a search result. I lost a few minutes to blog posts written against older versions before I just asked the binary.
Pi-hole ignoring the tailnet
Tunnel up, subnet routes approved, ping 1.1.1.1 working, and google.com resolving to nothing.
Pi-hole's default interface listening behaviour doesn't answer queries arriving on the tailscale0 interface. Settings, then DNS, then set it to listen on all interfaces and permit all origins. Or scope it to the tailnet CIDR if you want to be tidier about it than I was.
The bash alias
Small thing, big quality of life improvement:
alias headscale='docker exec headscale headscale'
Now every command in every tutorial you find online works verbatim, without mentally prefixing the docker part every time. Just remember it's there if you ever install the real binary on the same box, or you'll spend an entertaining twenty minutes wondering why the local install ignores your config file.
What I still need to do
I'm not going to pretend this is finished.
The old WireGuard tunnel and its dedicated Pi-hole are still running. Headscale does everything they did, but I'm leaving them up until I've gone through the Nginx Proxy Manager config and confirmed nothing still points at a 10.0.0.x address. Deleting infrastructure at 2 AM after a successful afternoon is how you learn what depended on it.
Was it worth it?
Yes, and for reasons beyond the printer.
The setup collapsed two Pi-holes and two IP ranges into one mental model. My phone now behaves identically at home and on cellular. Every internal service I add from now on works remotely for free, without port forwarding or a new proxy host or any thought about which side of a tunnel it lives on.
And no ports are open to my LAN. The only publicly reachable thing is the Headscale control server, which hands out keys and doesn't carry traffic.
The total cost was one afternoon, most of which was spent on four error messages that pointed at the wrong thing. Hopefully this post saves you those.
If you want to check the printer thing specifically: Bambu printers in LAN-only mode expose MQTT on 8883 and FTPS on 990. There's no web UI, so don't waste time typing the IP into a browser like I did. Once you're on the tailnet, OctoApp just takes the same local IP and access code you use at home.
See you in the next one!
03 Aug 2026 5:00am GMT
02 Aug 2026
Planet Python
Mike C. Fletcher: OMI Audio extension for glTF
Another OMI-based extension, this time for positional audio mixing. Again, Claude-coded, using the OMI/KHR audio extensions for glTF as the base model and then implementing the actual mixing using Numpy. Supports mp3, wav, opus and flac inputs and stereo (headset) outputs. Up on github as omi_audio and on PyPI as omi_audio as well.
This is loosely the same model as Web Audio's Panner Node. The only significant difference from the KHR extension is that we also support VRML97 style double-ellipse emitter. The package delegates file retrieval to the application, and doesn't include any special effects or the like.
02 Aug 2026 4:06pm GMT
Talk Python to Me: #557: Security of everything at PyCon 2026
Security has always been the vegetables of software. Everyone agrees it matters, and somehow it never quite makes it onto the plate. At PyCon US this year, that changed. For the first time ever, security got its own dedicated, day-long track, one of just two at the whole conference, sitting right next to AI. And the room was packed to the back wall. <br/> <br/> On this episode, I'm joined by the three people at the center of it. Seth Larson, Security Developer in Residence at the Python Software Foundation and, very recently, a CPython core developer. Juanita Gomez, a PhD researcher at UC Santa Cruz in open source security, who co-chaired the track. And Mike Fiedler, PyPI's Safety and Security Engineer, one of the very few people paid full-time to keep the packages you install safe. <br/> <br/> We use the arc of the track's talks to take the temperature of Python security right now: supply chain attacks, dependency cooldowns, zero trust, SBOMs, and the push to bring Rust into CPython. And why not one of us thinks security is anywhere close to solved. Turns out that's the good news. It's why the room was full.<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/training'>Talk Python Courses</a><br/> <br/> <h2 class="links-heading mb-4">Links from the show</h2> <div><strong>Guests</strong><br/> <strong>Juanita Gomez</strong>: <a href="https://www.linkedin.com/in/juanitagomezr/?featured_on=talkpython" target="_blank" >linkedin.com</a><br/> <strong>Mike Fiedler</strong>: <a href="https://miketheman.dev?featured_on=talkpython" target="_blank" >miketheman.dev</a><br/> <strong>Seth Michael Larson</strong>: <a href="https://sethmlarson.dev?featured_on=talkpython" target="_blank" >sethmlarson.dev</a><br/> <br/> <strong>Trailblazing Python Security</strong>: <a href="https://us.pycon.org/2026/tracks/security/?featured_on=talkpython" target="_blank" >us.pycon.org</a><br/> <strong>Everything Security at PyCon US 2026 (PSF blog)</strong>: <a href="https://pyfound.blogspot.com/2026/06/everything-security-at-pycon-us-2026.html?featured_on=talkpython" target="_blank" >pyfound.blogspot.com</a><br/> <strong>Dependency Cooldowns</strong>: <a href="https://cooldowns.dev/?featured_on=talkpython" target="_blank" >cooldowns.dev</a><br/> <strong>Anatomy of a Phishing Campaign (Mike Fiedler) Recording</strong>: <a href="https://www.youtube.com/watch?v=uXW1qeUS6Yw" target="_blank" >www.youtube.com</a><br/> <strong>FedRAMP</strong>: <a href="https://www.gsa.gov/technology/government-it-initiatives/fedramp?featured_on=talkpython" target="_blank" >www.gsa.gov</a><br/> <strong>Zero Trust in 200ms: Implementing Identity-Per-Transaction with Python & Serverless-Tristan McKinnon</strong>: <a href="https://www.youtube.com/watch?v=WOOlzsJx9cg" target="_blank" >www.youtube.com</a><br/> <strong>Rust for CPython project</strong>: <a href="https://blog.python.org/2026/04/rust-for-cpython-2026-04/?featured_on=talkpython" target="_blank" >blog.python.org</a><br/> <strong>pre-PEP</strong>: <a href="https://discuss.python.org/t/pre-pep-rust-for-cpython/104906?featured_on=talkpython" target="_blank" >discuss.python.org</a><br/> <strong>Rust for CPython: Making Python Safer and More Robust for Everyone - Emma Smith</strong>: <a href="https://www.youtube.com/watch?v=42kibVnUHYE" target="_blank" >www.youtube.com</a><br/> <strong>SBOMit</strong>: <a href="https://github.com/in-toto/sbomit?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>Asleep at the Wheel: Getting your SBOMs to pay attention... - Sanchit Sahay, Abhishek Reddypalle</strong>: <a href="https://www.youtube.com/watch?v=VYY3HnRtV6U" target="_blank" >www.youtube.com</a><br/> <strong>Volatility</strong>: <a href="https://volatilityfoundation.org/?featured_on=talkpython" target="_blank" >volatilityfoundation.org</a><br/> <strong>Post Incident Runtime SBOM Generation from Python Memory - Hala Ali</strong>: <a href="https://www.youtube.com/watch?v=mgY7zuDYrag" target="_blank" >www.youtube.com</a><br/> <strong>zizmor</strong>: <a href="https://docs.zizmor.sh/?featured_on=talkpython" target="_blank" >docs.zizmor.sh</a><br/> <strong>GitHub Actions security in Python packages (Andrew Nesbitt write-up)</strong>: <a href="https://nesbitt.io/2026/05/25/github-actions-security-in-python-packages.html?featured_on=talkpython" target="_blank" >nesbitt.io</a><br/> <strong>andrew/pycon: data & analysis for the GitHub Actions security talk</strong>: <a href="https://github.com/andrew/pycon?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>GitHub Actions Security in Python Packages - Andrew Nesbitt</strong>: <a href="https://www.youtube.com/watch?v=vn3YIqdseWI" target="_blank" >www.youtube.com</a><br/> <strong>gh-profiler: examine a GitHub user's profile to gauge their contributions</strong>: <a href="https://github.com/ehmatthes/gh-profiler?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>PyCon US YouTube channel</strong>: <a href="https://www.youtube.com/@PyConUS" target="_blank" >www.youtube.com</a><br/> <strong>SBOMit: adding verification to SBOMs (OpenSSF)</strong>: <a href="https://openssf.org/blog/2023/12/13/introducing-sbomit-adding-verification-to-sboms/?featured_on=talkpython" target="_blank" >openssf.org</a><br/> <strong>Ecosystems</strong>: <a href="https://ecosyste.ms?featured_on=talkpython" target="_blank" >ecosyste.ms</a><br/> <br/> <strong>Watch this episode on YouTube</strong>: <a href="https://www.youtube.com/watch?v=ktEssFryRjY" target="_blank" >youtube.com</a><br/> <strong>Episode #557 deep-dive</strong>: <a href="https://talkpython.fm/episodes/show/557/security-of-everything-at-pycon-2026#takeaways-anchor" target="_blank" >talkpython.fm/557</a><br/> <strong>Episode transcripts</strong>: <a href="https://talkpython.fm/episodes/transcript/557/security-of-everything-at-pycon-2026" 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>
02 Aug 2026 3:37pm GMT
31 Jul 2026
Django community aggregator: Community blog posts
Chicago Like a Local: Things to Do During DjangoCon US 2026 (Part 1)
DjangoCon US 2026 is coming back to Chicago from August 23-28, and we at Caktus couldn't be more thrilled, not only to be returning as a Silver Sponsor, but also because I (Keanya) have the privilege of chairing the conference once again!
31 Jul 2026 11:35pm GMT
Issue 348: One month until DjangoCon US
News
See You in Chicago in One Month!
In just one month, developers, maintainers, educators, and Django enthusiasts from around the world will gather in Chicago for DjangoCon US 2026.
DSF member of the month - Katherine "Kati" Michel
Kati is a longtime member of the Django community who has been a speaker, organizer of DjangoCon US, and member of DEFNA's board of directors. She shares some insightful thoughts on the current state of Django and where it can go from here, especially around AI.
Next Stop Pescara!
Looking forward to this year's Django on the Med ποΈ? Perhaps you should come? And what to do if you can't!
Updates to Django
Today, "Updates to Django" is presented by Raffaella from Djangonaut Space! π
Last week we had 13 pull requests merged into Django by 10 different contributors - including 2 first-time contributors! Congratulations to Mundur and Philip Narteh for having their first commits merged into Django - welcome on board!
News in Django 6.2:
- In the asynchronous request path, error responses (such as those rendered by
handler404andhandler500) are now rendered on the request's thread-sensitive thread, rather than on a shared thread pool, so that database connections used during error handling are managed byclose_old_connections(). - The admin
view_on_siteURL now consistently returns an HTTP 403 response when a staff user lacks view or change permission for the target model. - The admin history view now checks permissions before object existence, consistently returning an HTTP 403 response for staff users without the view or change permission regardless of whether the object exists.
Django Fellow Reports
Django Fellow Report - Natalia
Focus this week was on the djangoproject.com email incident π§π§± and security work π. I worked with Ops to diagnose and resolve the outage, addressing the source of abuse in the account registration flow. In parallel, I continued security reviews (including GeoDjango) and handled incoming reports ποΈ. Overall brain ended up quite fried π§ π³π©.
Django Fellow Report - Sarah
Triaged a couple of interesting tickets, including one around paginator solutions. Reviewed four more and authored three. Also weighed in on the move to Playwright for integration testing and worked on six security reports.
And her baby boy now has his first two teeth.
Django Fellow Report - Jacob
Like last week, I focused almost all of my attention on improving our patches for the same two pending security reports. One reporter thanked us for our professional handling and brought a smile to my face when adding, "as I am sure you are getting completely slop cannoned all the way down." π«
More steady progress reviewing the GSoC work on expressions in SQL FROM clauses, too.
DjangoCon US
LAST DAY! 10% off + guaranteed t-shirt +swag ends July 31st)
Quick heads-up: today is the last day to save on DjangoCon US 2026 and the last day we can guarantee your t-shirt and tote bag for in-person registrations.
Articles
Setting Django's DEBUG safely
DEBUG=True does far more than turn on pretty error pages: it exposes stack traces and configuration to attackers, disables error reporting, degrades query performance, and serves unminified static files. A grep of Django's own source shows the setting changing behavior across more than two dozen files, which is the real argument for never letting production depend on it.
Nifty Django Feature: setUpTestData
setUpTestData() runs once per test class where setUp() runs again for every test method, so moving shared fixtures into it cuts repeated create and destroy work off your suite. A short Pet and Species example makes the difference concrete: the species row is built once for the whole class, while anything created in setUp is rebuilt for each test.
Django: release code words up to 6.1
Every Django release announcement hides a "code word" introducing the new features, and this post tables them all from 1.7 through 6.1, each linked to its Wiktionary definition and scored for rarity using the wordfreq package. Carlton's mezcla (3.2) wins on obscurity by not appearing in the English frequency data at all, while Sarah's composite (5.2) was a wink at that release's composite primary keys.
Python: spy on function calls with unittest.mock's wraps
Despite the name, unittest.mock builds spies as well as mocks: pass wraps and calls fall through to the real function and return real results, while the mock still records them for assertions. A caching example checks that compute_owl_sound() runs only once across two owl_sound() calls, and pairs it with autospec=True, which is optional when spying on functions but required for methods.
Python: inspect interleaved unittest.mock calls with attached mocks
To assert the order of calls across several mocks, attach them to a parent so its mock_calls records everything on one interleaved timeline. Methods attach themselves when you build the parent with create_autospec(Kettle, instance=True), while separate module-level functions need an explicit attach_mock() before you can check the whole sequence in a single assertion.
Podcasts
Talk Python #556: Updates on Django's Async Story - Carlton Gibson
Carlton answers all your questions on Django, async, what to be excited about in Django 6.1, and more. Well worth the full listen.
Sponsored
It's hard to ask to meet a stranger
So I'll go first. I'm Tim Schilling. I enjoy talking to people about software, Django and career development. I recently pivoted to specifically focus on helping others in their careers. If you could use some support, or just want to talk with me, set up a call with me! 
Videos
No More Spreadsheets! Building PyLadiesCon Infrastructure with Python and Django - Mariatta
From PyCon US, Mariatta outlines how PyLadies said no to spreadsheets and instead started building out its own conference infrastructure using Python and Django.
Django Job Board
Two new listings lead the board this week: a one-year Security Developer role at the Python Software Foundation covering CPython vulnerabilities and PyPI malware, plus senior full-stack work at Hive Collective, with Django roles at MyOme and Fusionbox still open.
Security Developer at Python Software Foundation π
Senior Full Stack Engineer at Hive Collective π
Senior Backend Engineer at MyOme
Python + TypeScript Engineers at Fusionbox
Projects
Spwig/commerce
Spwig is a full-stack, self-hosted e-commerce platform on Django 5, now open source under AGPL-3.0. Server-rendered storefront, admin, REST API, POS, and page builder in a single modular monolith.
31 Jul 2026 3:00pm GMT
23 Jun 2026
Planet Twisted
Glyph Lefkowitz: Adversarial Communication
As I have discussed in previous posts, "AIs" can make mistakes. In fact, they do make mistakes, and their mistake-making patterns are such that where and how they will make mistakes is both uncertain and constantly changing.
Thus, in any scenario where you want to attempt to make "productive" use of "AI", you must have a system in place for checking every result. Not checking some results; checking every result. If each result might have a consequence for you (and if it didn't have a consequence, why bother automating it?) and you cannot predict in advance which kinds of results will need verification, then verification is always required.
The verification often ends up being just as expensive as doing the work in the first place, which means that if you want your usage of "AI" to be personally profitable, you have to find someone else to externalize the cost of verification onto. This person becomes your adversary, and, if you are successful, your "AI's" victim.
The Ladder-Climber And Their Reverse-Centaur Rungs
One way that this constellation of facts can straightforwardly assemble themselves into a dystopian nightmare is the phenomenon, described by Cory Doctorow, of the reverse centaur. This is when your employer non-consensually turns you into the verification system. The "AI" does the fun part of initially performing the work, and then you do the boring part where you check if the robot is right and clean up its messes, even if everyone already knows that it would, in aggregate, be cheaper for you to do the work in the first place.
Reverse centaurs can be made from any automation, not only "AI" automation. I think that there is a reason that this term happens to have emerged in the "age of AI", though, and not with earlier automation technologies (even those which were considerably more viscerally horrific). That reason is: the wrongness of "AI" output is not merely a technical feature that must be compensated for, it is a generalized externality.
As I mentioned above, if you are responsible for the entirety of the work, both extruding the "AI" output and checking it, it's usually cheaper to have humans do the entirety of the work to begin with. When humans do the writing directly, we can check as we go, and thus verification doesn't need to be as comprehensive.
When "AI" coding advocates say "code review is the bottleneck", what they are observing is that the LLM is still rolling the dice for each PR, and a human is still necessary to verify that each of those rolls is a winner. But calling this process "code review" is a bit of a misnomer; it's not really "code review" in the traditional sense, it's human understanding.
Before the advent of "AI", the human understanding was implicit in the process of writing the code in the first place1, and the code review was a way of diffusing and extending that understanding. Now that the code can be authored with no initial understanding taking place, that cost has not gone away, it has moved.
Human understanding was always the bottleneck.
However, this is taking a collaborative view of a software project, where satisfying the needs and solving the problems of your customers are the goals. We can see that "AI" is a bad tool to satisfy those goals, because all it's doing is converting the first half of the work, that of understanding the code as you write it, to understanding the agent's output as you read it.
What if, instead, we were to take the view that every software company is a Hobbesian nightmare, red in tooth and claw? In this view, the only goal of a software project is for the individual developers to make their promo cycles and get their bonuses. Given that there is only a certain amount of money to go around, this is a zero-sum game where each programmer wants to look more productive than their colleagues.
Pretty much every organization finds it easy to reward "productivity" as expressed by lines of code emitted, but the benefits of doing thorough and thoughtful design, analysis, and code review very difficult to reward. In this world, an LLM is an invaluable tool for the sociopathic ladder-climber, particularly if your legacy organization is still structuring their workflows as if the person prompting the bot is "writing" the code, and then they get to foist off the act of "reviewing" the code onto someone else.
Here, the prompter effectively externalizes the cost of the LLM's failures but internalizes any benefits. The prompter will vibe-code a big feature, so large that the assigned reviewer can't possibly comprehend it all effectively. When this happens, the reviewer will, eventually, be pressured to approve it, even if they can try to spot a few problems along the way. The reviewer has their own work to get back to, after all, the obligation to review the prompter's (read: the bot's) code is a drain on their time that they are not going to get rewarded for.
If this feature is a big success, the prompter gets a promotion. If it causes a big issue, well, the reviewer must not have been careful enough.
This is why LLMs are "good for coding", and also why their biggest promoters keep having outages.
The Generative Gish Galloper
Coding is the biggest "success story" of this type of adversarial communication, but it is by far not the only instance of such a thing. LLMs create a new form of leverage that can turn Brandolini's law from a linear advantage into an exponential one. If you are engaged in a political debate where you want to overwhelm the other side in nonsense, an LLM can generate bullshit faster than it is physically possible for a human being to type, let alone respond thoughtfully. There is an asymmetry to the utility of this weapon as well: only one side of the political spectrum wants to flood the zone and destroy trust in institutions and the concept of truth. There's a good reason that the fascists love it.
Straightforward Spam and Fraud
This is kind of obvious, but LLMs can generate lightly-customized, plausible-looking text much more quickly than any human being. This facilitates their use in fraud, spam, and scams. In a spamming or fraudulent interaction, once again, the costs are externalized onto the victim: the recipient of a spam message has to do all the work of "checking" the LLM's output. Spammers already expect very low hit rates from boilerplate, and if the LLM can increase those percentages from 1% to 5% the technology will pay for itself; they don't need anything like reliable accuracy.
Customer "Support"
If you have any kind of commercial relationship with a company, I probably don't even need to mention this: customer "support" bots are a misery. Everybody knows it at this point. But customer support is usually conceptualized by businesses as an adversarial interaction, because it is a cost center. They maintain internal metrics on time-to-resolution and try to optimize them. Implicitly, this creates a dynamic where the goal of the customer service agent's job is not to solve your problem, but to emit noise that will cause you to think your problem is resolved, or to give up, as fast as possible. Unsurprisingly, LLMs can emit this noise faster than humans can, getting those customers off the phone. But those customers will remember those interactions, and the story outside the TTR metrics is horrible.
Similarly to the situation in software development, LLMs can look very good on paper for customer support, but mostly what they are doing is illuminating the problems with the industry's existing metrics, by turning "winning the metrics battle against the customer" into a more obvious and immediate defeat for the company's long term reputation.
"Education"
In 2026 it is sadly a fact of life that students cheat all the time using "AI", and that this cheating is very successful, in that the teachers find it very hard to detect.
LLMs are great for cheating on schoolwork because the student is externalizing the work of the checking onto the teachers, who are often starting at a disadvantage to begin with, at least in the US.
My view is that this is happening because of a divergence in the way that students vs. teachers (or, more accurately, "the broader educational system") view grading.
When a student is asked to write an essay, the teachers see the effort as both intrinsically worthwhile for the student, as well as useful as a pedagogical tool to evaluate and react to the student's progress. The student, by contrast, sees a stumbling block designed to knock them off the path to success and into a permanent underclass. It is no wonder that the student sees "AI" as useful to their own goals and has no compunction about deploying it.
There is a bitter irony that the ability to understand the inherent value of actually writing the essay on their own is the sort of thing that students can really only learn by writing a bunch of essays. There's no way that I can think of which makes the benefit legible as long as a shortcut is available.
The net effect here is a downward spiral, where the already-wobbling educational system is sustaining an attack that it doesn't have the resources to recover from. The individual students' attacks against their teachers and their schools' grading systems might appear to momentarily succeed, but they will win the battle and lose the war.
Spamming "For Good"?
Usually when we talk about someone unilaterally choosing to enter into an adversarial relationship, that's an "attack" and for good reasons we have a negative impression of the attacker. However, I would be remiss if I did not point out that there are some cases where the relationship was already adversarial; just because you're the attacker doesn't mean that you are evil.
For example we might imagine use-cases like automatically filing appeals for prior authorizations against health insurance. It's relatively well-known at this point that the main way for-profit insurers maintain their margins is by denying claims right up to the line of the policies themselves being fraud, so using a spamming tool to fight them might be entirely justifiable2 in that case.
Similarly, using an LLM could be justified in a fight against a company refusing to honor a warranty. One could imagine using an LLM to immediately generate replies and escalations.
However, even in imagined cases like these, the underlying problem is that the insurers and the vendors already have a tremendous amount of structural power, so it is more likely that they will have the advantage in deploying a communications weapon like an LLM, as well as enacting policies to simply ignore any LLM-based communication that you might submit. Worse, if these strategies were to become widespread, they might provide an excuse to reject any communications by feeding them into an unreliable "LLM detector" and issuing an automated "computer says no" even to hand-written correspondence.
It is also worth stressing that these cases are imagined, as compared to the very real coworker-abuse, spam, scam, fraud, and disinformation campaigns being waged in real life today.
Therefore, while legitimate uses might exist, it's hard to imagine that there's anywhere they would be genuinely valuable and sustainable. In the best case "AI" will provide a temporary advantage for underdogs that will provoke an arms race which the resource-advantaged adversaries will win in the long run, in the worst case the arms race itself will cement permanent structural change that will make things worse.
"Search" By Stealing
Most of the adversarial utility of "AI" is on the "write" side, since write-amplification is more obviously aggressive than reading. But the "read" side of LLMs - summarization and question-answering - can be a form of attack as well.
To begin with, the act of reading itself is currently enormously destructive, but that's arguably not a fundamental aspect of this technology. They could set reasonable rate-limits and respect things like robots.txt, as search engines have for decades now. They could also refrain from committing criminal levels of copyright infringement. But, today, using "AI" tools does suborn this sort of out-of-control crawling.
More insidiously, consider the scenario described in this YouTube video. The LTT Bros decided to try Linux again, and in the course of so doing, they had problems. When trying to solve these problems, they were faced with a choice: they could consult Reddit, or they could ask an LLM. Asking an LLM would "gaslight the heck out of" them, but they still found it preferable, because they would at least get an answer without getting yelled at.
Initially this sounds great. But it also means that you want to extract knowledge from a community, while mechanically eliding any values or norms that the community may want to impart as part of offering that knowledge. As someone who spent many years in a community tech support role, this is worrying. Many requests for support are people asking how to do things that will momentarily solve a superficial problem but create a long-term reliability problem or even an immediate security risk, that the question-asker doesn't want to hear about. Consider the question "I'm tired of entering my password so much, how do I make it so my laptop unlocks automatically". An obsequious chatbot will helpfully tell you how to do this without pushback.
But, this is also a sort of ethically murky area. The Linux community is somewhat famously, for many years now, a toxic cesspool of general hostility, misogyny, etc. It is certainly a good thing that people can get access to this knowledge without subjecting themselves to abuse. But it also means that the people with the power and the privilege to change the community for the better can just quietly withdraw, rather than fixing the problems. It also means that the positive elements of culture cannot be transmitted, and people will have no opportunity to learn about unknown unknowns.
In this case, the "adversarial" communication is with society. The thing that using an LLM for search lets you do is withdraw from society and avoid forming any personal connections. There are some personal connections which are painful and annoying, and so that can feel like a momentary balm. But the need to make connections in general is, like, the concept of society itself.
Who Am I Hurting?
LLMs are good at adversarial communication. They are so good at it, relative to their other benefits, that they will tend to make communications adversarial if you are not remaining vigilant about the possibility that it might do so. My request to you, dear reader, if you are going to use such tools, is to always ask yourself, "who might I be hurting, if I use an LLM for this?"
If you're using an "AI", who is its adversary? If you haven't given it one yet, who might the "AI" turn into an adversary? Who might you overwhelm with an asymmetric amount of output, or, if you're receiving information and not sending it, who are you taking that information from without consulting?
Figure out the answers to these questions and conduct yourself accordingly; the answer might be "yourself".
Acknowledgments
Thank you to my patrons who are supporting my writing on this blog. If you like what you've read here and you'd like to read more of it, or you'd like to support my various open-source endeavors, you can support my work as a sponsor!
-
One of the reasons that software developers tend to prefer greenfield development is that when you are given a blank page, you can project your own specific understanding onto it. You can structure the codebase in a way that works for your brain, down to the variable naming conventions and the module layouts. LLM-assisted development makes everything into instant brownfield work, which makes developers instantly miserable; even those who are excited about the technology will frequently complain about how it feels like their agency has been stolen and their joy in the work has been diminished. But I digress. β©
-
Modulo the massive amount of other externalities involved in using LLMs, of course, but I don't have the time or energy to get into those here. β©
23 Jun 2026 8:06pm GMT
09 Jun 2026
Planet Twisted
Hynek Schlawack: How to Ditch Codecov for Python Projects
Codecov's unreliability breaking CI on my open source projects has been a constant source of frustration for me for years. I have found a way to enforce coverage over a whole GitHub Actions build matrix that doesn't rely on third-party services.
09 Jun 2026 12:00am GMT
22 May 2026
Planet Twisted
Glyph Lefkowitz: Opaque Types in Python
Let's say you're writing a Python library.
In this library, you have some collection of state that represents "options" or "configuration" for a bunch of operations. Such a set of options is a bundle of potentially ever-increasing complexity. Thus, you will want it to have an extremely minimal compatibility surface, with a very carefully chosen public interface, that is either small, or perhaps nothing at all. Such an object conveys state and might have some private behavior, but all you want consumers to be able to do is build it in very constrained, specific ways, and then pass it along as a parameter to your own APIs.
By way of example, imagine that you're wrapping a library that handles shipping physical packages.
There are a zillion ways to do it ship a package. There are different carriers who can ship it for you. There's air freight, and ground freight, and sea freight. There's overnight shipping. There's the option to require a signature. There's package tracking and certified mail. Suffice it to say, lots of stuff.
If you are starting out to implement such a library, you might need an object called something like ShippingOptions that encapsulates some of this. At the core of your library you might have a function like this:
1 2 3 4 5 |
|
If you are starting out implementing such a library, you know that you're going to get the initial implementation of ShippingOptions wrong; or, at the very least, if not "wrong", then "incomplete". You should not want to commit to an expansive public API with a ton of different attributes until you really understand the problem domain pretty well.
Yet, ShippingOptions is absolutely vital to the rest of your library. You'll need to construct it and pass it to various methods like estimateShippingCost and shipPackage. So you're not going to want a ton of complexity and churn as you evolve it to be more complex.
Worse yet, this object has to hold a ton of state. It's got attributes, maybe even quite complex internal attributes that relate to different shipping services.
Right now, today, you need to add something so you can have "no rush", "standard" and "expedited" options. You can't just put off implementing that indefinitely until you can come up with the perfect shape. What to do?
The tool you want here is the opaque data type design pattern. C is lousy with such things (FILE, pthread_*_t, fd_set, etc). A typedef in a header file can easily achieve this.
But in Python, if you expose a dataclass - or any class, really - even if you keep all your fields private, the constructor is still, inherently, public. You can make it raise an exception or something, but your type checker still won't help your users; it'll still look like it's a normal class.
Luckily, Python typing provides a tool for this: typing.NewType.
Let's review our requirements:
- We need a type that our client code can use in its type annotations; it needs to be public.
- They need to be able to consruct it somehow, even if they shouldn't be able to see its attributes or its internal constructor arguments.
- To express high-level things (like "ship fast") that should stay supported as we add more nuanced and complex configurations in the future (like "ship with the fastest possible option provided by the lowest-cost carrier that supports signature verification").
In order to solve these problems respectively, we will use:
- a public
NewType, which gives us our public name... - which wraps a private class with entirely private attributes, to give us an actual data structure, while not exposing the constructor,
- a set of public constructor functions, which returns our
NewType.
When we put that all together, it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
As a snapshot in time, this is not all that interesting; we could have just exposed _RealShipOpts as a public class and saved ourselves some time. The fact that this exposes a constructor that takes a string is not a big deal for the present moment. For an initial quick and dirty implementation, we can just do checks like if options._speed == "fast" in our shipping and estimation code.
However, the main thing we are doing here is preserving our flexibility to evolve the related APIs into the future, so let's see how we might do that. For example, let's allow the shipping options to contain a concrete and specific carrier and freight method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
As a NewType, our public ShippingOptions type doesn't have a constructor. Since _RealShipOpts is private, and all its attributes are private, we can completely remove the old versions.
Anything within our shipping library can still access the private variables on ShippingOptions; as a NewType, it's the same type as its base at runtime, so it presents minimal1 overhead.
Clients outside our shipping library can still call all of our public constructors: shipFast, shipNormal, and shipSlow all still work with the same (as far as calling code knows) signature and behavior.
If you need to build and convey some state within your public API, while avoiding breakages associated with compatibility churn, hopefully this technique can help you do that!
Acknowledgments
Thanks for reading, and thank you to my patrons who are supporting my writing on this blog. If you like what you've read here and you'd like to read more of it, or you'd like to support my various open-source endeavors, you can support my work as a sponsor.
-
The overhead is minimal, but it is not completely zero. The suggested idiom for converting to a
NewTypeis to call it like a function, as I've done in these examples, but if you are wanting to use this pattern inside of a hot loop, you can use# type: ignore[return-value]comments to avoid that small cost. β©
22 May 2026 12:33am GMT

