skip to navigation
skip to content

Planet Python

Last update: November 01, 2026 07:48 AM UTC

November 01, 2026


The Python Coding Stack

Notes from the Near Future • The Permanently Curious Stack

The Python Coding Stack today becomes... Notes from the Near Future, The Permanently Curious Stack • Keeping up with the times, or staying just a bit ahead of them

November 01, 2026 12:33 AM UTC

October 31, 2026


The Python Coding Stack

Thoroughly Checked

These figures look surprisingly good.

October 31, 2026 11:46 PM UTC


Jaime Buelta

Software is a discovery process

The development of software is a relatively new discipline, and one that is constantly changing. The last couple of years have been an earthquake with the arrival of AI, but even before that, the field changed beyond recognition from one decade to the next. Still, there’s something that I think is part of its essence, and it hasn’t changed. It has been widely recognised for the last quarter of a century, since the Agile Manifesto. It is borderline impossible to know exactly what functionality a software project needs until it’s done. And... Read More

October 31, 2026 06:13 PM UTC


Seth Michael Larson

Extracting Billy Hatcher “Joycarry Game” ROMs with Python

October 31, 2026 12:00 AM UTC

October 30, 2026


PyCharm

Summary Available fixed versions The issue is fixed in the following versions: The 2025.2.6.* updates for all IDEs now contain only the JBR fix. They will get a separate hotfix for the JCEF problem. Until then, use a workaround to disable JCEF in your IDE (see instructions below). IDE versions 2025.1 and earlier, and Toolbox […]

October 30, 2026 07:42 PM UTC


EuroPython

Humans of EuroPython: Sangarshanan

This week we’d love to thank Sangarshanan, a member of the Communications Team at EuroPython 2026.

October 30, 2026 06:39 PM UTC


Rodrigo Girão Serrão

Python abi3t: what is it?

Learn what the abi3t and the limited C API are and what that means for Python users.

This article explains what the limited C API and the abi3t are to Python developers who don't have experience with C extensions.

15 days of Python 3.15

To celebrate the release of Python 3.15, over the next 15 business days I'll be writing about a new 3.15 feature every day. Explained clearly and with examples so you don't have to sift through the changelog.

Subscribe to receive this free email series:

If you want, you can also check the schedule of the upcoming emails.

Python wheels

A Python wheel is the binary file that you download from PyPI every time you “pip install” a Python package. Conversely, it's also the binary file you build when you're getting ready to publish your project to PyPI so that others can use it.

If you use uv to create a packageable project, cd into it, and then build it, you'll notice that uv builds a Python wheel for you:

% uv init --package pkgtest
Initialized project `pkgtest` at `~/pkgtest`
% cd pkgtest
~/pkgtest % uv build
Building source distribution...
Building wheel from source distribution...
Successfully built dist/pkgtest-0.1.0.tar.gz
Successfully built dist/pkgtest-0.1.0-py3-none-any.whl

The bottom line of output says that uv was successful in building the wheel pkgtest-0.1.0-py3-none-any.whl.

A Python wheel has the extension .whl and the three last segments of the file name are the compatibility tags: {python tag}-{abi tag}-{platform tag}. These compatibility tags determine on what systems and environments the wheel can be installed:

Compatibility tag Meaning
Python tag Python implementation and version required
ABI tag What ABI is required, if any...

October 30, 2026 04:27 PM UTC


PyCharm

Compromised eMudhra Certificates: Update Your JetBrains IDEs, ReSharper, .NET Tools, Toolbox App, and Gateway Now

October 30, 2026 03:24 PM UTC


LernerPython blog, from Reuven Lerner

Pandas MultiIndex: What a two-level index looks like

What does a Python Pandas data frame’s multi-index look like? This:

The post Pandas MultiIndex: What a two-level index looks like appeared first on LernerPython.

October 30, 2026 07:00 AM UTC


"Michael Kennedy's Thoughts on Technology"

Your GitHub language bar is lying (how to fix it)

tl;dr; GitHub’s language bar counts every byte in your repo, including generated docs and test suites. A few lines in .gitattributes tell it what to skip. For my project, that took it from “75% HTML” to roughly 62% Rust and 38% Python, which is what the project really is.


