Planet Python
Last update: August 19, 2026 07:48 PM UTC
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 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. As an example, the PSF pushed for exactly that in astral-sh/setup-uv, which is now live and will shave off usage as people upgrade.
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 PyPI should be considered a supply chain risk until the PSF is able to hire more engineering staff. 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.
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.
md5-240e0a066fbd28891db9e76043276e9f
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:
md5-4f979c536cfd56d696221cec539cf01c
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:
md5-4546488f0cbcfe00d7f3a1769cb9a0b9
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:
md5-56b84a927fc32234b9d0f638c01c98c7
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:
md5-a56c9c32b9ebeff41b4204e7dd201499
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.
md5-530571d20754d44d4ee009954881e467
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.
Python Software Foundation
Announcing the Packaging Council Election Candidates for 2026!
What an exciting list! Please take a look at who is running for the inaugural Python Packaging Council (PPC) on the Nominees page. This election has 17 nominees for 5 open seats on the council.
This inaugural election fills all five seats on the PPC. The two candidates receiving the highest number of votes shall be designated Cohort A with a two year term, and the three candidates receiving the next highest number of votes shall be designated Cohort B with a one year term.
In future elections, each cohort will be elected for a full two-year term in alternating years, so that roughly half of the PPC turns over each cycle.
- 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!
If you wish to vote in this election, you must affirm your intention to vote no later than Tuesday, August 25th, 2:00 pm UTC, to participate in this election.
Every PSF Voting Member (Supporting, Contributing, and Fellow) needs to affirm their membership to vote in this election. 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 pc-elections@python.org.
If you are a voting member of the PSF that affirmed your intention to participate in this election, you will receive an email from "OpaVote Voting Link <noreply@opavote.com>" with your ballot -- the subject line will read "Python Packaging Council 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, please get in touch by emailing pc-elections@python.org so we can look into your account and make sure we have the most up-to-date email for you.
PyCharm
Hybrid and Local AI course at DeepLearning.AI
Python Software Foundation
Announcing the PSF Board Candidates for 2026!
What an exciting list! Please take a look at the 18 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.
Wingware
Wing Python IDE Version 12.0.2 - August 13, 2026
Wing Python IDE version 12.0.2 has been released. This release adds a native ARM64 Windows version of Wing, improves remote development on Windows, streamlines Claude Code setup, and improves performance and responsiveness, particularly when working with very large projects and on Windows. It also reduces the size of the analysis cache database by about 20% and fixes a number of bugs. See the change log for details.
Wing 12 integrates the Claude Code AI coding agent directly into the IDE, with a new Claude Code tool, a Tasks tool for planning and reviewing AI agent work, and a set of MCP servers that give the agent access to Wing's source code analysis, unit testing, debugger, and code review features. See the list of Wing 12 features below for details.

