Planet Python
Last update: August 13, 2026 04:49 PM UTC
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.
Unbundling and Deprecating Low-Usage Plugins in PyCharm
As part of ongoing maintenance, we are unbundling and deprecating low-usage plugins starting with PyCharm 2026.2. This includes support for Data Wrangler, Hugging Face, and Google Colab, among others.
A more focused set of bundled plugins means a leaner codebase, enabling us to keep PyCharm fast and responsive and invest our effort where it has the most impact.
You can continue installing compatible versions from the JetBrains Marketplace, but these plugins will no longer be bundled or actively maintained by the PyCharm team. Read this blog post for the full list, deprecation timeline, and next steps.
Why we’re making this change
The tools and workflows developers rely on keep evolving, and several of these plugins never reached the level of adoption we hoped for. After reviewing usage trends, we’ve decided to move a set of low-usage plugins out of active development, so our team can focus on features with broader impact for Python developers.
A smaller set of bundled plugins also means a leaner, more maintainable codebase. As PyCharm continues to grow, we want to invest our engineering effort where it has the most impact and keep the IDE fast and responsive over time.
Unbundling and deprecating a plugin doesn’t necessarily mean deleting it. If a certain plugin’s functionality is still used, we’ll move that plugin’s code to a separate Obsolete Plugins repository. The plugin will remain searchable and installable on JetBrains Marketplace with a fixed compatibility range, but will no longer be rebuilt with every new release or maintained by the PyCharm team.
Which plugins are affected
The following plugins are being deprecated; those currently bundled will be unbundled first:
- Data Wrangler
- Hugging Face
- Google Colab (Jupyter Notebook Colab)
- Spark, including PySpark support
- AI Playground
- AI Agents Debugger
- dbt
- Databricks
Other low-usage plugins may be deprecated in the same way in future releases.
Timeline and what to expect
v2026.2
- The bundled plugins listed above will be unbundled from PyCharm.
- The PyCharm team will no longer develop new features for these plugins or maintain them.
- Compatible versions will remain available for installation from JetBrains Marketplace and stay compatible with v2026.2.
- The plugin source will be published in the Obsolete Plugins repository, where you can continue to build and install it manually.
v2026.3 and beyond
- The PyCharm team will no longer publish compatible versions of these plugins starting from v2026.3.
What this means for you
If you rely on any of these plugins, you can continue to install a compatible version from JetBrains Marketplace for PyCharm 2026.2. Because the source moves to the Obsolete Plugins repository under an open model, the community can keep building and installing the plugins manually. If you’re interested in maintaining one of them, we’d love to hear from you.
Thank you
We’re grateful to everyone who used these plugins, filed issues, and shared feedback over the years. Thank you!
The PyCharm team
Python GUIs
Adding QTabWidget to a Layout Alongside Other Widgets in PyQt6 — How to combine QTabWidget with other layouts without it taking over your entire window
I'm trying to create a layout in PyQt and implement different layouts into one QHBoxLayout. Everything works fine, but when I add a QTabWidget alongside other layouts, it becomes the only visible widget — as if everything else disappears. Isn't it possible to have a QTabWidget in a layout beside other layouts?
Good news: you absolutely can place a QTabWidget inside a layout alongside other widgets and sub-layouts. You don't need to wrap it in a QGroupBox or use any special workaround. The issue usually comes down to how the tabs themselves are set up — specifically, whether the tab pages have any content and layout of their own.
Let's walk through what's happening and how to fix it.
Why the QTabWidget seems to "take over"
When you create a QTabWidget and add empty QWidget pages to it, those pages have no layout and no content. Depending on how the widget calculates its size, this can cause unexpected sizing behavior in the parent layout. The tab widget may request more space than you expect, or the other widgets may collapse because the layout gives the tab widget priority.
The fix is straightforward: make sure each tab page has a layout, and give the tab widget a reasonable size policy or stretch factor so it shares space with its neighbors.
A minimal example that works
Let's start with a small, complete example. We'll create a horizontal layout with a vertical stack of colored widgets on the left and a QTabWidget on the right — sitting happily side by side.
First, here's a simple Color helper widget that fills itself with a solid color, useful for visualizing layouts:
from PyQt6.QtWidgets import QWidget
from PyQt6.QtGui import QColor, QPalette
class Color(QWidget):
"""A simple widget that displays a solid color."""
def __init__(self, color):
super().__init__()
self.setAutoFillBackground(True)
palette = self.palette()
palette.setColor(QPalette.ColorRole.Window, QColor(color))
self.setPalette(palette)
Now let's build the full window:
import sys
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget,
QHBoxLayout, QVBoxLayout, QTabWidget, QLabel,
)
class Color(QWidget):
def __init__(self, color):
super().__init__()
self.setAutoFillBackground(True)
palette = self.palette()
palette.setColor(palette.ColorRole.Window, QColor(color))
self.setPalette(palette)
from PyQt6.QtGui import QColor, QPalette
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QTabWidget in a Layout")
# Main horizontal layout
main_layout = QHBoxLayout()
# Left side: a vertical stack of colored widgets
left_layout = QVBoxLayout()
left_layout.addWidget(Color("black"))
left_layout.addWidget(Color("red"))
left_layout.addWidget(Color("yellow"))
main_layout.addLayout(left_layout)
# Middle: a single green widget
main_layout.addWidget(Color("green"))
# Right side: a QTabWidget
tab_widget = QTabWidget()
tab_widget.setMovable(True)
# Create tab pages WITH layouts and content
tab1 = QWidget()
tab1_layout = QVBoxLayout()
tab1_layout.addWidget(QLabel("This is Tab 1"))
tab1_layout.addWidget(Color("lightblue"))
tab1.setLayout(tab1_layout)
tab2 = QWidget()
tab2_layout = QVBoxLayout()
tab2_layout.addWidget(QLabel("This is Tab 2"))
tab2_layout.addWidget(Color("lightyellow"))
tab2.setLayout(tab2_layout)
tab_widget.addTab(tab1, "Tab 1")
tab_widget.addTab(tab2, "Tab 2")
main_layout.addWidget(tab_widget)
# Set the central widget
container = QWidget()
container.setLayout(main_layout)
self.setCentralWidget(container)
app = QApplication(sys.argv)
window = MainWindow()
window.resize(800, 400)
window.show()
sys.exit(app.exec())
Run this and you'll see the colored widgets on the left, the green widget in the middle, and the tab widget on the right — all sharing the horizontal space.
Controlling how much space each section gets
If you want finer control over how the horizontal space is divided, you can use stretch factors. These tell the layout how to distribute extra space among its children. For a deeper dive into how QHBoxLayout, QVBoxLayout, and QGridLayout work in PyQt6, see our guide to PyQt6 layouts.
main_layout.addLayout(left_layout, 1) # stretch factor 1
main_layout.addWidget(Color("green"), 1) # stretch factor 1
main_layout.addWidget(tab_widget, 2) # stretch factor 2 (gets twice as much space)
The numbers are relative. Here the tab widget gets twice the space of the left section and the green widget. Adjust these to taste.
Complete working example
Here's the full example with stretch factors and a grid layout section included, similar to the original code:
import sys
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget,
QHBoxLayout, QVBoxLayout, QGridLayout,
QTabWidget, QLabel,
)
from PyQt6.QtGui import QColor, QPalette
class Color(QWidget):
"""A simple widget that displays a solid color."""
def __init__(self, color):
super().__init__()
self.setAutoFillBackground(True)
palette = self.palette()
palette.setColor(QPalette.ColorRole.Window, QColor(color))
self.setPalette(palette)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QTabWidget Alongside Other Layouts")
main_layout = QHBoxLayout()
# Section 1: Vertical stack
v_layout1 = QVBoxLayout()
v_layout1.addWidget(Color("black"))
v_layout1.addWidget(Color("red"))
v_layout1.addWidget(Color("yellow"))
v_layout1.setContentsMargins(0, 0, 20, 0)
v_layout1.setSpacing(20)
main_layout.addLayout(v_layout1, 1)
# Section 2: Single widget
main_layout.addWidget(Color("green"), 1)
# Section 3: Another vertical stack
v_layout2 = QVBoxLayout()
v_layout2.addWidget(Color("blue"))
v_layout2.addWidget(Color("purple"))
main_layout.addLayout(v_layout2, 1)
# Section 4: Grid layout
grid_layout = QGridLayout()
grid_layout.addWidget(Color("grey"), 0, 0)
grid_layout.addWidget(Color("black"), 1, 0)
grid_layout.addWidget(Color("darkgrey"), 1, 1)
grid_layout.addWidget(Color("orange"), 2, 1)
main_layout.addLayout(grid_layout, 1)
# Section 5: Tab widget
tab_widget = QTabWidget()
tab_widget.setMovable(True)
tab_widget.setTabPosition(QTabWidget.TabPosition.North)
# Tab 1 with content
tab1 = QWidget()
tab1_layout = QVBoxLayout()
tab1_layout.addWidget(QLabel("Content for Tab 1"))
tab1_layout.addWidget(Color("lightblue"))
tab1_layout.addWidget(Color("lightyellow"))
tab1.setLayout(tab1_layout)
# Tab 2 with content
tab2 = QWidget()
tab2_layout = QVBoxLayout()
tab2_layout.addWidget(QLabel("Content for Tab 2"))
tab2_layout.addWidget(Color("lightgreen"))
tab2.setLayout(tab2_layout)
tab_widget.addTab(tab1, "Tab 1")
tab_widget.addTab(tab2, "Tab 2")
main_layout.addWidget(tab_widget, 2)
# Set up the central widget
container = QWidget()
container.setLayout(main_layout)
self.setCentralWidget(container)
app = QApplication(sys.argv)
window = MainWindow()
window.resize(900, 400)
window.show()
sys.exit(app.exec())
When you run this, you'll see all five sections displayed side by side in a single horizontal layout — colored blocks, a grid, and a tab widget, all coexisting without any one section dominating the window.
QTabWidget works perfectly in any layout alongside other widgets. Just make sure the tab pages have their own layouts and content, and use stretch factors to control how space is distributed. If you'd prefer to design these complex layouts visually rather than in code, you can use Qt Designer to build your GUI layout. For a broader overview of the widgets you can place inside your tabs and layouts, take a look at our PyQt6 widgets tutorial.
For an in-depth guide to building Python GUIs with PyQt6 see my book, Create GUI Applications with Python & Qt6.
Python Bytes
#491 Feeling Judged
<strong>Topics covered in this episode:</strong><br> <ul> <li><strong>Claude Code /insights</strong></li> <li><strong><a href="https://blog.trailofbits.com/2026/06/30/shipping-post-quantum-cryptography-to-python/?featured_on=pythonbytes">Post-quantum crypto lands in Python</a></strong></li> <li><strong><a href="https://realpython.com/python-news-august-2026/?featured_on=pythonbytes">MCP goes stateless — and FastMCP gets renamed</a></strong></li> <li><strong><a href="https://github.com/microsoft/inshellisense?featured_on=pythonbytes">inshellisense - IDE style command line auto complete</a></strong></li> <li><strong>Extras</strong></li> <li><strong>Joke</strong></li> </ul><a href='https://www.youtube.com/watch?v=5iaT_bO3INA' style='font-weight: bold;'data-umami-event="Livestream-Past" data-umami-event-episode="491">Watch on YouTube</a><br> <p><strong>About the show</strong></p> <p><strong>Sponsored by</strong> <a href="https://pythonbytes.fm/xweather">Xweather</a> Xweather combines enterprise-grade weather intelligence with agent-ready APIs, natural language capabilities, and an MCP server so your agents can adapt workflows, automate responses, and make better decisions based on real-world conditions. Michael will tell you more about them later in the show. Get started for free at <a href="http://pythonbytes.fm/xweather">pythonbytes.fm/xweather</a> <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> 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. Finally, if you want an artisanal 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.</li> </ul> <p><strong>Michael #1: Claude Code /insights</strong></p> <ul> <li>Michael’s Insights: <a href="https://blobs.pythonbytes.fm/michael-kennedy-claude-code-insights-2026-08-09.html?cache_id=6c6ef5">michael-kennedy-claude-code-insights-2026-08-09.html</a></li> <li>Be careful sharing these outputs, they include details references to your projects, errors, security findings, etc. ;)</li> <li><code>/insights</code> reads your last 30 days of local session transcripts and hands back an interactive HTML report on how you actually work. <ul> <li><strong>One command, zero setup</strong>: type <code>/insights</code> in a session, or run <code>claude -p "/insights"</code> from the shell for a non-interactive version that just prints the path</li> <li><strong>Reads what's already on disk</strong>: pulls session logs from <code>~/.claude/projects/</code>, skipping agent sub-sessions and anything under 2 messages or 1 minute</li> <li><strong>Project areas</strong>: clusters your sessions into themes like "CLI Tooling" or "Documentation" with session counts</li> <li><strong>Friction analysis</strong>: categorizes where things went wrong by root cause - and quotes your own prompts back at you</li> <li><strong>Interaction style</strong>: tells you whether you're a delegator or a micromanager, plus which workflows are worth doubling down on</li> <li><strong>Actually actionable</strong>: suggests concrete <a href="http://CLAUDE.md?featured_on=pythonbytes">CLAUDE.md</a> additions and Claude Code features you're not using</li> <li><strong>The catch</strong>: Haiku does the per-session classification, so the first run takes several minutes; results cache to <code>~/.claude/usage-data/facets/</code> and the report lands at <code>~/.claude/usage-data/report.html</code></li> </ul></li> </ul> <p><strong>Calvin #2: <a href="https://blog.trailofbits.com/2026/06/30/shipping-post-quantum-cryptography-to-python/?featured_on=pythonbytes">Post-quantum crypto lands in Python</a></strong></p> <ul> <li><code>pyca/cryptography</code> 48 ships ML-KEM (key establishment) and ML-DSA (signatures) — NIST's post-quantum standards, now one <code>pip install</code> away.</li> <li>Big deal because it's the 11th most-downloaded package on PyPI (~1.2B downloads/month) and sits under Ansible, Certbot, Airflow, and paramiko. No PQ there, no PQ anywhere in Python.</li> <li>Trail of Bits did the work (Rust bindings, cross-backend API, tests, AWS-LC backend support), funded by the Sovereign Tech Agency.</li> <li>Timing tracks a June 22 White House order setting federal deadlines: PQ key establishment by end of 2030, PQ signatures by end of 2031.</li> <li>Not a drop-in swap — the wire sizes explode. ML-DSA-65 signatures are 3,309 bytes vs Ed25519's 64; ML-KEM-768 public keys are 1,184 bytes vs X25519's 32. Hardcoded field sizes and length prefixes will bite.</li> <li>API looks like the existing asymmetric primitives, except ML-KEM is encapsulate/decapsulate rather than a Diffie-Hellman exchange. SLH-DSA (the hash-based conservative backstop) is still in progress. The primitives are here, but protocols haven't caught up — so you won't be running post-quantum Certbot this week.</li> </ul> <p><strong>Sponsor: Xweather</strong></p> <p>You're using agents that can write code, summarize documents, and automate workflows. But they're missing one thing: awareness of the world around them. This is where today's sponsor, Xweather comes in. Xweather combines enterprise-grade weather intelligence with agent-ready APIs, natural language capabilities, and an MCP server built for tools like Claude, Codex, Copilot, and modern IDEs – so your agents can adapt workflows, automate responses, and make better decisions based on real-world conditions. Backed by Vaisala, whose instruments fly on NASA missions to Mars, Xweather delivers trusted data and unique insights that go beyond conditions to actual impact – from real-time lightning strikes to road surface forecasts. Start with 15,000 free API calls each month and pay only for what you use as you grow. Xweather is your full weather stack, for developers by developers. Start building for free today at <a href="http://pythonbytes.fm/xweather">pythonbytes.fm/xweather</a>. The link is in your podcast player's show notes and on the episode page. Thanks so much to Xweather for supporting Python Bytes.</p> <p><strong>Calvin #3: <a href="https://realpython.com/python-news-august-2026/?featured_on=pythonbytes">MCP goes stateless — and FastMCP gets renamed</a></strong></p> <ul> <li>From <strong>Philipp Acsany</strong> over at Real Python</li> <li>The <code>2026-07-28</code> spec landed July 28 and the Python SDK shipped 2.0.0 the same day. Biggest rewrite since MCP launched, and it's breaking on purpose. Context for scale: the Tier 1 SDKs are pulling close to half a billion downloads a month, with TypeScript and Python each past a billion total.</li> <li>The headline is the stateless core. The <code>initialize</code>/<code>initialized</code> handshake and the <code>Mcp-Session-Id</code> header are both retired — protocol version, client identity, and capabilities now ride in <code>_meta</code> on every request, with an optional <code>server/discover</code> RPC if a client wants capabilities up front. Any request can land on any instance behind plain round-robin, no shared storage.</li> <li>Server-initiated calls are the hard part of the migration. Sampling, elicitation, and <code>roots/list</code> no longer call back to the client; instead the server returns <code>resultType: "input_required"</code> and the client retries with <code>inputResponses</code> attached. Multi Round-Trip Requests, MRTR. Also: <code>Mcp-Method</code> and <code>Mcp-Name</code> are now required headers so gateways route on headers instead of cracking JSON bodies, and missing-resource errors move to standard <code>32602</code>.</li> <li>Deprecation sweep with an actual policy behind it — Roots, Sampling, Logging, and the legacy HTTP+SSE transport all deprecated with a twelve-month minimum offramp. Tasks graduated out of the experimental core into a real extension, which is what the formalized extensions framework was for. MCP Apps is now an official extension too, so a tool call can return sandboxed interactive HTML. Auth picked up RFC 9207 issuer validation, issuer-bound credentials, and a shift from DCR toward CIMD.</li> <li>Python SDK 2.0 is where it gets personal: <code>FastMCP</code> is now <code>MCPServer</code>, no alias, no shim. <code>McpError</code> → <code>MCPError</code>. Wire types went snake_case (<code>is_error</code>, <code>input_schema</code>) and moved to a standalone <code>mcp_types</code> package, with <code>mcp.types</code> kept as a permanent alias. One <code>Client</code> object replaces the old transport + <code>ClientSession</code> + <code>initialize()</code> stack. <code>httpx</code> became <code>httpx2</code>. Sync handlers run on worker threads now, so <code>asyncio.get_running_loop()</code> raises inside them.</li> <li>The good news: one <code>MCPServer</code> serves both protocol eras, so 2025-era clients keep working with nothing to configure, and a <code>Resolve(fn)</code> parameter lets one tool body cover MRTR and the old path. 1.x is maintenance-and-security-fixes only — pin <code>mcp>=1.28,<2</code> if this week is already full. The Tasks extension isn't in 2.0.0 yet, so Tasks has left the core spec but hasn't landed in the SDK. If you only <em>call</em> MCP servers, you mostly just get the benefits for free. If you <em>ship</em> one, you already know what your week looks like. And if you use the standalone <code>fastmcp</code> package instead of the official SDK — different project, 3.x line, none of this touches you. The rename is partly to stop the two from being confused.</li> </ul> <p><strong>Michael #4: <a href="https://github.com/microsoft/inshellisense?featured_on=pythonbytes">inshellisense - IDE style command line auto complete</a></strong></p> <ul> <li>via Doug Nichols</li> <li><code>inshellisense</code> provides IDE style autocomplete for shells.</li> <li>It's a terminal native runtime for <a href="https://github.com/withfig/autocomplete?featured_on=pythonbytes">autocomplete</a> which has support for 600+ command line tools.</li> <li><code>inshellisense</code> supports Windows, Linux, & macOS.</li> <li>If you are using a <a href="https://www.nerdfonts.com/?featured_on=pythonbytes">NerdFont</a> patched font, you can enable the NerdFonts support in your config file</li> </ul> <p><strong>Extras</strong></p> <p>Calvin:</p> <ul> <li>Django 6.1 Released — https://www.djangoproject.com/weblog/2026/aug/05/django-61-released/</li> <li>DjangoCon US is quickly arriving, grab your tickets now! — https://2026.djangocon.us/ Michael:</li> <li><a href="https://pythonbytes.fm/ai-integration">AI integration: Python Bytes for AI</a></li> <li><a href="https://training.talkpython.fm/courses/up-and-running-with-rust?featured_on=pythonbytes">Up and Running with Rust Course is out!</a></li> </ul> <p><strong>Joke: <a href="https://programmerhumor.io/security-memes/google-p5hl?featured_on=pythonbytes">But they already know</a></strong></p>
Python Insider
Python 3.12.14, 3.11.16 and 3.10.21 are now available!
[Python Releases Eclipse Your Security!](https://discuss.python.org/t/python-3-12-14-3-11-16-and-3-10-21-are-now-available/108537) New security releases for 3.10, 3.11 and 3.12 are now available.
August 11, 2026
TestDriven.io
Storing Django Static and Media Files on Cloudflare R2
This tutorial shows how to configure Django to load and serve up static and media files, public and private, via Cloudflare R2.
PyCoder’s Weekly
Issue #747: Modern OOP, bisect, Django Async, and More (2026-08-11)
#747 – AUGUST 11, 2026
View in Browser »
New Book: Modern Object-Oriented Python
Real Python’s new book on classes, Python’s data model, and object-oriented design is out in Early Access. Nine chapters collected from a decade of tutorials, re-edited into one curriculum, and updated for today’s Python.
REAL PYTHON
Binary Search in Python With bisect
Python’s bisect module implements binary search for you. Here’s how bisect_left, bisect_right, and insort work, plus recipes for finding the closest match or all values in a range.
TREY HUNNER
Let AI Agents Into Your B2B App. Securely
More of your users are asking to connect AI agents to your product, and you want to say yes. PropelAuth lets you give each agent scoped, revocable access, so you stay in control of what it can do. Learn More →
PROPELAUTH sponsor
Updates on Django’s Async Story
For years, “Django and async” came with an asterisk. The docs themselves warned you off it, but that has changed. Talk Python interviews Carlton Gibson and they talk async in Django.
TALK PYTHON
Articles & Tutorials
Take the 2026 Python Typing Survey
Now in its third year, the Python Typing Survey has become a recognised reference point for the direction of Python’s type system. It’s being referenced in PEPs and presentations to the Python community. Whether you’re an experienced user of Python types or someone who hasn’t yet written your first type annotation, participating in this survey helps the open-source community make Python better for everyone.
SURVEYMONKEY.COM • Shared by Abby Mitchell
Hydra for Python Configuration: Build Modular and Maintainable Pipelines
Hardcoded paths and parameters can quickly drift out of sync across a data science project’s scripts. This article shows how to move them into Hydra configuration files, access values with dot notation, override settings from the command line, swap entire configuration groups, and run experiment sweeps with a single multirun flag.
CODECUT.AI • Shared by Khuyen Tran
📘 Modern Object-Oriented Python: Write Clean, Pythonic Code With OOP
Learn to design classes that feel native to Python: special methods, inheritance vs. composition, properties, data classes, and the SOLID principles. Get Your Copy →
REAL PYTHON sponsor
asyncio.all_tasks() Could Silently Drop Tasks on the Free-Threaded Build
A concrete example of how free-threading turns old GIL-era assumptions into silent bugs: asyncio.all_tasks() dropping live tasks from another thread. Useful for anyone porting or debugging code under the free-threaded build. Written by the author of the fix.
DEADLOVELLL.GITHUB.IO • Shared by Timofei Ivankov
Celery: From First Task to Advanced Recipes
Celery is a mature distributed task queue for Python, but many practical details aren’t immediately obvious from the documentation. This guide goes from the basic usage to timeouts, retries, preventing parallel execution, and emulating async/await support.
STANISLAV GOLEV • Shared by Stanislav Golev
Programmatically Developing LLM Prompts With DSPy
How can you move from manually writing prompts for an LLM application toward defining them programmatically? This week on the show, Brett Kennedy returns to discuss his new book “Building LLM Applications with DSPy.”
REAL PYTHON podcast
Nifty Django Feature: setUpTestData
The setUp() call in TestCase gets invoked for every single test. Django adds setUpTestData() which only gets called once per test class, meaning your tests can run faster.
TIM SCHILLING
Thoughts on “SIMD in Pure Python”
This article is a response to SIMD in Pure Python and discusses ways of optimizing his code even further.
ALISA SIRENEVA
Since When? Which Python Version Added That
An interesting tool where you paste Python into it to see which version added each feature it uses, or look up a single built-in, module, or piece of syntax.
PYTHONMORSELS.COM
Validating Data With Pointblank in Python
Learn how to validate data in Python with Pointblank: declare quality checks, split clean from failing rows, and rerun validation plans from YAML.
REAL PYTHON
Acidica
Ned has written a toy BASIC interpreter in Python. This announcement post tells you why (“for fun”) and talks about the accomplishment.
NED BATCHELDER
Some More Things About Django I’ve Been Enjoying
Julia has been learning Django to create websites in “2010 style”. This post talks about new things she’s picked up along the way.
JULIA EVANS
Sending Emails Using Python
Learn how to send emails with Python using SMTP and smtplib, attach files, format HTML messages, and personalize bulk emails.
REAL PYTHON course
Projects & Code
Events
Weekly Real Python Office Hours Q&A (Virtual)
August 12, 2026
REALPYTHON.COM
Python Nordeste 2026 (PyNE 2026)
August 13 to August 16, 2026
PYTHONNORDESTE.ORG
Python Help: Problems to Solutions
August 13, 2026
NONE
Python Atlanta
August 13 to August 14, 2026
MEETUP.COM
PyCon Korea 2026
August 15 to August 18, 2026
PYCON.KR
DjangoCologne
August 18, 2026
MEETUP.COM
PyCon Ghana 2026
August 20 to August 23, 2026
PYCON.ORG
PyCon Latam 2026
August 20 to August 24, 2026
PYLATAM.ORG
PyCon JP 2026
August 21 to August 24, 2026
PYCON.JP
Happy Pythoning!
This was PyCoder’s Weekly Issue #747.
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 ]
August 10, 2026
Brett Cannon
My nomination statement for the 2026 Python packaging council
I have decided to run for the inaugural/2026 Python packaging council (PPC). I will say I have the support of my employer (Microsoft) to do this, but they didn&apost ask me to and my usual thing that I would quit before I let any employer pressure me into doing anything I didn&apost agree with still stands.
I will admit that writing this was a little hard for me since it&aposs for the entire PSF membership (compared to the SC which is only Python core developers), and so I had to assume someone had no idea who I was (where with the core devs I have been around for so long that at the core dev sprint last year I was the 4th longest-serving member in attendance). As well, I&aposm not good at humblebragging, so I had to think about what to say, and in a way that didn&apost dismiss what I&aposve done like I typically do (as an example, I introduced myself at lunch at PyCon US once and someone at the table said, "we know who you are, Brett"; that was very flattering, humbling, and I still don&apost totally believe people who didn&apost just attend a talk I gave at that conference know who I am).
Anyway, here is a list of stuff I have done for Python packaging and some stuff I would like to see happen as I put in my self-nomination statement.
Qualifications
My qualifications for joining the council include:
- Being a Python core developer for over 23 years and the 12th most prolific contributor over Python&aposs lifetime (since April 2003)
- Serving on the first 5 Python steering councils (2019 – 2023 councils; I chose not to run a sixth time)
- Co-maintaining the &apospackaging&apos project for 7 years (since Aug 2019, and thus I&aposm a PyPA member)
- (Co-)author of 7 packaging PEPs (roughly 9% of all packaging PEPs, and roughly 5% of all PEPs regardless of type; 4th most prolific author under either classification)
- PEP 518 --
pyproject.toml - PEP 621 --
[project]table inpyproject.toml - PEP 650 -- Specifying Installer Requirements for Python Projects (withdrawn)
- PEP 665 -- Predecessor to PEP 751 (rejected)
- PEP 685 -- Comparison of extra names
- PEP 751 --
pylock.toml - PEP 794 -- Import name metadata
- PEP 518 --
- Being a PEP delegate for 5 packaging PEPs
More about me can be found on my blog, public notes, and GitHub profile.
Goals
Here are some high-level goals I have in mind for the PPC.
Setting up the inaugural PPC
Having served on the first 5 Python steering councils, I have a somewhat unique experience in knowing what can (not) end up working for councils such as the PPC. If I were to be elected, I would try to help my fellow PPC members learn from the SC&aposs experience.
Developer experience
There are two groups of users of packaging: producers and consumers.
For the people producing packages, I would want to help make the experience better. That includes having clearer specs with less edge cases and any new specs that would help ease packaging up some code. And hopefully making the process around specs easy enough that people are willing to bring up instances of where something should be updated.
For consumers, I would also like to see the experience improve. For example, part of why uv is so fast is it doesn&apost strictly follow the current specs (while pip always tries to follow the spec accurately). In those cases where uv doesn&apost follow a spec but has found it to work out, I think we should evaluate if there&aposs a change to be made so that pip can have an equivalent benefit.
A good example of this is that uv assumes all wheel files have the same metadata, while pip doesn&apost since the specs say the metadata can vary. If the specs could somehow be updated so you only had to check a single copy of release metadata, then pip doesn&apost have to check every wheel it considers when trying to determine what to install which takes time.
Secure supply chain
Unfortunately, there are bad people on the internet. And those bad people know there are a lot of Python developers, so they are trying to exploit Python projects for nefarious reasons. As such, I think we should do what we can to make things hard for these bad people while not adding a bunch of burden on those who are doing us all a service by sharing their code in the world (i.e. better security without sacrificing the developer experience).
There are two ways to thwart attackers: keep them out and prevent yourself from being exploited if there is vulnerable. One way to help keep attackers out is verifying files are legitimate. One possibility for this is to make getting reproducible builds easier, from source to wheel. This would require everything from code to help package up the bits in a reproducible way to metadata to be able to trace a wheel file back to its source code. This would let people be able to independently verify the files uploaded to PyPI were not tampered with between the source repository to uploading.
For preventing exploitation once some vulnerable code exists, one approach is software bills of material (SBOMs). If we could make it easy to have SBOMs for every step of the packaging process as well as for anything you install, it would make it easier to know when you may be running vulnerable code. This work was started with PEP 770 (which I was a PEP delegate on), but there are more opportunities to record more SBOMs (transparently) along more of the packaging process.
James Bennett
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 whose members are the constituent lines of the text?
Hopefully, your first instinct is to reach for some sort of standard-library function, maybe something like the splitlines() method of Python’s str type. Because it turns out this “simple” question is actually pretty complex to answer! For example, quite some time ago I read a post by William Woodruff pointing out the surprising discovery that Python treats up to eleven different Unicode code points or code point sequences as indicating a line break.
At the time I meant to write about that, but a lot of other things started fighting for my time, and it’s only now that I’m finally digging it out of my drafts. Still, better late than never, so today let’s dig into some of the many ways there are to break a line of text and how they’ve been standardized and specified and ultimately wound up in the set Python uses.
In the beginning…
Once upon a time, there was ASCII. Of course there were other things before ASCII, and alongside ASCII, but for today’s discussion we really only need to go back to ASCII; if you want the full history of physical teletypes, how they evolved from typewriters and influenced character sets for computing and so on, I suggest Wikipedia. Here, I’m just going to gloss over and simplify a lot of that to focus on the topic at hand.
So. Once upon a time, there was ASCII. And it wound up being incredibly influential and important in computing, to an extent other early character sets couldn’t match. And because it was used on computers which used teletypes (basically electronic typewriters connected as input/output devices) as a user interface, it contained control characters for sending commands to the teletype. Such as a LINE FEED (byte value 0x0A) to advance the paper vertically to the next line, and a CARRIAGE RETURN (byte value 0x0D) to re-align the print head/carriage with the horizontal start point of the line.
These are often abbreviated LF and CR (or by their C-family escape sequences \n and \r, respectively), and you might think that since physically advancing a typewriter-style device to be ready to print the next line requires both operations, that would have just become the universal way everybody did new lines. Or at least the universal way everybody did them in English, or in the US, where ASCII dominated. Right?
Well, nothing is ever that simple. Physical teletypes apparently benefited from the two-character approach (as opposed to a single “new line” character) because it gave them time to physically move everything into the right position. But as virtual teletypes—“printing” to a television-like display instead of to paper—became more common, that was less of an issue. So there were multiple possible options for representing line breaks, and several of them showed up in historical systems. For example:
- CP/M used
CR LF. And so MS-DOS, which aimed for compatibility with it, usedCR LFtoo. And so Microsoft Windows, which wanted to be compatible with MS-DOS, also used it. - Meanwhile, Multics chose to use just
LFwith noCR, and Unix went along with that choice. - But Commodore and Apple and many others went yet another way and used plain
CR, with noLF.
This meant “plain text” was not easily portable between these various systems, since none of them could agree on how to represent a line break. Which led to one of my all-time favorite programming jokes, in the infamous “NOT the comp.text.sgml FAQ” document:
Q. What’s an RE?
A. RE is an acronym for Record End, which is sort of like a newline, only different. Goldfarb’s First Law of Text Processing states that:
“… if a text processing system has bugs, at least one of them will have to do with the handling of input line endings.”
[The Handbook, footnote p. 321]
The Record End concept was introduced to make sure that SGML parsers don’t violate Goldfarb’s First Law.
(for the uninitiated, Charles Goldfarb created SGML)
Anyway, over twenty years ago Python tried (in Python 2.3) to smooth this over by introducing “universal newline” mode for opening files, which accepts all three options: a plain \n (Unix), or a plain \r (classic Mac), or an \r\n sequence (DOS and Windows) will all be interpreted as line breaks.
But even in ASCII there there are other ways of breaking a line. For example, at byte value 0x0C ASCII includes the FORM FEED control character (FF, or \f). Which is not one of the traditional characters used by major operating systems as a “newline”, but nonetheless does cause a new line to occur: it moves to the next page (if necessary, by ejecting the current sheet of paper from the printer and feeding in a new one). And there’s also 0x0B, VERTICAL TAB (VT or \v): just as a “regular” tab (\t) causes a horizontal adjustment, a vertical tab causes a vertical one. So it, too, causes output to advance to another line (probably skipping several in the process).
And the C1 control characters added 0x85, the NEXT LINE character (typically abbreviated NEL), useful for translating back and forth between ASCII and IBM’s EBCDIC character set (which had “New Line” as a single character).
Then Unicode happened
Today we live in a Unicode world, and Unicode tries its hardest to catalog and standardize and describe how to work with all the world’s writing systems. Chapter 5, Section 8 of the Unicode Standard, “Newline Guidelines”, lists seven code points to recognize as causing new lines. Five of them we’ve seen already:
U+000A LINE FEED, from ASCIIU+000B LINE TABULATION, from ASCII’s vertical tabU+000C FORM FEED, from ASCIIU+000D CARRIAGE RETURN, from ASCIIU+0085 NEXT LINE, from the C1 control codes
The CR LF sequence is also recognized, on systems which use it.
But the other two code points are new and were created specifically for Unicode:
U+2028 LINE SEPARATOR(which Unicode likes to abbreviate asLS)U+2029 PARAGRAPH SEPARATOR(similarly abbreviated asPS)
The Unicode Standard explains that the traditional newline characters had started to become ambiguous, because of the rise of tools such as word-processing programs which implicitly broke lines to wrap them for display and so began using explicit “newline” characters to mean a paragraph break rather than a line break. So Unicode added two new code points whose purposes are explicit. And the standard says that “[I]n Unicode text, the PS and LS characters should be used wherever the desired function is unambiguous.”
This set of line-breaking code points originated in version 5.0 of Unicode, with Unicode Technical Report #13, which lists the seven “newline” code points and the CR LF sequence. This is also the set of code points and sequences defined for line boundaries in Unicode regular expressions, Unicode Technical Standard #18.
And expanding on Chapter 5 of the Standard, there’s Unicode Standard Annex #14, “Unicode Line Breaking Algorithm”. As the name implies, this document formally specifies the line-breaking algorithm for Unicode, including defining things like which characters offer an opportunity to break a line, whether the break is mandatory, and whether the break would come before or after the character in question. It does this in a typical Unicode way: by defining a set of named properties and specifying which characters have which properties.
Two ways about it
But there are still three “newline” characters supported by Python that we haven’t seen yet, and they come from a place that might be surprising: Unicode Standard Annex #9, the bidirectional algorithm. And it’s OK if you’re wondering what that has to do with newlines, because it’s not immediately obvious if you don’t already know about it.
Some written scripts, like the Latin script this blog post is written in, are written and read left-to-right: the start of a line of text is on the left-hand side, and the end is on the right-hand side. Other scripts, such as Arabic or Hebrew, do the opposite, and are right-to-left. And so Unicode, which again wants to cover all the world’s writing systems and let you use any or all of them, has to support both left-to-right and right-to-left horizontal text direction.
But more than that, it has to support switching direction within a single piece of text. You might have something that’s in, say, Arabic but quotes something in Spanish in the middle of a line; that would require a short section of left-to-right inside an otherwise right-to-left text. Or you might be writing something that uses boustrophedon, switching directions on each line. So Unicode includes direction-control characters like U+200E LEFT-TO-RIGHT MARK and U+200F RIGHT-TO-LEFT MARK to handle this. But it also needs to know the scope of a direction change, and that’s where the last “newline” characters come in: the Unicode bidirectional algorithm says that “[t]he effects of all of these formatting characters are limited to the current paragraph; thus, they are terminated by a paragraph separator”.
So Unicode characters have, among their properties, a “bidirectional class” which influences how they affect the bidirectional algorithm. And the characters which act as paragraph separators for purposes of ending the effects of an explicit directional marker all share a common value for this: bidirectional class B. The characters with that class include quite a few that we’ve already seen, along with three more characters:
U+001C INFORMATION SEPARATOR FOURU+001D INFORMATION SEPARATOR THREEU+001E INFORMATION SEPARATOR TWO
But these are better known by their original ASCII names: FILE SEPARATOR, GROUP SEPARATOR, and RECORD SEPARATOR. ASCII provided these to help represent data structures in memory and on storage media. Today it’s not as common to try to use control characters for this purpose, though they do have the virtue of being rare in actual text, unlike other common delimiters such as tab or comma.
End of the line
And now, after looking at multiple character sets and five Unicode technical documents, we can finally state clearly what’s going on in Python.
Python’s splitlines() treats ten different code points, and one multi-code-point sequence, as causing a line break. These are:
- The sequence
U+000D U+000A(CR LF). - The four code points which have line-breaking property
BK(Mandatory Break (Non-tailorable)):U+000B LINE TABULATION,U+000C FORM FEED,U+2028 LINE SEPARATOR, andU+2029 PARAGRAPH SEPARATOR. - The one code point which has line-breaking property
CR(Carriage Return (Non-tailorable)):U+000D CARRIAGE RETURN. - The one code point which has line-breaking property
LF(Line Feed (Non-tailorable)):U+000A LINE FEED. - The one code point which has line-breaking property
NL(Next Line (Non-tailorable)):U+0085 NEXT LINE. - The three code points which don’t have any of the above line-breaking properties, but do have bidirectional property
B:U+001C INFORMATION SEPARATOR FOUR,U+001D INFORMATION SEPARATOR THREE, andU+001E INFORMATION SEPARATOR TWO
Which is also exactly what’s stated by a comment in the CPython source code accompanying the list of individual code points that are considered to break lines, but hopefully now you have a better understanding of what that comment means and how this particular set was arrived at.
Talk Python to Me
#558: Hyper-Personal Software with Python
Every company has one. The little internal tool that Jane built back in 2021, and then Jane left. Nobody understands it, nobody will touch it. There are two unwritten rules around it: don't change it, it's working. And if you break it, you bought it. That's dark-matter enterprise software. <br/> <br/> For every app you can actually see, there are ten of these sitting in the shadows, frozen. Michael Booth thinks that just changed. He read my article on hyper-personal software and ran with it, writing about hyper-team software: small teams inside big companies finally building the tools that were never going to get built. <br/> <br/> We cover where this works, where it quietly goes wrong, and the guardrails that keep it from turning into a mess. 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/devopsbook'>Python in Production</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>Michael Booth</strong>: <a href="https://github.com/mjboothaus/?featured_on=talkpython" target="_blank" >github.com</a><br/> <br/> <strong>Talk Python AI Integrations</strong>: <a href="https://talkpython.fm/blog/posts/announcing-talk-python-ai-integrations/" target="_blank" >talkpython.fm/blog</a><br/> <br/> <strong>From Hyper-Personal to Hyper-Team Software: Small Team-Built, AI-Assisted Tools Inside the Enterprise</strong>: <a href="https://www.databooth.com.au/posts/hyper-team-software/?featured_on=talkpython" target="_blank" >www.databooth.com.au</a><br/> <br/> <strong>What hyper-personal software looks like (MK's article)</strong>: <a href="https://mkennedy.codes/posts/what-hyper-personal-software-looks-like/?featured_on=talkpython" target="_blank" >mkennedy.codes</a><br/> <br/> <strong>Databooth Site</strong>: <a href="https://www.databooth.com.au?featured_on=talkpython" target="_blank" >www.databooth.com.au</a><br/> <br/> <strong>Wall Street just lost $285 billion because of 13 markdown files</strong>: <a href="https://martinalderson.com/posts/wall-street-lost-285-billion-because-of-13-markdown-files/?featured_on=talkpython" target="_blank" >martinalderson.com</a><br/> <strong>SaaSpocalypse is real but everyone is panicking about the wrong thing</strong>: <a href="https://www.reddit.com/r/SaaS/comments/1rtszfp/saaspocalypse_is_real_but_everyone_is_panicking/?featured_on=talkpython" target="_blank" >www.reddit.com</a><br/> <strong>Warp Terminal</strong>: <a href="https://www.warp.dev?featured_on=talkpython" target="_blank" >www.warp.dev</a><br/> <br/> <strong>Watch this episode on YouTube</strong>: <a href="https://www.youtube.com/watch?v=rWSRsEBiyiE" target="_blank" >youtube.com</a><br/> <strong>Episode #558 deep-dive</strong>: <a href="https://talkpython.fm/episodes/show/558/hyper-personal-software-with-python#takeaways-anchor" target="_blank" >talkpython.fm/558</a><br/> <strong>Episode transcripts</strong>: <a href="https://talkpython.fm/episodes/transcript/558/hyper-personal-software-with-python" 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>
Ed Crewe
From Routing Checks to Trajectory Testing: Evaluating an Agentic Chatbot
pre[class*="language-"] { border-radius: 6px; font-size: 14px; overflow-x: auto; }
Which Agentic Chatbot?
I have been working on a Python based AI test framework for a chatbot interface for my company's product, Postgres AI Hybrid Manager. The manager allows the setup of Postgres clusters across cloud or on-prem and attaching various AI tools such as Langflow. So a combination of more traditional Postgres backup, migration, telemetry and analytics features along with LLM workflows leveraging the data it holds.
The product already has a control plane UI for managing Postgres estates. It also has full help for the product, all Postgres versions, analytics, AI and add ons. The chatbot brings all these things together: ask a question, get the relevant help, or ask it to do something such as migrate a cluster, or evaluate telemetry that would otherwise require clicking through the UI.
That makes it a pretty handy interface, especially for the less technical. However it is not a simple to test and ensure good quality responses.
A normal deterministic API test is simple. Send a request, check the status code, check the JSON body, perhaps check the database state. An LLM-backed agent does not pass or fail so clearly. It can route to the wrong capability and still return fluent text. It can pick a plausible but wrong tool. It can miss half the task and still sound confident. It can complete the first turn of a conversation and lose the plot on the second. It could get malformed or missing data from tooling that leads it to deliver a misleading conclusion. It might only provide help to something that should be from tool data or was a request for an action such as create a cluster.
So the testing problem was not “does the chatbot return a reasonable response?” It was “how do we test the whole chat path is doing the right thing?”
This is the story of how our agent-eval test framework evolved as we worked to see that our chatbot was not only getting the right answer, 42 , but whether it was asking all the right questions of the right tools to get that answer. Known as trajectory testing ...
You're Golden
Before we can tell our story we need to define some terms.
A Golden is an example of a perfect desired output from a test input. They often refer to more complex outputs that may need saving as separate files, but a simple assertable output such as 42, is a golden too!
Whilst complex goldens may be used and marked for semantic similarity against the test output. It is more common for complex outputs to be described by a rubric. A rubric is a checklist of qualitative properties a good answer must exhibit, written in plain English as opposed to a golden example of an answer.For AI testing the tests are termed evals, ie they evaluate the tool, but not by strict assertions, because one thing you can be sure of with an LLM is that given the same input, you usually get subtly different output, ie they are non-deterministic. Which means for LLM outputs the only way to test them is to use an LLM-as-judge, ie give that LLM the test output and a rubric or golden and let it mark it against that. Then you set a pass threshold for that mark, to translate your complex output into a pass or fail.
You can also total up all the passes to give you a Task Completion Rate, TCR. So with complex AI agentic LLM interactions a 100% pass of all evals is often not realistic. Hence you set a TCR below 100% for the whole test suite of evals to pass. Start with the smallest useful test. The core principle of evals is not complicated, you want the input to give you the expected output.
But for an Agentic application this may require a sequence of LLM calls and tools: Making the final output dependent on the route that should be chosen, the tool(s) that should be called, the actions to be taken, further LLM calls that may be necessary and finally the core data that the response to the user should contain.
Our first version did not try to solve every part of that. It started with routing, simple and deterministic.
Routing is the starting point
The chatbot originally had an agent per tool. The tool being the code and API calls that performed actions or returned data or help.
Different specialist agents owned different parts of the product surface: Control-plane actions, Postgres database operations, schema design, roles and permissions, cluster reporting, migration, and so on.
Before any specialist can help, something has to choose the right specialist.
So the first eval suite asked a narrow question:
Given this user prompt, did the chatbot route to the expected tool?That gave us a fast health check. We could keep a corpus of prompts, map each one to an expected destination, run them through either a direct model path or against the real deployment and its tools, and score whether the selected destination tool matched the golden.
A golden here is just the name of the tool:
- id: "core-iam-001" prompt: "List all my projects" expected_tool: "control-plane" tags: ["core", "control-plane", "project"]And the check on the other end is deliberately dumb — an equality test, not a semantic one:
self.success = tool_match(predicted_tool, expected_tool)
Agents became skills, but routing remained
The design moved away from “one agent per tool family” toward a more consolidated orchestrating agent with skills.
That is a better fit for how modern agent systems are evolving. A skill = instructions, constraints, and a subset of tools that are relevant for a task. It is a form of progressive disclosure. Give the model the minium it needs at each step to save tokens.
But this did not make routing irrelevant.
Instead of asking “did we transfer to the right sub-agent?”, the eval asks “was the right skill made visible and selected for this task?” The labels changed but a skill could still use the wrong tool.
Routing evals stayed valuable because they were fast, explainable, and easy to run in CI. But they are limited, routing should always be correct but it doesn't mean that the final agent response is too.
TCR jumps to the endpoint, the response
Task Completion Rate, or TCR, was the next step.
The user asked for a cluster comparison, or a schema recommendation, or help diagnosing a database issue. We need to know whether the full response actually completed these tasks.
Responses are complex goldens so they need the LLM-as-a-judge pattern: run the chatbot, take the actual response, and ask a judge model to score it against expected sections.
The eval has a rubric here for judging the output:
- id: "tcr-core-014" prompt: "Compare CPU usage between these two clusters" expected_sections: - "identifies which cluster has higher CPU usage" - "cites at least one supporting metric" - "suggests a plausible next step"The judge gets one simple instruction: score each
expected_sectionsbetween 0.0–1.0 A metric class then just thresholds it for pass / fail:self.success = score >= 0.7The judge must be calibrated and a consistent model used for comparing runs over time. Enabling skill an prompt tuning from metric trends. The rubric must be specific enough to avoid marking waffle as success. But it turns a non-deterministic complex output into a simple pass and fail. It also separated two different levels of QA:
- Can the underlying model answer the task if given the right context?
- Does the deployed chatbot complete the task through the real product path?
That led to two execution modes.
Direct mode calls the model with simulated context. It is faster and useful for prompt and rubric development.
Proxy mode calls the real chatbot. It is slower, but it exercises the production path: routing, skill selection, tool calls, guardrails, streaming responses, conversation state, and the actual service wiring.
Both matter. Direct mode tells you whether the model is capable of the answer. Proxy mode tells you whether your product is capable of really delivering it via agents running your deployment's tools.
This is the major difference from standard AI LLM testing, the model is only a small pluggable engine for the full agentic skill set that requires the actual deployment domain of data, actions and tools. Direct mode testing of only the model, is occasionally useful but E2E testing of the Chatbot deployment is required for agentic AI Chatbot QA, tuning and validation.
Multi-step conversations changed the unit of testing
Single-turn TCR is still too small for many real chatbot tasks.
Users do not always provide all required information in one message. They ask to create a cluster, then pick a project, then choose a size, then confirm. They ask for a schema review, then refine the problem, then ask for a migration path. They troubleshoot by adding information over time.
So the framework has to exercise test cases that are conversations, not just single prompts.
That sounds like a minor data-model change. It was not. Once a test has steps, the eval runner has to preserve conversation state. In proxy mode, that means carrying the real
conversation_idreturned by the chatbot and sending each follow-up as part of the same server-side conversation. In direct mode, it means building a synthetic conversation history so the model sees the prior turns.In code that split is about as literal as it sounds. Proxy mode threads a real id through each call:
response = client.send_message(prompt=msg, conversation_id=conversation_id) conversation_id = response.conversation_id # captured on turn 1, reused afterDirect mode has no server-side conversation to lean on, so it fakes one by re-rendering the transcript into the prompt itself, every turn:
full_prompt = f"## Conversation History\n{render(history)}\n\n{next_prompt}"Same test case, same expected outcome, but a different code path depending on which half of the system is actually holding the conversation state. That's impacts multi-turn evals because conversation memory is part of the harness code for the actual deployment not just a model issue.
The scoring also becomes more interesting. You want per-step checks, because the assistant should ask the right clarifying question at the right time. You also want an overall score, because a conversation can have reasonable individual turns and still fail to complete the user's goal.
Coding it yourself: deepeval underneath
Everything above sits on top of deepeval, the open-source LLM eval library. We add a Synthesize → Execute → Evaluate pipeline, a plugin system, YAML goldens, CI wiring, and Langfuse push on top of it But the core library underneath is plain deepeval, and you do not need any of the surrounding machinery we used. Here are routing, TCR and multi-step just built directly on deepeval (simplified deepeval 3.6.9)
A test case is just an input/output pair.
LLMTestCaseis the base unit everything else scores:from deepeval.test_case import LLMTestCase test_case = LLMTestCase( input="List all my projects", actual_output=chatbot_response_text, # what the system under test said expected_output="control-plane", # the golden - a skill label here, not prose additional_metadata={"predicted_skill": predicted_skill}, )Routing is a custom metric, not a built-in one. deepeval ships plenty of semantic metrics, but “did it route to the right skill” is an exact-match business rule, so you write your own
BaseMetric. This is a simplified version of the same shape our realAgentMatchmetric takes:from deepeval.metrics import BaseMetric from deepeval.test_case import LLMTestCase class AgentMatch(BaseMetric): def __init__(self, threshold: float = 1.0): self.threshold = threshold self.async_mode = False # routing checks are cheap; no need for async here def measure(self, test_case: LLMTestCase) -> float: predicted = test_case.additional_metadata["predicted_skill"] expected = test_case.expected_output self.score = 1.0 if tool_match(predicted, expected) else 0.0 self.success = self.score >= self.threshold return self.score async def a_measure(self, test_case: LLMTestCase) -> float: return self.measure(test_case) def is_successful(self) -> bool: return bool(self.success) @property def __name__(self): return "Agent Match"
tool_matchis the check from earlier. Run it with deepeval's own runner rather than hand-rolled assertions, and you get retries, pretty output, and a result object for free:from deepeval import evaluate evaluate(test_cases=[test_case], metrics=[AgentMatch()])TCR is where deepeval's built-in
GEvalearns its keep.GEvalis deepeval's off-the-shelf LLM-as-judge metric, you give it criteria (or explicit evaluation steps) and it handles the judge prompt, the JSON parsing, and the scoring for you. Our rubric-per-lineexpected_sectionsmaps ontoevaluation_stepsalmost directly:from deepeval.metrics import GEval from deepeval.test_case import LLMTestCase, LLMTestCaseParams task_completion = GEval( name="TaskCompletion", evaluation_steps=[ "Check whether the response identifies which cluster has higher CPU usage", "Check whether the response cites at least one supporting metric", "Check whether the response suggests a plausible next step", ], evaluation_params=[LLMTestCaseParams.INPUT, LLMTestCaseParams.ACTUAL_OUTPUT], threshold=0.7, ) test_case = LLMTestCase( input="Compare CPU usage between these two clusters", actual_output=chatbot_response_text, ) evaluate(test_cases=[test_case], metrics=[task_completion])Multi-step conversations get their own test case type.
ConversationalTestCasetakes a list ofTurns instead of a single input/output pair, and pairs with aBaseConversationalMetricinstead ofBaseMetric:from deepeval.test_case import ConversationalTestCase, Turn convo = ConversationalTestCase( turns=[ Turn(role="user", content="Create a new cluster"), Turn(role="assistant", content="Sure - which project should it go in?"), Turn(role="user", content="acme-prod"), Turn(role="assistant", content=final_response_text), ], expected_outcome="A cluster is created in acme-prod after resolving the missing project name", )deepeval has a conversational counterpart to
GEvaltoo (ConversationalGEval), scored against the whole turn sequence rather than a single response which is the natural fit for “did the assistant ask the right clarifying question at the right time”, the per-step-plus-overall shape TCR needed once prompts became conversations.Put together, that is the whole starting kit:
LLMTestCaseplus a hand-writtenBaseMetricfor hard business rules like routing,GEvalfor rubric-style task completion,ConversationalTestCaseplusConversationalGEvalonce a prompt becomes a conversation, andevaluate()to run the lot and get a result object back.
Everything else we built, the YAML goldens, the plugin architecture, the CI wiring, the Langfuse push exists to run more of these at scale and make the failures easy to find. But none of it is required to get started. If you are testing your own agentic chatbot, this is how to begin.This is where instrumentation started to matter much more.
For a single-turn answer, a markdown report with pass/fail rows is often enough to start debugging. For multi-step conversations, that is thin. You need to know which turn failed, whether the route changed, whether the wrong tool was called, whether the tool call used correct arguments, whether the model forgot earlier context, or whether the final answer simply missed a required section.
That is why we added span-level telemetry and pushed eval traces into Langfuse.
Langfuse made the failures inspectable
The useful thing about Langfuse is not just having another pretty dashboard. Although that is important for spotting quality regressions over time via regular CI/CD automated runs.
The vital thing was being able to treat an eval run as a set of traces. A run becomes a session. Each test case becomes a trace. The trace carries the prompt, response, scores, tags, model, mode, scenario, and the spans emitted by the proxy.
For a chatbot path, those spans are where the debugging starts. You can see routing, tool execution, LLM calls, latency, and token usage where it is available. You can filter by scenario and model. You can compare runs. You can look at a failing conversation and see whether the problem began at route selection, tool selection, tool arguments, or final synthesis.
That changes the tuning loop.
Without traces, an eval failure says “this case failed”. With traces, it can say why it failed.
That distinction matters because the fix lands in different places...
Is it a routing rule?
Is it a skill description?
Is it a tool schema?
Is it the judge rubric?
Is it that the eval has has an expectation that the product has never actually promised?Trajectory testing -> knitted the pieces together
Routing and TCR started as separate signals.
Routing asked whether the right capability was selected. TCR asked whether the final task was completed. Multi-step testing asked whether that held across a conversation. Instrumentation showed what happened between those points.
Trajectory testing is the next natural step: score the path itself.
For an agentic product, the fully correct path is essential to response quality.
So trajectory tests add expectations about intermediate actions:
- which tool or flow should be used
- whether the arguments are valid
- whether the conversation reached the right state
- whether the final answer completed the task
The label-based routing tests are still useful as fast canaries. They tell us whether the classifier shape has drifted and distinguish tiers - see the next section.
But full trajectory tests judge the route by consequence: did the system actually follow the tool path that would satisfy the user?So retain the fast determisitc routing tests, but move more user-visible behavioural coverage into trajectory and TCR.
Sovereign AI makes the eval problem tiered S/M/L/XL
There is one more constraint that makes this more than a generic chatbot-testing story.
Our chatbot has to work for sovereign and air-gapped deployments. In those environments, prompts, tool results, schema details, and operational data cannot be sent to a hosted frontier model outside the customer's trust boundary. The inference model may run inside the customer's environment.
That usually means a smaller model.
Smaller models are not just cheaper versions of larger ones. They have different context limits, weaker tool-selection behaviour, and less tolerance for an over-wide capability surface. If you show a smaller model every possible tool and skill, you have increased the chance that it chooses a bad one.
So the architecture becomes tiered. Models are effectively T-shirt sized. A small self-hosted model sees a curated subset of reliable skills. A larger model can be allowed to see more. Some experimental or complex skills only make sense for the highest tiers.
That changes the meaning of a routing eval again.
The correct visible skill set is no longer universal. It depends on the model tier. A prompt that should route to an advanced skill for an XL model may need to be dropped, refused, or handled differently for a smaller model that should not see that skill at all.
This is why trajectory testing and routing need to be tier-aware. We are not only asking whether the chatbot can complete a task. We are asking whether it can complete the task through the capability surface that a deployment's LLM size allows.
What I would keep from the journey
The final shape was not obvious at the start.
We began with routing because it was the first integration failure point and the cheapest one to isolate. We added TCR because correct routing did not prove task completion. We added multi-step cases because real users have conversations, not isolated prompts. We added telemetry because multi-step failures are otherwise too hard to debug. We moved toward trajectory testing because the route, tools, arguments, and answer need to be judged as one path.
If I were starting another agentic product eval framework, I would keep that order.
Do not start by trying to build a grand universal benchmark. Start with the smallest failure point that would embarrass the product if it regressed. Then move the signal closer to the user's actual goal.
For a chatbot wired into a real control plane, that means testing more than the output text. It means testing the route, the skill, the tool call, the arguments, the conversation state, the final answer, and the model tier that made those options visible in the first place.
That is the difference between checking that an AI system said something vaguely relevant and checking that it actually did all the things the user asked of it.
Django Weblog
Django is moving to an annual release cycle
Django's Steering Council has accepted the Django Enhancement Proposal DEP 20 to move Django to an annual release cycle. From January 2028, Django will make one feature release a year, giving every feature release the LTS-level three years of support, and version numbers will carry the feature release year: Django 2028, then Django 2029, and so on.
Better Python support
Python releases annually, each October. Django's eight-month cycle hasn't fit that well: LTS releases carried a wide Python matrix, including versions long past their upstream end-of-life.
Under the new cycle, each Django version supports the three latest Python versions at release, and picks up the new Python version during its first year. Django's support window ends in step with its oldest supported Python.
Every release is an LTS
Every feature release gets three years of support: one year of mainstream bugfixes, then two years of security and data-loss fixes. The "LTS" label is retired — every feature release now carries that same, unique commitment.
No more LTS gap: no racing a deadline to jump two years of changes at once. Upgrade one year at a time, whenever suits you within the support window. Three versions are supported at any time, giving third-party packages a clear, rolling target.
API stability and deprecation policies are unchanged — deprecation periods actually get longer in calendar terms.
Transition timeline
Django 2028 will be the first release under the new cycle.
| Release | Date | End of mainstream support | End of extended support |
|---|---|---|---|
| Django 6.1 | August 2026 | April 2027 | December 2027 |
| Django 6.2 LTS | April 2027 | December 2027 | April 2030 |
| Django 2028 | January 2028 | January 2029 | January 2031 |
| Django 2029 | January 2029 | January 2030 | January 2032 |
| Django 2030 | January 2030 | January 2031 | January 2033 |
Nothing changes before 2028. Support commitments for Django 5.2 LTS and 6.2 LTS stand as made.
Read the DEP
DEP 20 has the full specification and the reasoning behind each decision. Thanks to everyone who took part in the discussion, and to the Steering Council for its consideration.
August 09, 2026
Ned Batchelder
Caller-specific coverage
I’ve had an idea rattling around to get more detail from coverage measurement. Can we measure the coverage in a function separately for each caller of the function?
Here’s why I want it: in Acidica, my toy BASIC interpreter, I had code to implement the built-in functions that looked something like this:
match func_name:
case "LEN":
if len(args) != 1:
raise TypeError(f"Wrong arguments for LEN, got {len(args)}")
return len(args[0])
case "LEFT$":
if len(args) != 2:
raise TypeError(f"Wrong arguments for LEFT$, got {len(args)}")
return args[0][:args[1]]
# ... 19 other built-ins ...
I didn’t like the repeated code here: each different func_name has to
check that it got its expected number of arguments and perhaps raise an error.
So I refactored:
def expects(nargs: int, func_name: str, args: tuple) -> None:
if len(args) != nargs:
raise TypeError(f"Wrong arguments for {func_name}, got {len(args)}")
match func_name:
case "LEN":
expects(1, func_name, args)
return len(args[0])
case "LEFT$":
expects(2, func_name, args)
return args[0][:args[1]]
Nice. The code is tighter, easier to read, and common behavior is implemented in one place.
But the old code had an advantage: because each error condition had its own
raise line, coverage measurement could tell me whether I had tested every
func_name for the wrong number of arguments. With the error handling
happening in a helper function, that information is lost. I’ll know that
some func_name had a test for the wrong number of arguments, but
not that all of them did.
Here’s where the new idea comes in. What if I could indicate that for the
expects function, I want separate coverage data for each distinct calling
site? Then I could see that every func_name had a test for both the wrong
number of arguments and the right number of arguments. The simple branch inside
expects would be measured separately for each caller.
I have a quick proof-of-concept. A decorator on expects does the work.
Coverage.py already has dynamic contexts which are used for things like tracking
which tests called which code. The decorator starts a new context named for the
calling location, then restores the context when the function returns:
def coverage_per_caller(func):
@functools.wraps(func)
def _wrapper(*args, **kwargs):
cov = coverage.Coverage.current()
name = func.__name__
caller = inspect.currentframe().f_back
file = caller.f_code.co_filename
lineno = caller.f_lineno
prev_context = cov.switch_context(f"per_caller:{name}:{file}:{lineno}")
try:
ret = func(*args, **kwargs)
finally:
cov.switch_context(prev_context)
return ret
return _wrapper
I had to make one tiny (unreleased) change to coverage.py for this:
switch_context used to return None, but now it returns the previous
context so that we can nest them properly.
To my delight, this works! I can look at the HTML coverage report and see the
caller contexts for the lines in expects. I can see that 20 callers ran
the if line, but only 2 ran the raise, and the context names show
the file and line number of the callers for each:
This isn’t the whole solution yet. Things to improve:
- I’d like to post-process these contexts to show which callers were missing
lines inside
expects. What I’m looking for is the same kind of “this line is missing” information that I got from the original inlined logic. - These per-caller contexts overwrite the contexts we were already collecting (the test names). Ideally we’d have some kind of sub-context so that we could track both (or many) at once.
- It’s not great that I had to add a decorator to the source code. Driving this through the coverage configuration would keep these kinds of details out of the source.
But it’s a start, and gives me other ideas. I could use some aspect of the
data passed into a function as the context name. In this example, we could have
used func_name as the context instead of the caller’s location. Maybe
you have ideas for other uses.
LernerPython blog, from Reuven Lerner
Free real-world Pandas exercises, with solutions
If you want to get better at Pandas, the hard part isn’t finding tutorials. It’s finding problems worth solving. Most exercises hand you a tidy little table of five rows and ask you to sum a column — which teaches you the syntax, but nothing about the job.
For the last 3.5 years, I’ve written Bamboo Weekly, a weekly set of Pandas exercises built on real, current, public data: coal plants, earthquakes, Netflix viewing hours, government corruption indices, and IPO filings, among many others. Real data, which means the columns are named badly, the dates are strings, and answering the interesting questions takes four steps rather than one.
As of this week, every issue older than two years is free — no signup, no subscription. That’s issues #1 through #78, with 155 posts, and more than 500 exercises with fully worked-out solutions. Another opens every week as it passes its second birthday.
Why real data changes what you learn
A toy dataset teaches you groupby. A real one teaches you that the column is a string when you expected a number, that three rows have a country name nobody standardised, and that observed=True changes your answer.
Here is an actual example from the archive — the Global Coal Plant Tracker, one row per generating unit, asking which countries emit the most CO2 from coal:
import pandas as pd
url = ('https://www.bambooweekly.com/content/files/wp-content/uploads/2024/02/'
'global-coal-plant-tracker-january-2024.xlsx')
(
pd.read_excel(url, sheet_name='Units',
usecols=['Country', 'Annual CO2 (million tonnes / annum)'])
.groupby('Country')
['Annual CO2 (million tonnes / annum)']
.sum()
.sort_values(ascending=False)
.head(3)
)
Country
China 10091.0
India 3941.4
United States 1999.9
Four methods, one question, and every step is one you would actually use at work. That is the whole idea. You improve your data-analysis muscle memory with Bamboo Weekly, and then you’re ready to tackle problems at work with greater confidence.
Method guides, with the mistakes people actually make
Alongside the exercises, I’ve written up 16 of the Pandas methods that come up most often. Each one covers what the method does, the argument forms worth knowing, a worked example on a real dataset you can load from the URL in the code, and the mistakes that catch people — all verified against Pandas 3.
Reading data
- read_csv — the arguments that earn their keep
- read_excel — sheet names, and why the first sheet is usually wrong
Selecting and filtering
- loc — labels, conditions, and why chained assignment silently fails
- iloc — positions, and where they stop matching labels
- pd.col — new in Pandas 3, and where it does not work
Reshaping and transforming
- assign — new columns without mutating anything
- drop — and when
filteris the better tool - sort_values — several keys, opposite directions
- set_index — the index earns its keep
Grouping and aggregating
- groupby — split, apply, combine
- agg — named aggregation, and why it beats the dict form
- pivot_table — and how it differs from
pivot - value_counts — the second thing to run on new data
Dates and times
- to_datetime — and why guessing the format corrupts data quietly
- resample — including the frequency codes nobody remembers
Method chaining
- pipe — how to end a chain in a Plotly chart
If you are upgrading to Pandas 3, two of those are worth reading first. pd.col replaces most of the lambdas in your chains, and resample will break your code outright: 'M', 'Y', 'T', 'H' and 'S' no longer warn, they raise ValueError.
Try one without installing anything
Each method guide links to a matching exercise on the LernerPython practice system, which runs entirely in the browser. No installation, no signup, no account.
Where to start
Pick a method you use constantly and read its page. You will probably recognize one or more of the mistakes, and see how your code can be cleaner, clearer, and more efficient. Or open the archive to an interesting issue, and try the questions before reading the solutions. Peeking at the answers before you’ve tried your hand at solving the problem yourself is harder, but it also teaches you more.
New issues go out every week, and the two-year-old ones keep opening up behind them.
The post Free real-world Pandas exercises, with solutions appeared first on LernerPython.
August 06, 2026
Django Weblog
Call for applicants for a Django Executive Director
The Django Software Foundation is announcing a call for an Executive Director. The Executive Director is the operational leader of the Django Software Foundation, a paid position reporting to the Board of Directors, responsible for setting the Foundation's strategic direction and turning it into action, while managing day-to-day operations. They serve as the main connector between the Board, staff, community, and sponsors.
The Django Software Foundation (DSF) is a 501(c)(3) nonprofit that develops and maintains Django, a free and open-source web application framework. The Foundation exists to support the development of Django by sponsoring sprints, meetups, gatherings and community events; to promote the use of Django among the web development community; to protect the framework's intellectual property and long-term viability; and to advance the state of the art in web development.
This is a new role for the Foundation. Django itself has been around since 2005, but the DSF wasn't founded until 2008, and the person who takes on this role will play a key part in maturing the Foundation's internal structure, helping ensure the DSF can properly support and sustain this important ecosystem going forward. The position is initially for a period of one year, renewable subject to an annual performance evaluation. Depending on the candidate, the role may be part-time or full-time.
Beyond running the Foundation, the Executive Director is a representative of the DSF itself. They embody Django's welcoming culture and help the community sustain the framework's home. The Executive Director is often called upon to represent the Foundation publicly, including at Django conferences and events, and to grow awareness of the DSF as an organization, distinct from the framework it supports.
Responsibilities
Executive Director duties include (but are not limited to):
- Fundraising: leading sponsorship development, corporate and individual membership growth, and partner relationships, including support for the jump from our current 300K USD annual fundraising goal to 500K USD. At the current funding level (around 300K per year), a full-time Executive Director isn't yet sustainable. We'd like to fix that, and we want you to lead that change.
- Admin and operations management: day-to-day operations and administration of the DSF, financial reporting, grant management, and the general running of the organization. Over time, helping grow the DSF into a more mature organization by establishing the operational foundations that support the nonprofit's growth.
- Managing the DSF Assistant and Fellows: overseeing the DSF Assistant and the Django Fellows program, the paid maintainers funded by the DSF.
- Marketing and outreach: community outreach and communications, representing the DSF publicly (for example, conference representation), and growing awareness of the Foundation as distinct from the framework.
- Legal, trademark, and follow-ups: overseeing international trademark policy enforcement, creating, signing, and renewing contracts, handling legal correspondence, and the unglamorous administrative follow-through that keeps a 501(c)(3) compliant. First-hand legal knowledge isn't required here; you'll work with counsel.
- Working group check-ins: regular coordination with the DSF working groups, the volunteer committees handling events, AI, accessibility, fundraising, and more.
- Working with our Django events and conferences like our DjangoCons.
Requirements
An Executive Director is responsible for fundraising, operations, communications, and community coordination. This is a broad remit, and it isn't our expectation that you come into the job an expert in every part of it. We hope you'll have solid experience in a few of these areas, particularly the ones most central to the role (fundraising and partnership development, nonprofit operations, and stakeholder communication). A willingness to learn and a demonstrated history of doing so are more important than comprehensive knowledge.
The areas you can expect to work across include (and are not limited to):
- Fundraising, sponsorship, and partnership development
- Nonprofit operations, financial reporting, and grant management
- Contracts, trademark, and 501(c)(3) compliance (in coordination with counsel)
- Public representation, marketing, and communications
- Coordinating staff, volunteers, and working groups
- Technical knowledge is not required, but is a nice-to-have:
- Knowledge of, or familiarity with, the Django and Python community
- Familiarity with open source licenses and communities
And required professional skills such as:
- Conflict resolution
- Time management and prioritization expertise
- Ability to focus in short periods of time and do substantial context switches
- Self-awareness to recognize their own limits and reach out for help
- Relationship-building and coordination with the Board, staff, working groups, sponsors, and external parties
- Tenacity, patience, compassion and empathy are essential
Therefore, a Django Executive Director requires the skills and judgment of an experienced nonprofit leader who is comfortable with fundraising, operations, and coordination with community stakeholders. Open-source experience and familiarity with the Django or Python community in particular are a big plus.
Being part of the Django community isn't a prerequisite for this position. We'll consider applications from anyone with a proven history of nonprofit leadership or comparable experience in an open-source or mission-driven community, but this is a remote position based in the United States, and unfortunately we are not able to offer visa sponsorship for this role.
The DSF is an equal opportunity employer. We welcome applicants of every background and don't discriminate on the basis of race, color, religion, gender, gender identity or expression, sexual orientation, national origin, disability, age, or veteran status.
How to apply
If you're interested in applying for the position, please submit your application via hiring@djangoproject.com. Your application should include:
- A cover letter (optional)
- A resume or CV
- A brief vision statement (500 to 1000 words) addressing your understanding of the Foundation's current position, the key opportunities and challenges you see for the Foundation, and your approach to the role
References may be requested during the interview process.
The compensation for this role is a base salary of $90,000 to $120,000, plus a bonus of up to $60,000 tied to our progress toward the $500,000 fundraising goal, which we'll tier as we work toward it. Depending on the candidate, the DSF will consider a part-time position and adjust the salary accordingly.
Applicants will be evaluated based on the following criteria:
- Relevant nonprofit leadership and operational experience
- Track record in fundraising and partnership development
- Understanding of the position and of the DSF's current stage
- Clarity, formality, and precision of communications
- Familiarity with open source and/or the Django and Python community
- Strength of reference(s)
Applications will be open until midnight Central Time, September 14, 2026, with the expectation that the successful candidate will start around November 1, 2026 (to be confirmed).
Reference: Announcing the Search for a DSF Executive Director (Django Project blog, June 17, 2026).