You know that colorful bar on every GitHub repo’s page, the one that says what the project is written in? I just learned you can tell it what to count. On one of my projects, it made a big difference.

Why does GitHub think my Rust project is HTML?

My project chameleon-rs is a Rust re-implementation of the Chameleon template engine, shipped as a Python package (I wrote about it here). Most of the engine is Rust, with a Python layer for the public API and framework integrations. Here’s what GitHub said it was:

HTML? Three quarters of it? There’s barely any hand-written HTML in the whole repo. Yes, it’s a templating engine, but the templates live in the tests and docs.

The documentation site was to blame. I build the docs with Great Docs and commit the output to site/ so it can be served straight from the repo. That’s about 9 MB of generated HTML, JavaScript and CSS, and GitHub counted every byte of it as if I’d written it by hand.

How does GitHub decide what language a repo is?

The bar comes from Linguist, GitHub’s language detection library. Linguist goes over every tracked file, works out its language, and adds up the bytes. More bytes, bigger slice. It already skips some things on its own, like well-known vendor folders, minified files and common generated files. But it has no way to know that your site/ folder is build output.

You tell it with a .gitattributes file at the root of your repo. It’s the same file Git uses for line endings and diff settings, and Linguist reads a few attributes of its own from it:

Each line is a path pattern followed by attributes. Think of a .gitignore line with some settings tacked on the end.

Round one: the docs

Two lines took care of the worst of it:

site/** linguist-generated
tests/upstream/** linguist-vendored

The second line covers Chameleon’s own test suite, which I vendor unmodified to measure compatibility. It’s not my code, so it shouldn’t count either.

After pushing, the bar read 55.9% Python, 43.3% Rust. Much better, but still not right.

Round two: the tests

The engine is mostly Rust, so why was Python winning? I measured the bytes:

Where Size
Python package 420 KB
Python tests 685 KB
Rust, everything 950 KB

The Python tests were bigger than the Python package! It’s nice to have a thorough test suite. But it was making a Rust engine look like a Python project.

The tests are coming out too. To keep things fair, I excluded the Rust tests and benchmarks along with the Python tests, otherwise I’d just be skewing it the other way. Here’s the final file:

# GitHub's language statistics (Linguist) skip these: the Great Docs build output (`make docs`)
# is generated, and Chameleon's test suite is vendored unmodified.
site/** linguist-generated
tests/upstream/** linguist-vendored

# Tests and benchmarks aren't the product: the Python tests outweigh the Python package, so
# counting them makes a mostly-Rust engine read as mostly Python. The Rust ones are left out
# too, to measure both languages alike (inline `#[cfg(test)]` modules still count).
tests/** linguist-detectable=false
crates/*/tests/** linguist-detectable=false
crates/*/benches/** linguist-detectable=false
crates/chameleon-core/src/tests.rs linguist-detectable=false

You can check your patterns before pushing with git check-attr:

git check-attr linguist-generated linguist-detectable -- site/index.html tests/test_api.py

Where did it end up?

The bar should land around 62% Rust and 38% Python. That matches what chameleon-rs is: a Rust engine with a Python API on top. GitHub recalculates a few minutes after you push to the default branch.

That bar is one of the first things people see when they land on your project. If yours is off because of a docs build, vendored code, or a big test suite, it’s a few lines of .gitattributes to fix. I’m off to do the same for some of my other projects now.

October 30, 2026 05:19 AM UTC

October 29, 2026


Rodrigo Girão Serrão

Thread-safe iterators

Learn how to write and use thread-safe iterators in Python 3.15.

This article shows how to write and use iterators in a thread-safe manner using the new tools from Python 3.15:

15 days of Python 3.15

To celebrate the release of Python 3.15, over the next 15 business days I'll be writing about a new 3.15 feature every day. Explained clearly and with examples so you don't have to sift through the changelog.

Subscribe to receive this free email series:

If you want, you can also check the schedule of the upcoming emails.

Iterators are not thread-safe

Most iterators are not thread-safe by default. For example, generator iterators can raise a ValueError if accessed simultaneously from two different threads, as you'll see now.

First, you're going to define a slow generator:

import time

def slow_counter():
    idx = 0
    while True:
        time.sleep(1)
        idx += 1
        yield idx

counter = slow_counter()

The generator counter takes a second to produce each successive number. Then, you're going to write a function that is going to try to consume a value out of the counter in a dedicated thread:

def count_consumer(name, counter):
    try:
        idx = next(counter)
    except ValueError as e:
        print(f"ValueError in {name}: {e}")
    else:
        print(f"{name} got {idx = }")

The function count_consumer will run in two different threads at the same time. The idea is that the second thread will try to fetch a value out of counter while the first thread is still waiting for the first slow result.

By passing count_consumer to two different threads, you'll see that the second thread triggers an exception:

t1 = threading.Thread(target=count_consumer, args=("t1", counter))
t2 = threading.Thread(target=count_consumer, args=("t2", counter))

t1.start()
t2.start()

t1.join()...

October 29, 2026 05:09 PM UTC


Python Software Foundation

Announcing PSF Community Service Award Recipients!

October 29, 2026 09:46 AM UTC


The Python Coding Stack

3. How can words turn into actions?

Agents Unpacked • Chapter 3: A tool request can look right and still ask for the wrong thing.

October 29, 2026 09:27 AM UTC


LernerPython blog, from Reuven Lerner

Pandas xs: Keep the dropped level with drop_level=False

Using xs on a multi-indexed Python Pandas data frame drops the named level — unless you use drop_level=False:

The post Pandas xs: Keep the dropped level with drop_level=False appeared first on LernerPython.

October 29, 2026 07:00 AM UTC


Seth Michael Larson

Bowser’s Fury outshines Sunshine, SA2B, and Billy Hatcher

October 29, 2026 12:00 AM UTC


Armin Ronacher

Custom Voices With Qwen3-TTS

October 29, 2026 12:00 AM UTC

October 28, 2026


Rodrigo Girão Serrão

4 upgrades to the Python standard library in 3.15

Learn about 4 of the most exciting updates made to the Python standard library in 3.15.

This article highlights 4 of the most exciting updates made to the Python standard library in 3.15:

15 days of Python 3.15

To celebrate the release of Python 3.15, over the next 15 business days I'll be writing about a new 3.15 feature every day. Explained clearly and with examples so you don't have to sift through the changelog.

Subscribe to receive this free email series:

If you want, you can also check the schedule of the upcoming emails.

New object_hook in json.load

The function json.load now has a parameter object_hook, which can be set to a callable. When set, JSON objects that are decoded into dictionaries are passed to object_hook and replaced by their result.

For example, suppose you have the following JSON data in a file data.json:

{
  "name": {
    "first": "Rodrigo",
    "last": "Girão Serrão"
  },
  "interests": [
    "Python",
    "Rust",
    "AI"
  ]
}

Define an object hook and use it when loading the data:

import json

def object_hook(d):
    print(f"Inside object_hook with {d = }.")
    return d

with open("data.json", "r") as f:
    data = json.load(f, object_hook=object_hook)

Running this code produces two lines of output:

Inside object_hook with d = {'first': 'Rodrigo', 'last': 'Girão Serrão'}.
Inside object_hook with d = {'name': {'first': 'Rodrigo', 'last': 'Girão...

October 28, 2026 11:10 AM UTC


LernerPython blog, from Reuven Lerner

Pandas xs: Using the `level` argument with an int or a name

Using xs to retrieve from a Python Pandas multi-index? “level” takes an int or string: – int: The index level, with 0 the outermost– string: The level’s name — but […]

The post Pandas xs: Using the `level` argument with an int or a name appeared first on LernerPython.

October 28, 2026 07:00 AM UTC


Python GUIs

Passing Arguments to Threaded Functions in PyQt6 — Understanding the difference between passing a function and calling a function when using QThreadPool and QRunnable

I'm running a long-running function in a separate thread using QThreadPool and a QRunnable worker. When I pass the function without arguments, it runs correctly in the background. But when I try to pass arguments, the GUI freezes until the function finishes. What am I doing wrong?

October 28, 2026 06:00 AM UTC

Getting a Value from a Model Using Row-Column Indices — How to retrieve data from a Qt model using row and column integers

How do I retrieve a value from a QAbstractTableModel using a pair of integers (row and column), for example when clicking a button, rather than clicking on a cell in a QTableView?

October 28, 2026 06:00 AM UTC


"Michael Kennedy's Thoughts on Technology"

Chameleon templates, modernized: chameleon-rs

Chameleon templates, modernized: chameleon-rs

tl;dr; chameleon-rs is my Rust implementation of Chameleon for Python web apps. It runs existing templates unchanged, with faster rendering, async support, and support for free-threaded Python.

The Chameleon HTML (template) language is one of my favorites. It was the default in Pyramid when I used to use that framework for my projects. I’ve since created fastapi-chameleon and chameleon-flask to allow you to easily use Chameleon on these more modern web frameworks.

Why modernize Chameleon for free-threaded Python?

But the world has changed. With official support for free-threaded Python in Python 3.14 and continued evolution in 3.15, we need to start expecting more from our web apps. Is the Chameleon package updated for multi-threaded Python? Not at all. In fact, the repository shows support only up to Python 3.13 and a failing build. It’s not keeping up with the times.

And there are many more features that belong in modern templating languages for Python apps. Partial template support for excellent libraries like HTMX. Faster performance. Markdown support. And much more.

I wanted more performance, parallelism, and modern features from all the major templating languages like Chameleon, Jinja2, or Django templates. Amazingly, they have all failed to deliver. That changes today (at least for Chameleon, for now).

What is chameleon-rs?

I reimplemented the Chameleon template language almost 100% in Rust. The core goals were compatibility, performance, modernization, and thread safety.

Existing templates and code run unchanged, with byte-identical output, several times faster. It also adds what a modern Python web app expects:

No runtime dependencies.

How much faster is chameleon-rs?

Measured result: In my web app, switching to chameleon-rs cut median response times by roughly 20% across the site.

chameleon-rs compiles a template to a native program and runs it in Rust. The template’s own work (the loops, conditions, escaping, macro and slot calls, partials and output assembly) happens without Python bytecode. Python only runs where the template calls into your code: an attribute lookup, a method call, a helper.

You can see our synthetic benchmarks on the performance page of the docs. But the graph below is much more convincing. It shows the median response time when running on traditional Chameleon (the package) or chameleon-rs.

Median response times with Chameleon and chameleon-rs

This is not “templates render 20% faster”. This is the entire web app, with user authentication, significant database work, logging, ecommerce, etc., seeing roughly 20% lower response times by using chameleon-rs!

How do I switch from Chameleon to chameleon-rs?

chameleon-rs runs code written for Chameleon unchanged. There are two ways to switch: install the compatibility alias and change nothing else, or change your imports.

One line: compat.install()

Add this at the very top of your app’s entry point (the module your server imports, or __main__), before anything imports Chameleon:

import chameleon_rs.compat
chameleon_rs.compat.install()

Or adopt more of the API and change the imports. Every public name is available from chameleon_rs directly:

# Before
from chameleon import PageTemplate, PageTemplateFile, PageTemplateLoader
import chameleon_partials
import chameleon_flask

# After
from chameleon_rs import PageTemplate, PageTemplateFile, PageTemplateLoader
import chameleon_rs.partials as chameleon_partials
import chameleon_rs.flask as chameleon_flask

What modern features does chameleon-rs add?

Performance itself is a feature. Compatibility is a feature. But why stop there? I added partials support, Markdown support, and asyncio support directly inside the template, as well as automatic event loop support for any async def view function. That’s the modernization part. And it was achieved by making chameleon-rs a superset of what Chameleon offered before, not by changing it.

Built-in template partials

A partial is a template file rendered inside another template, with its own small model. Use them for anything you repeat: a card in a listing, a form field, a table row, a fragment you also return on its own to HTMX (see HTMX).

chameleon-rs has partials built in. Every template can call render_partial, with nothing to register or pass in:

<div tal:repeat="video videos">
  ${render_partial('shared/partials/video_card.pt', video=video, show_date=True)}
</div>

Markdown inside Chameleon templates

chameleon-rs can convert Markdown to HTML while it renders, with a markdown: expression:

<article>${markdown: episode.notes}</article>
<div tal:content="markdown: comment.text"></div>

It’s opt-in: Chameleon has nothing like it, so you turn it on with configure() and use it in new templates. The converter is built into chameleon-rs (comrak, in Rust), so there’s no Markdown package to install. And because it entirely happens in Rust, it’s roughly 26x faster than mistune with the same extensions.

Turning it on

import chameleon_rs
from chameleon_rs import PageTemplate

chameleon_rs.configure(markdown=True)

page = PageTemplate('<article>${markdown: notes}</article>')
print(page(notes='# Show notes\n\nWe talked about **templates** and `Rust`.'))

Async rendering with render_async()

Chameleon renders synchronously. In an async web app (FastAPI, Starlette, Quart, Litestar, Robyn) that means a render runs on the event loop, and nothing else in that worker runs until it finishes. chameleon-rs keeps render() exactly as it was and adds render_async(), which renders on the event loop without holding it while it waits for data.

The framework integrations use it for you: an async def view decorated with @template renders with render_async. See the async rendering guide for the details and opt-in extras.

Thread safety for free-threaded Python

chameleon-rs has been tested extensively to be thread-safe. I have repeatedly profiled and optimized it to share as little data and take as few locks as possible. So when you’re ready to move to a truly multi-threaded (no GIL) web framework, your template library won’t hold you back.

How can I test chameleon-rs before switching?

If this sounds interesting, you can use shadow mode to try it out. Shadow mode renders a sample of real traffic with both engines (existing Chameleon and chameleon-rs) and reports any difference, while Chameleon keeps serving. Your users won’t see a difference, and you can check that the sampled requests get exactly the same output.

October 28, 2026 03:01 AM UTC

October 27, 2026


Talk Python to Me

#567: Personal AI Agents for Pythonistas

The most popular Python project you've probably never heard of has 252,000 GitHub stars and merged over 1,800 pull requests in a single week. It's Hermes, a self-hosted AI agent you program just by talking to it. And it never clocks out. It runs 24/7 on a box in your house, wakes up every 15 minutes, keeps your YouTube channel in sync, and has your morning briefing ready before you're up. Mine started as a few config files. A few months later, it's 16,000 files of skills, memories, and tools it built for itself, all while I wasn't watching. Jeff Triplett joins me to explore this honestly weird but kind of amazing world. And yes, Hermes has a few words for you, too.

October 27, 2026 10:27 PM UTC


PyCoder’s Weekly

Issue #758: Security, Optimization, itertools, and More (2026-10-27)

October 27, 2026 07:30 PM UTC


Python Bytes

#502 Introducing chameleon-rs

Topics include , chameleon-rs, Bringing uv and ty together, and Stlite runs Streamlit apps entirely in the browser.

October 27, 2026 05:39 PM UTC


Rodrigo Girão Serrão

Typing improvements in Python 3.15

This article goes over the major improvements made to the typing module in Python 3.15.

Since static type hinting is becoming more and more popular in the Python world, every Python release brings with it major improvements to the world of typing. This article goes over the main improvements made to typing in Python 3.15 like the improvements to TypedDict, the addition of typing.TypeForm, or the addition of the decorator typing.disjoint_base.

15 days of Python 3.15

To celebrate the release of Python 3.15, over the next 15 business days I'll be writing about a new 3.15 feature every day. Explained clearly and with examples so you don't have to sift through the changelog.

Subscribe to receive this free email series:

If you want, you can also check the schedule of the upcoming emails.

New class parameters for TypedDict

Python 3.15 added two class parameters to TypedDict: extra_items and closed. The gist of it is that extra_items allows specifying that your dictionary may have other, unknown keys whose values have the specified type:

from typing import TypedDict

class Movie(TypedDict, extra_items=str):
    title: str
    year: int

The definition above means that a variable of type Movie might have other keys, as long as their values are assignable to the type str:

movie: Movie = {
    "title": "Forrest Gump",
    "year": 1994,
    "genre": "Comedy / Romance",
}

The extra item "genre": "Comedy / Romance" is allowed because the value is a string and extra_items=str.

To completely disallow extra items, regardless of their type, you can set closed=True as a class parameter.

There's an article dedicated to the improvements made to TypedDict in Python 3.15 that...

October 27, 2026 10:54 AM UTC