Downloads
Wing 12 -- the full Python IDE, available as Wing Pro (for agentic development) or Wing Classic (for manual development) depending on your license, with a free 30-day trial of Wing Pro.
Wing 101 v. 12 -- a simplified free Python IDE for teaching beginning programmers.
Wing 11 and earlier versions are not affected by installation of Wing 12 and may be installed and used independently. However, project files for Wing 11 and earlier are converted when opened by Wing 12 and should be saved under a new name, since Wing 12 projects cannot be opened by older versions of Wing.
New in Wing 12
AI Coding Agent Integration with Claude Code
Wing 12 adds a Claude Code tool that integrates the Claude Code AI coding agent with the IDE. Set Up for Claude Code in the Project menu configures the active project for AI agent development.
A set of MCP (Model Context Protocol) servers gives Claude Code access to Wing's source code analysis, testing, and debugger functionality, so the agent can more efficiently navigate and understand your code, write, run, and fix unit tests, and use the debugger to diagnose difficult runtime errors. In our benchmarks, giving Claude Code access to Wing's MCP servers made agent-driven coding tasks both faster and cheaper.
Tasks Tool
The new Tasks tool lets you plan, queue, execute, review, and audit the history of AI agent development tasks, making it easier to supervise and inspect the agent's work before committing it to revision control.
FIX Features and Write Tests
Wing 12 adds AI agent driven FIX features that hand the current debugger bug, failing unit tests, or code warnings to Claude Code for resolution. New Write Tests items in the Testing and editor context menus prompt the agent to write unit tests for selected code.
Code Actions
Wing 12 also adds AI Code Actions, accessed from the FIX icon in the editor toolbar, that operate on selected code or the enclosing scope. Built-in actions include explaining code, reviewing it for quality or security risks, fixing code warnings, optimizing for performance, and updating comments and docstrings. The action list is user-extensible, so you can add your own prompts for tasks you run often.
Pseudo-Terminal for OS Commands and Debug I/O
The OS Commands and Debug I/O tools now default to using a pseudo-terminal that implements full ANSI terminal emulation, so you can run and debug programs that use color output, cursor positioning, or full-screen TUIs.
Redesigned OS Commands Capability
The OS Commands tool has been replaced with configurable OS Commands in the Tools menu. Each OS Command acts like its own tool, for use in any tool or editor split.
Tools in Editor Splits and Reorganized Tools Menu
Tools can now also be added or dragged to editor splits, allowing for much more flexible workspace layout. The Tools menu has been reorganized into related groups, with less-used and legacy tools in an Other sub-menu, so more commonly used tools are easier to find.
Test Discovery and Preferences Search
Wing 12 adds automatic test file discovery and discovery of individual unit tests within files, so you usually don't need to specify test file patterns or add test files individually. The Preferences dialog now supports text search and back/forward navigation.
Other Minor Features and Improvements
Wing 12 also includes many other improvements, including:
- IDE build for ARM64 Windows
- Improved performance and responsiveness
- Improved remote agent installation and remote development
- Significantly faster source code analysis
- Prompts for SSH passphrases and HTTPS credentials when needed during VCS operations
- Faster detection of externally modified files, with reduced CPU load
- Saving and restoring of tool console scrollback across project close and reopen
- Clickable OSC 8 hyperlinks in the OS Commands and Debug I/O tools
- A preference to select the ssh or plink.exe SSH implementation
- A notice on the next startup when Wing's previous session ended in an unexpected crash
Wing 12 also makes a number of other bug fixes and usability improvements.
Product Line Changes
Wing 12 simplifies the product line. The Commercial / Non-Commercial use distinction has been replaced by two feature-based product tiers:
- Wing Pro -- the full-featured Python IDE including AI agent development tools
- Wing Classic -- the complete traditional Python IDE for hands-on development, with no AI agent features
Anyone may purchase either tier for any purpose. Existing Commercial and Non-Commercial Use licenses both become Wing Pro. Customers who don't need the AI agent features may move to Wing Classic at renewal time, or any time sooner by contacting support@wingware.com.
Wing Personal has been discontinued. Existing Wing Personal users may continue to use Personal 11.x indefinitely, switch to free Wing 101, or purchase a Wing Classic license. See Pricing for details.
Changes and Incompatibilities
The single-LLM-query AI features originally introduced in Wing 11 (the AI Coder and AI Chat tools) are considered legacy in Wing 12 and hidden from the user interface by default. They remain available in projects that already use them and can be re-enabled with Project Properties > AI in Project Properties or in the Projects > AI preferences.
See Wing's Claude Code Agent Integration for Wing 12's AI agent approach.
If you have questions, please don't hesitate to contact us at support@wingware.com.
Trey Hunner
Reorganizing Python's sys module
The sys module is the primary junk drawer of the Python standard library.
A good junk drawer holds miscellaneous items that don’t have another sensible home.
I often think of utils modules as “junk drawer” modules.
I believe that both utils modules and junk drawers have their purpose, but junk drawers can get out of hand.
As I recently noted in my talk about pathlib, I see both the sys module and the os module as junk drawer modules.
They serve a similar purpose to utils modules, but they have a different name.
In this post, I wonder: what would Python’s sys be like if it was designed today, from scratch?
The overview: 9 new sys submodules
As of Python 3.15, the sys module has 115-121 attributes (depending on whether you’re in the REPL and whether an exception has occurred), and 63 of those are functions.
All of those names need permanent homes.
How can we reorganize a utils-style module that’s grown quite large?
Give it submodules!
No, really… this isn’t the worst solution.
Django’s utils package isn’t so bad.
So we could split sys into these 9 submodules:
sys.cli: for command-line argument handling and program controlsys.imports: related to imports and modulessys.io: handles standard I/O streamssys.repl: handles REPL display controlsys.interpreter: system information and installation detailssys.memory: memory management tools used for profiling, optimization, and debuggingsys.exceptions: exception handlingsys.profile: profiling and introspectionsys.runtime: interpreter runtime behavior
You may notice some similarities to other modules in the Python standard library. That’s a hint that it might be worth moving some of these utilities into other parts of the standard library. But moving functionality between top-level modules is a much bigger change, so let’s set that idea aside.
For now, let’s take a closer look at our tentatively re-organized sys package.
The sys package and its submodules
A couple of these 9 submodules have only a handful of attributes, a couple have over 20, and the rest fall somewhere in the middle.
sys.cli
This submodule would handle command-line arguments and program control:
argv: Command-line arguments listorig_argv: Original unmodified argumentsexit(): Function to terminate Pythonflags: Named tuple of interpreter flags_xoptions: Dictionary of-Xcommand options
sys.imports
Everything related to imports and modules:
path: Module search path listmodules: Dictionary of loaded modulesbuiltin_module_names: Tuple of built-in modulesstdlib_module_names: Frozen set of standard library modulespath_hooks: List of path-to-finder callablespath_importer_cache: Finder object cachemeta_path: Meta path finder objectspycache_prefix: Bytecode cache directoryset_lazy_imports(),get_lazy_imports(),set_lazy_imports_filter(),get_lazy_imports_filter(),lazy_modules: Lazy import controls (Python 3.15+)
sys.io
The standard I/O streams:
stdin: Standard input streamstdout: Standard output streamstderr: Standard error stream__stdin__,__stdout__,__stderr__: The original values of those three streams (useful for restoring them after replacing them)
sys.repl
Hooks and settings for Python’s interactive prompt:
displayhook(): Called to show the result of each REPL expression__displayhook__: The original value ofdisplayhookps1: The primary prompt string (>>>)ps2: The continuation prompt string (...)__interactivehook__: Called when an interactive session starts up_baserepl(): Starts the basic fallback REPL
sys.interpreter
Information about the Python build, the Python installation, and the operating system:
platform: Platform identifier string (linux,darwin,win32, etc.)version: Python version stringversion_info: Python version as a named tupleimplementation: Python implementation details (CPython, PyPy, etc.)executable: Path to the Python interpreterprefix,exec_prefix: Installation prefixesbase_prefix,base_exec_prefix: Installation prefixes, ignoring virtual environmentsplatlibdir: Platform-specific library directory namemaxunicode: Maximum Unicode code point (1114111)maxsize: Maximum size of containersbyteorder: Native byte order ('little'or'big')hexversion: Version encoded as single integerapi_version: C API versioncopyright: Python copyright noticeabiflags: ABI flags from PEP 3149abi_info: ABI details namespace (Python 3.15+)float_info: Floating point implementation detailsint_info: Integer implementation detailshash_info: Hash algorithm parametersfloat_repr_style: floatreprstyle ('short'or'legacy')winver,dllhandle,getwindowsversion(): Windows-specific detailsgetandroidapilevel(): Android-specific detail_base_executable,_framework,_git,_home,_stdlib_dir: Assorted build and installation details
sys.memory
Memory management tools used for profiling, optimization, and debugging:
getsizeof(): Size of an object in bytesgetrefcount(): Number of references to an objectgetallocatedblocks(): Number of allocated memory blocksgetunicodeinternedsize(): Number of interned stringsintern(): Intern a string_is_interned(): Check whether a string is interned_is_immortal(): Check whether an object is immortal_debugmallocstats(): Print memory allocator statistics_clear_type_cache(),_clear_internal_caches(): Clear interpreter caches
sys.exceptions
Tools for accessing and handling exceptions:
exc_info(): Currently handled exception, as a 3-tupleexception(): Currently handled exception (Python 3.11+)last_exc,last_type,last_value,last_traceback: The most recent unhandled exception, mostly for REPL useexcepthook(): Called to display unhandled exceptions__excepthook__: The original value ofexcepthookunraisablehook(): Called for exceptions that can’t be raised__unraisablehook__: The original value ofunraisablehooktracebacklimit: Maximum number of traceback levels to display
sys.profile
Profiling, tracing, auditing, and other runtime introspection:
setprofile(),getprofile(): Profiling hooks_setprofileallthreads(): Set profile function for all threads (Python 3.12+)settrace(),gettrace(): Tracing hooks_settraceallthreads(): Set trace function for all threads (Python 3.12+)call_tracing(): Call a function with tracing enabledmonitoring: Low-overhead monitoring events namespace (Python 3.12+)_getframe(),_getframemodulename(): Frame inspection_current_frames(),_current_exceptions(): Frames and exceptions across all threadsactivate_stack_trampoline(),deactivate_stack_trampoline(),is_stack_trampoline_active(): Support for the perf profiler (Python 3.12+)audit(): Raise an auditing eventaddaudithook(): Register an audit hookremote_exec(),is_remote_debug_enabled(): Remote debugging support (Python 3.14+)
sys.runtime
Settings that control interpreter runtime behavior:
setrecursionlimit(),getrecursionlimit(): Recursion depth controlsetswitchinterval(),getswitchinterval(): Thread switching controlis_finalizing(): Whether the interpreter is shutting downbreakpointhook(): Called by the built-inbreakpointfunction__breakpointhook__: The original value ofbreakpointhookdont_write_bytecode: Suppress.pycfile generationwarnoptions: Warning filter settingsgetfilesystemencoding(),getfilesystemencodeerrors(): Filesystem encoding detailsgetdefaultencoding(): Default string encoding (alwaysutf-8)get_int_max_str_digits(),set_int_max_str_digits(): Limit on int-to-string conversion (Python 3.11+)setdlopenflags(),getdlopenflags(): Dynamic loading controlthread_info: Thread implementation details_is_gil_enabled(): Whether the GIL is enabled (Python 3.13+)_jit: JIT compiler introspection namespace_dump_tracelets(): Dump the JIT’s internal tracelets_get_cpu_count_config(): Configured CPU count overrideget_asyncgen_hooks(),set_asyncgen_hooks(): Async generator lifecycle hooksget_coroutine_origin_tracking_depth(),set_coroutine_origin_tracking_depth(): Coroutine debugging depth_enablelegacywindowsfsencoding(): Windows mbcs encoding compatibility
Could this actually be done?
When I first pondered this experiment last year, this was a very hypothetical thought experiment that I assumed could never be done. I still mostly feel the same way.
There are a few big problems with such a refactoring:
- What would the transition period look like for the huge amount of code that currently uses existing
sysfeatures? - Would backwards compatibility be maintained forever? If so, would this cause more confusion than it’s worth?
- How would code that monkey patches attributes like
sys.stdoutwork?
I thought the third question was the biggest roadblock, but I now think it’s the first 2 questions.
Those first 2 questions are big questions and I haven’t thoroughly thought through the upsides and downsides of such a refactoring.
That third question is a technical one, but I think I can answer it… but the answer is messy.
The magic of module-level __setattr__
When Python users want to capture all output from their program to a file, they reassign sys.stdout to an in-memory file-like object.
That’s how the contextlib.redirect_stdout helper works, and many testing tools use the same technique.
This monkey patching of sys.stdout is somewhat common, which poses a bit of a problem for us.
Imagine that stdout actually lived in a sys.io submodule.
If sys.stdout and sys.io.stdout were two separate attributes, code that assigned to one would be invisible to code that read from the other.
We need a way to synchronize reads and writes of the old flat sys namespace to forward them to the newly nested namespace within the right submodule.
Python has supported customizing module-level attribute reads since Python 3.7, thanks to module-level __getattr__ functions (PEP 562).
But Python doesn’t support module-level __setattr__ functions (that idea was proposed in PEP 726 and rejected).
Although… every Python module is an instance of ModuleType, and Python allows changing the class of a module object.
If we swap in a ModuleType subclass, we can define whatever __getattr__ and __setattr__ behavior we’d like.
This trick is demonstrated in a proof-of-concept newsys package.
The newsys package reimplements sys as a package with 9 submodules.
As a proof of concept, this module simply proxies to the sys module.
All reads and writes of newsys.io.stdout or newsys.stdout proxy to the original sys.stdout (since that’s what everything else still uses under the hood in the existing Python interpreter).
What would the transition look like?
If this transition was ever actually done, I imagine it might look something like this:
- Add the submodules, with every old flat name still working via forwarding
- Update the documentation to nudge folks toward the new names
- Soft deprecate the flat names someday (or maybe never)
- (Likely never) hard deprecate the flat names
There’s a tiny bit of precedent for sys submodules: sys.monitoring (added in Python 3.12) is an actual module that lives under sys.
But since sys isn’t a package, import sys.monitoring doesn’t work, as noted at the top of the sys.monitoring documentation page.
But, I doubt this will ever be done. Python isn’t known for reorganizing modules just to clean things up (outside of the big Python 2/3 split).
Removing names like sys.path and sys.argv would break a huge amount of code… and I can imagine Python tutorials and long-time Python users dragging their feet on re-learning “the new way”.
After all… why re-learn something when the old version already works and isn’t going anywhere?
If this was ever done, the flat names might need to keep working forever, and permanent aliases might cause more confusion than such a reorganization is worth.
A thought experiment, not a proposal
I’m not seriously proposing that we actually reorganize sys… at least not seriously enough to draft a PEP.
But I do think there’s a practical takeaway here for our own code.
When a utils module grows out of hand, submodules can help.
And if other code relies on the old flat names, a module-level __getattr__ function (or that hacky __class__ trick) can keep the old names working while you reorganize.
I doubt sys will ever change, but I had fun imagining a version of Python where it did.
Python Insider
Announcing the Packaging Council Election Candidates for 2026!
Announcing the 17 nominees for the inaugural Python Packaging Council election, and how to vote in the election.
August 12, 2026
Django Weblog
DSF Office Hours
The DSF Board hosts open office hours every Wednesday at 6:00 PM UTC (check your local time). Anyone in the Django community is welcome to drop in. You do not need an agenda or an invitation. Video call details are on the DSF Office Hours page.
We have been running these since October 2024, and right now we have two things we would especially like to talk with you about.
Who shows up
On any given Wednesday, you might find DSF Board members, Steering Council members, Django Fellows, working group members, and community members who are curious about joining a working group. There is no membership requirement. Anybody from the community can join, and often does.
The Executive Director search
We recently published a call for applicants for a Django Executive Director. If you are considering applying, or you are still deciding whether it is the right fit, come to office hours and ask us anything: what the job actually looks like, what we expect in the first year, how the search works. We would rather answer your questions directly than have you guess from a job posting.
If this is the first you are hearing about the search, please help us spread the word. The best candidate may be someone who has not thought to look.
Fundraising to support it
Hiring an Executive Director is why we raised our 2026 fundraising goal from $300,000 to $500,000, which needs about $16,000 per month in additional recurring support. We have made real progress and are working to close the rest.
If your company uses Django, you can help through corporate sponsorship, a direct donation, or GitHub Sponsors. Most of these are a small lift for a company that already depends on Django. If you want fundraising materials to bring to your leadership team, or help picking the option that fits, come to office hours, and we will get you what you need.
Everything else
Office hours cover plenty beyond that: what our working groups are up to and how to join one, projects the Foundation is working on, and whatever you have been wondering about how the DSF operates. In the weeks before a board meeting, we use the time to gather feedback on what we are about to discuss. If you want the board to hear something, this is a direct line.
One thing office hours are not: a general Django support channel. It is not the place to debug your code or market a product. For coding help, the Django Forum will get you faster answers.
Office hours are the most direct way to keep up with the Foundation, but they are not the only one. We wrote up all the other places we post and where the conversation happens if Wednesdays do not work for you.
Otherwise, put a Wednesday on your calendar and say hello.
PyCharm
Whatâs New in PyCharm 2026.2.1
This PyCharm release is a big one for anyone building with AI. Your agents can now roll up their sleeves inside your Jupyter notebooks â working against a live kernel instead of firing off disconnected scripts. And they finally know which Python to use, so packages land in the right environment every time.
We’re also welcoming marimo notebooks into the IDE and introducing changes to bundled plugins to keep PyCharm fast and focused.
Release highlights
Jupyter notebook skill for AI agents
Let AI agents such as Claude Code and Codex create, edit, and run .ipynb notebooks via PyCharm’s notebook model and a live kernel, so variables, models, and data persist across cells instead of disappearing when the agent shells out. For you, this means more reliable notebook and ML work â with fewer tokens used. To start, just open the AI chat and ask the agent to work in your notebook.
Agent environment coordinator
Tired of AI agents installing packages into the wrong Python environment? This new skill gives the agent your project’s configured interpreter and tool â uv, Poetry, pip in a venv, or conda â so commands target the right environment, not a system one. If none exists, it can set one up via PyCharm, and the agent decides how to use the information. To start, ask the agent to run or install something in your project.
marimo notebooks in PyCharm [third-party plugin]
You can now open, edit, and run marimo notebooks directly in PyCharm with the new plugin developed by the marimo team.
Work with reactive cells and interactive UI elements in a dedicated notebook without leaving your IDE. Because marimo notebooks are stored as Python files, they are Git-friendly, executable as scripts, and easy to integrate into your existing Python projects.
Changes to bundled plugins in 2026.2
As part of ongoing maintenance, we are unbundling and deprecating low-usage plugins, including Data Wrangler, Hugging Face, and Google Colab support. You can continue to install compatible versions from JetBrains Marketplace, but these plugins are no longer bundled or actively maintained by the PyCharm team. A more focused set of bundled plugins means a leaner codebase, helping us keep PyCharm fast and responsive and invest our effort where it has the most impact.
Redesigned Python Packages tool window
- Packages now appear in a collapsible tree alongside their dependencies so you can see what’s installed and why â with new icons, right-aligned versions, and inline Install/Update links.
- A new floating search popup (think âSearch Everywhereâ but for packages) makes finding and installing fast. It also shows you which environment will be used and lets you pick a module and dependency group.
- You can install a package into specific
uv/Poetry dependency groups like dev or test per workspace member, change versions inline or through the new Change Version dialog, and install from VCS via Custom Installation. - Package repositories can be enabled or disabled, with state remembered across sessions. In addition, unreachable URLs show a clear error, and Remote Development support is substantially improved.
Clearer type checking
Get clearer, more actionable type messages:
- Richer type-mismatch errors, now with a breakdown of why the types don’t match.
- A type diff for callables and other composite types, so both sides read the same way.
- Fully rendered names and types in inspection tooltips, with clickable links.
Bug fixes
- Virtual environments for end-of-life Python: PyCharm no longer creates venvs for Python 2.7, 3.6, and 3.7. You can still create a new env from a command line and add it to the IDE manually.
- SQLAlchemy 2.0 Session.get() inference:
Session.get(Entity, id)(and SQLModel) is now inferred as a model instance rather than the class.
Download PyCharm
All of these updates are available in PyCharm 2026.2.1. Update right from the IDE or the Toolbox App, or download the latest version to try everything out on your own projects. As always, weâd love to hear your feedback.
We Stopped AI Agents From Installing Into the Wrong Python: Task Success Rates Jumped to 95%+
AI agents are supposed to save you time. Ask one to install a dependency or run your project, though, and it often does the opposite: It installs into the wrong Python, ignores the uv or virtual environment your project uses, and hands back a broken setup for you to fix yourself.
PyCharmâs new Agent Environment Coordinator skill fixes this, and this blog post shows just how helpful it proves to be.
AGENT ENVIRONMENT COORDINATOR The agent stopped guessing Python. Average task success 68% -> 98% Baseline With skill 28 Python tasks 6 AI models No system Python pollutionWe tested six AI models using 28 different Python programming tasks. Without access to the project’s real environment, they solved 68% of the tasks on average. After we gave them access, their average success rate shot up to 98% â and they didnât even modify the system Python.
If youâre currently using AI agents in your Python projects, read on to see how the Agent Environment Coordinator can improve their performance.
When the agent could see the project’s environment, it stopped failing
When using the Agent Environment Coordinator skill, each agent, regardless of the model, was able to complete far more of the 28 tasks. (See the Methodology section below for details on what the tasks entailed.) Here is the share of successfully completed tasks for each model, comparing the baseline to running with the skill in PyCharm:
MODEL BASELINE WITH SKILL Claude Sonnet 4.6 36% 96% Claude Sonnet 5 73% 100% Claude Opus 4.8 67% 100% Claude Opus 5.0 94% 98% Codex / GPT-5.5 62% 95% Codex / GPT-5.6 80% 100%Every model improved, with the weakest baseline improving the most.
Why we built this
LLMs almost never use a project’s dedicated virtual environment. They fall back to a system interpreter, ignoring the fact that there may be several system interpreters and real projects often have more complex, multi-interpreter setups already configured in PyCharm that the agent has no way to see.
For example, pip install httpx runs against the wrong Python, the package installs globally, the script fails, and the environment is polluted.
PyCharm already knows which interpreter belongs to your project and which tool manages it. The agent just couldnât ask â so we gave it a way.
How it works
The Agent Environment Coordinator lets the agent ask PyCharm two things. get_python_environment returns the correct interpreter for the file or module in question â the path plus the tool behind it (uv, Poetry, pip + venv, conda). If no environment exists yet, configure_python_interpreter sets one up by reusing PyCharm’s existing configuration mechanism â the same one that offers to create a .venv â so the new interpreter also becomes visible in the IDE.
The important part is what the skill doesn’t do. It returns information; it never intercepts or rewrites the command. The agent asks which Python to use, gets an accurate answer, and decides whether and how to use it to write the command itself. We hand it the missing context using existing mechanisms in PyCharm â we don’t let it take the wheel.
The payoff is practical: The agent works with your project setup out of the box. You donât need to coach it through prompts about which environment to use, or clean up wrong installs afterward.
Methodology
We built a dataset of 28 tasks covering everyday Python-environment work, like running tests, installing a library, listing dependencies, resolving a version conflict, and so forth.
Each task ultimately required the agent to pick the correct interpreter to execute a command. The eval also reduced the reward when the agent polluted the system environment, so a high score reflects a clean run, not just a passing one.
We ran the full set three times per model, with and without the skill, using Harbor, and averaged the results.
Results
Success rates climbed across the board â Sonnet 5 improved from 73% to 100%, Opus 5 from 94% to 100%, and Codex/GPT-5.6 from 80% to 100%.
Two things stand out in addition to this numerical jump:
- The improved success rates demonstrate that the models lacked context, rather than being incapable of completing the tasks. The models didn’t get better â they just stopped guessing the interpreter, which is why the weakest baseline improved the most.
- Because the eval docks points for polluting the system environment, these higher scores also imply cleaner runs. The agents didn’t just pass more often; they stopped leaving a mess behind.
Want to try it?
Open the AI chat in PyCharm 2026.2.1 and ask your agent to install a package or run something in your project â it’ll reach for the right interpreter on its own.
The Agent Environment Coordinator is one of PyCharm’s bundled skills. You can browse and manage all of them right in the IDE, expand the built-in library with external registries like public GitHub repositories, or import skills you’ve already set up for Claude Code or Codex.
We Gave AI Agents a Live Jupyter Kernel in PyCharm
If you’ve handed notebook work to an AI agent, you know how it tends to go: More often than not, it corrupts your .ipynb, loses your trained model the moment the run finishes, or burns budget sitting idle through a long job while you watch.
To solve this, weâre introducing a brand-new Jupyter skill. Built directly into PyCharm, it lets your AI agent work inside a live Jupyter kernel instead of handing the job to a subprocess and losing your progress. This one change means state persists across cells, the .ipynb isn’t corrupted, and long jobs wait until execution is completed instead of constantly checking and wasting precious tokens.
For Opus, the kernel ran cheaper than the shell
We tested the efficiency of the Jupyter skill by comparing the performance of agents when solving twelve different machine learning problems. We compared three different modes: strictly using bash, strictly using the kernel via the Jupyter skill, and a mixture of both.
While the agent was able to solve all twelve tasks in every mode, there was a difference in how much each mode spent. For Claude Opus 5, working through the kernel cost 59.09 USD versus 67.06 USD through the shell â about 12% cheaper.
MODE COST INPUT TOKENS CACHE READS Kernel (skill) Shell (baseline) USD 59.09 USD 67.06 72.7M 36.3M 98% 82%Hereâs the counterintuitive part: The kernel used more tokens, yet cost less. That’s because it keeps the prompt cache warm. 98% of its input was cache reads, versus 82% for the shell â and cache reads incur only 1/12 of the cost of creating a fresh cache.
Why we built this
Notebooks are where coding agents tend to fall apart. Most AI tools treat an .ipynb like a plain text file: They hand-edit the JSON (and corrupt it), and then run code by running a subprocess. The moment an agent starts the subprocess, the kernel state â the trained model, the loaded dataframe, and every import â lives in the child process, and vanishes when that process exits. The agent can’t inspect it, checkpoint it, or reuse it. Output is buffered until the run ends, so progress is invisible, and long training jobs get babysat â blind until the connection times out.
We asked the obvious question: What if the agent operated a live Jupyter kernel through the IDE?
So we built our new Jupyter skill, which exposes PyCharm’s own notebook intelligence â its notebook model and live-kernel control â to the agent. It does this through a single MCP wrapper, execute_tool, which covers the core notebook operations, including creating, editing, and reading notebooks; running cells; waiting on long runs; probing a running kernel; and controlling its lifecycle. The skill tells the agent when and how to use them.
How it works
The agent:
- Runs directly in the kernel. The agent writes real Python into a cell and runs it, so variables, models, and data persist across cells â exactly like a human working in a notebook.
- Waits instead of polling. Rather than polling on a fixed timer and re-billing context on every idle call,
wait_cell_executionis blocked until the cell finishes (or a safe cap), and then hands control back. This helps reduce idle round-trips. - Reads only what’s new. As a long run streams output, the agent reads the delta â just the lines since its last check â instead of re-sending the whole, ever-growing cell output every time.
Methodology
We used twelve tasks from the MLGym machine-learning benchmark â classification, regression, and reinforcement-learning problems, each of which requires the agent to load data, train, evaluate, and save a result. We ran them across Claude Opus 5 and OpenAI’s GPT-5.6 models, Sol and Terra, through Codex. We compared three modes: through the kernel only, through the kernel plus the shell, and through the shell alone. As these benchmark tasks expose test labels to the agent, we treat cost â not accuracy â as the reliable signal.
One caveat, for transparency: An audit found that one of the twelve tasks, Titanic, was contaminated â the agent could peek at the test set, and each agent used this to select the best model to present as the final solution. Titanic is a well-known, easy task for LLMs, and the issue appeared consistently across all three modes, so it doesn’t skew the comparison. The pattern holds even with Titanic removed â the kernel still ran 10% cheaper than the shell for Opus (56.34 USD versus 62.65 USD).
Results
The cost win is model- and task-dependent. It was clearest for Claude Opus on long, stateful jobs, while the shell came out cheaper on short tasks and for the Codex models â which already use the cache efficiently, so there the skill earns its place on workflow, not cost.
Where it still falls short
Two things are worth keeping in mind:
- Tell the agent to save its artifacts. In one run, the agent trained a solid model but never saved the submission file before finishing. This is easy to prevent from your side: Just add a clear instruction in your context file (e.g.
CLAUDE.md) or a skill so the agent saves any model the moment it clears your target metric. - Some tasks are still beyond agents. On a hard task, the agent’s approach simply wasn’t strong enough to clear the bar. That’s genuine ML difficulty, not a tooling gap â some complex problems still need a human in the loop.
The skill removes the mechanical waste, but doesn’t turn a weak approach into a strong one.
Want to try it?
Open the AI chat in PyCharm 2026.2.1 and ask your agent to work in a notebook â create one, load a dataset, or kick off a training run. The agent will operate the kernel directly instead of running commands in the shell.
You can also browse and manage skills directly from the IDE, expand the built-in library with external registries like public GitHub repositories, or let PyCharm import skills youâve already set up for Claude Code or Codex.
