Planet Python
Last update: September 16, 2026 01:48 PM UTC
September 16, 2026
Django Weblog
Executive Director Search Extended to September 22
We are extending the search for the Django Software Foundation's first Executive Director. Applications now close at the end of Tuesday, September 22, 2026, anywhere on Earth (AoE). As long as it is still September 22 somewhere, your application counts.
We are happy with the applications we received. We also knew several people had applications in progress, and it took a while for word about the role to reach everyone it should have. Giving people a little more time felt like the fairest option. If you were most of the way there, you have until Tuesday.
If you have already applied, this does not slow anything down for you. We are reviewing applications as they come in, and we will reach out to candidates and schedule interviews on the same timeline we planned.
The Executive Director will play a central role in helping the DSF grow its capacity and build a sustainable future for the Django project and its community. That includes leading fundraising and partnerships, supporting the Foundation's operations and programs, and working closely with the board, staff, volunteers, and the wider Django community.
We are looking for an experienced nonprofit leader who is comfortable taking on a broad role and building things as they go. Fundraising is central to this job. We need someone who is at ease sitting down with companies, making the case for Django, and connecting what the DSF does to what those organizations care about. Much of the Foundation's future depends on growing those relationships and turning them into steady support.
You don't need to be a Django or Python expert, and you don't need prior involvement in the Django community. Experience with open source or other community-driven organizations is welcome, but we are primarily looking for someone with strong fundraising instincts, plus leadership, communication, relationship-building, and organizational skills.
Could that person be you?
If you have been waiting for the right opportunity to step forward, we encourage you to review the role and submit your application.
And if you know someone who would be a strong fit, please share this with them. A great candidate may be just one introduction away.
See the full job description and application details.
If you are ready to help shape the next chapter of the Django Software Foundation, we want to hear from you. Applications close on September 22, 2026, anywhere on Earth.
September 16, 2026 12:50 PM UTC
Speed Matters
fastlogging-rs
fastlogging-rs: High-Performance Logging for many different Programming Languages
Logging is often the hidden bottleneck in your application.
Every log.info(...) call can block your hot path, serialize your threads, and slow down your I/O-bound workloads.
That’s why I created fastlogging-rs: a Rust-powered logging framework that is extremely fast, thread-safe,
and available with a similar API in 8 different programming languages.
My first release, 0.8.1, is available with the following features:
- Initial release of the Rust core (
fastloggingcrate) with bindings for Python, C, C++, Go, Java (FFM and JNI) and C# - Non-blocking, asynchronous logging — writers run in background threads
- Optional file rotation and compression
- Optional AES encryption for network logging
- Configuration via API or configuration file (JSON, XML, YAML)
- Automatic forwarding of log messages from sub processes to the main process
Why fastlogging-rs?
Because speed matters…
🚀 Significant Performance Improvements
Compared to Python’s built-in logging module:
- Writing to a file is up to 147× faster
- Rotating file logging is up to 207× faster
- Even compared to Apache Log4j, fastlogging-rs is up to 9× faster
When your application logs millions of messages, these speedups can turn minutes into seconds.
Benchmarks results for writing to a file
🌍 One Framework, 8 Languages
fastlogging-rs is written in Rust and comes with thin wrappers for your favorite programming language. All bindings share a similar API,
so you can use the same logging concepts across your whole stack:
| Language | Binding | Layer |
|---|---|---|
| Rust | fastlogging |
Native core |
| Python | pyfastlogging |
pyo3 (>= 3.10) |
| C | cfastlogging |
FFI (cbindgen header) |
| C++ | cxxfastlogging |
type-safe cxx bridge |
| C++ | cppfastlogging |
C++17 RAII over the C ABI |
| Go | gofastlogging |
cgo wrapper |
| Java | jfastlogging-ffm |
Foreign Function & Memory API |
| Java | jfastlogging-jni |
Java Native Interface |
| C# | csharpfastlogging |
P/Invoke |
⚡ Non-Blocking Architecture
Logging calls are non-blocking: each call performs a cheap level check (a single integer comparison, no lock)
and hands the message to a channel. A background LoggingThread drains that channel and dispatches to each
writer’s own thread. The speed of your writers never slows down your application — as long as the queue
doesn’t run full.
🔧 Rich Feature Set
- Thread-safe logging calls
- Multiple writers (sinks) per logger: console, file, network, syslog, callback
- Optional file rotation and compression
- Optional AES encryption for network logging
- Configuration via API or configuration file (JSON, XML, YAML)
- Automatic forwarding of log messages from sub processes to the main process
Installation
Rust
cargo add fastlogging
Python
pip install pyfastlogging
Usage Examples
Rust
use fastlogging::{logging_new_default, LoggingError};
fn main() -> Result<(), LoggingError> {
let mut log = logging_new_default()?;
log.info("Hello, fastlogging!")?;
log.shutdown(false)?;
Ok(())
}
Python
from fastlogging import Logging
log = Logging()
log.info("Hello, fastlogging!")
log.shutdown(False)
Python with a colored console writer
from pyfastlogging import TRACE, Logging, ConsoleWriterConfig
logger = Logging(
TRACE,
"main",
[ConsoleWriterConfig(TRACE, True)],
)
logger.trace("Trace Message")
logger.debug("Debug Message")
logger.info("Info Message")
logger.shutdown()
Benchmark Results
Writing to a file
| Framework | Time |
|---|---|
| Python logging | 29.37s |
| log4j | 1.48s |
| fastlogging-rs | 0.2s |
Rotating file logging
| Framework | Time |
|---|---|
| Python logging | 35.24s |
| log4j | 1.56s |
| fastlogging-rs | 0.17s |
For detailed benchmark data and methodology, see the benchmark documentation:
https://github.com/brmmm3/fastlogging-rs/blob/master/docs/benchmarks/index.html
You can also explore the full benchmark results with interactive charts and tables:
https://brmmm3.github.io/fastlogging-rs/
Get Started
If your application spends time logging, fastlogging-rs can provide substantial performance improvements with minimal code changes — in whichever language you happen to be writing.
The API is intentionally familiar, making migration from logging, log4j, or your current framework
straightforward while unlocking significantly faster execution.
Source code, documentation, and issue tracker:
https://github.com/brmmm3/fastlogging-rs
Licensed under the MIT or Apache-2.0 License.
September 16, 2026 07:40 AM UTC
September 15, 2026
PyCoder’s Weekly
Issue #752: Dict Performance, Hypothesis, Lazy Imports, and More (2026-09-15)
#752 – SEPTEMBER 15, 2026
View in Browser »
Sets and Dictionaries Can Have Quadratic-Time Performance
A rough first approximation is that a dict has O(1) performance, but that only holds true for smaller containers. This article explores the performance limits of sets and dictionaries.
DANIEL LEMIRE
Stop Writing Edge Case Tests. Use Hypothesis Instead
Introduction to property-based testing in Python with Hypothesis. Move from ‘what input should I test?’ to ‘what invariant should always hold?’
PEYTON GREEN • Shared by Anonymous
Tired of Getting Blocked While Scraping the Web?
ScrapingBee handles proxies, browsers, anti-bot systems, and retries so you can focus on your data. Get clean Markdown, JSON, or HTML from the web with up to a 99,9% success rate. ScrapingBee is SOC 2 Type II and GDPR compliant, and trusted by 4,000+ developers. Try ScrapingBee With 1,000 Free Credits
SCRAPINGBEE sponsor
Python 3.15 Preview: Lazy Imports
Learn how Python 3.15 lazy imports work, how deferring heavy modules cuts your app’s startup time, and which imports still have to stay eager.
REAL PYTHON
Articles & Tutorials
Nifty Django Feature: Q() Objects
Django’s ORM includes the filter() method for returning a subset of rows in the database. Anything you can do with filter() you can do with a Q() object, which encapsulates the filter’s arguments. Since it is an object you can dynamically create and manage filters in your code.
TIM SCHILLING
Profile on Guido van Rossum
The BBN Times has done a profile piece on Python’s creator Guido van Rossum. It covers his background, the creation of Python, and how he helped shepherd the language to its current state.
FELIX YIM
Making a Python Interpreter in 1024 Bytes
Austin challenged himself to make a tiny subset of Python in C. It isn’t quite Python, but bares a resemblance and with a little code golf he built something quite small.
AUSTIN Z. HENLEY
Reading __dict__ Once Deoptimizes Attribute Access
The usual explanation for why hoisting attributes out of a loop is faster has been wrong since CPython 3.11. Read about what more recent interpreters do.
TIMOFEI IVANKOV • Shared by Timofei Ivankov
An Effective Python Development Environment
Choose a Python development environment that helps you get coding. Find tutorials and courses on editors, uv, virtual environments, and useful tools.
REAL PYTHON
How Hard Is It to Find a Remote Python Data Job?
Piotr analyzed 88,975 Hacker News job posts from 2012 to 2026. The board shrank, remote work peaked, pay became clearer, and senior roles took over.
PIOTR PŁOŃSKI
Python Timer Functions
Learn how to time your Python code with the time module, then build a reusable Timer class that works as a context manager.
REAL PYTHON course
Prototyping a New CLI for Wagtail
Wagtail 8 introduced a new API which has allowed devs to create a command line tool for interacting with a Wagtail CMS.
THIBAUD COLAS
Teaching NumPy’s ufuncs New Tricks
Iason recently did an internship working on NumPy internals. This post talks about what he accomplished.
IASON KROMMYDAS
Projects & Code
Plotext 6: Plot Data, Images and Video in the Terminal
GITHUB.COM/PICCOLOMO • Shared by Savino Piccolomo
dbmask: Discover & Mask Sensitive Data in Databases
GITHUB.COM/SEALANDSEACAT • Shared by Siyuan Feng
Events
Weekly Real Python Office Hours Q&A (Virtual)
September 16, 2026
REALPYTHON.COM
PyCon Cameroon 2026
September 17 to September 20, 2026
PYTHONCAMEROON.ORG
PyData Bristol Meetup
September 17, 2026
MEETUP.COM
Python Leiden User Group
September 17, 2026
PYTHONLEIDEN.NL
PyLadies Dublin
September 17, 2026
PYLADIES.COM
Happy Pythoning!
This was PyCoder’s Weekly Issue #752.
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 ]
September 15, 2026 07:30 PM UTC
Python Bytes
#496 A lake house in Seattle
<strong>Topics covered in this episode:</strong><br> <ul> <li><strong><a href="https://eddie.codes/posts/pandas-should-go-extinct/?featured_on=pythonbytes">Pandas Should Go Extinct</a></strong></li> <li><strong><a href="https://github.com/tylerh111/pydantic-pint?featured_on=pythonbytes">Pydantic-pint puts real-world units in your Pydantic models</a></strong></li> <li><strong><a href="https://belderbos.dev/blog/how-libraries-run-rust-inside-python/?featured_on=pythonbytes">How Libraries Run Rust Inside Python (With PyO3)</a></strong></li> <li><strong><a href="https://aws.amazon.com/blogs/big-data/aws-and-ducklabs-building-the-future-of-analytics-together/?featured_on=pythonbytes">AWS acquires DuckLabs</a></strong></li> <li><strong>Extras</strong></li> <li><strong>Joke</strong></li> </ul><a href='https://www.youtube.com/watch?v=uK_gohaLkbM' style='font-weight: bold;'data-umami-event="Livestream-Past" data-umami-event-episode="496">Watch on YouTube</a><br> <p>Sponsored by <strong>Logfire from Pydantic</strong>: <a href="https://pythonbytes.fm/logfire">pythonbytes.fm/logfire</a></p> <p><strong>Connect with the hosts</strong></p> <ul> <li>Michael: <a href="https://fosstodon.org/@mkennedy">Mastodon</a> / <a href="https://bsky.app/profile/mkennedy.codes?featured_on=pythonbytes">BlueSky</a> / <a href="https://x.com/mkennedy?featured_on=pythonbytes">X</a> / <a href="https://www.linkedin.com/in/mkennedy/?featured_on=pythonbytes">LinkedIn</a></li> <li>Calvin: <a href="https://sixfeetup.social/@calvin?featured_on=pythonbytes">Mastodon</a> / <a href="https://bsky.app/profile/calvinhp.com?featured_on=pythonbytes">BlueSky</a> / <a href="https://x.com/calvinhp?featured_on=pythonbytes">X</a> / <a href="https://www.linkedin.com/in/calvinhp/?featured_on=pythonbytes">LinkedIn</a></li> <li>Show: <a href="https://fosstodon.org/@pythonbytes">Mastodon</a> / <a href="https://bsky.app/profile/pythonbytes.fm">BlueSky</a> / <a href="https://x.com/PythonBytes?featured_on=pythonbytes">X</a></li> </ul> <p>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.</p> <p>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.</p> <p><strong>Calvin #1: <a href="https://eddie.codes/posts/pandas-should-go-extinct/?featured_on=pythonbytes">Pandas Should Go Extinct</a></strong></p> <ul> <li>Pandas' slowness pushes teams toward "Big Data" tools (Spark, Databricks) they don't actually need — most workloads never hit true Big Data scale</li> <li>Amazon Redshift telemetry: ~95% of tables are under 100GB, ~87% of queries touch 80GB or less — that's "Medium Data," not Big Data</li> <li>Polars and DuckDB fill that gap: single-machine, fast, no cluster required</li> <li>1 Billion Row Challenge benchmark: Pandas took 4m28s vs. Polars 5.04s and DuckDB 5.19s — DuckDB also used 19x less memory</li> <li>On a real-world NYC taxi dataset (3GB parquet), pure DuckDB ran 2x faster than pure Pandas while using a fraction of the RAM</li> <li>Bonus: Apache Arrow lets you pass data between Pandas/Polars/DuckDB with zero copying, so trying them out doesn't mean a full rewrite</li> </ul> <p><strong>Michael #2: <a href="https://github.com/tylerh111/pydantic-pint?featured_on=pythonbytes">Pydantic-pint puts real-world units in your Pydantic models</a></strong></p> <p>Pydantic-pint bridges Pydantic and Pint so models can validate physical quantities like 4m or 12 meters instead of bare floats. Fields annotated with PydanticPintQuantity parse user input, convert between compatible units, and serialize quantities back out as strings. That closes a real gap for anything consuming API payloads, config files, or sensor data with measurements, letting you enforce units at the validation boundary instead of hoping every caller remembered them.</p> <ul> <li>via PyCoder's Weekly newsletter</li> <li>Unit mix-ups have literally crashed spacecraft; now your Pydantic models can refuse them at the door.</li> <li>Annotate a field as Annotated[Quantity, PydanticPintQuantity('km')] and inputs like 12 meters arrive auto-converted to kilometers</li> <li>Validation covers string, numeric, and quantity inputs, and model_dump_json serializes quantities as readable unit strings</li> <li>Installable from PyPI as pydantic-pint, MIT licensed, with docs at <a href="http://pydantic-pint.readthedocs.io/?featured_on=pythonbytes">pydantic-pint.readthedocs.io</a></li> <li>Early-stage solo project at version 0.4, so API stability and maintenance are open questions worth discussing</li> </ul> <p><strong>Calvin #3: <a href="https://belderbos.dev/blog/how-libraries-run-rust-inside-python/?featured_on=pythonbytes">How Libraries Run Rust Inside Python (With PyO3)</a></strong></p> <ul> <li>Pydantic v2's validation core (pydantic-core) is Rust under the hood, built with PyO3 — this post shows how that bridge actually works via a small hand-built JSON parser</li> <li>Four steps to get Rust into Python: write a normal Rust module, annotate with PyO3 macros (#[pyfunction], #[pymodule]), compile/install with maturin, then just import it</li> <li>The parser builds a Rust tree first — Python never touches it until the boundary crossing</li> <li>Key insight: converting the Rust result into Python objects (.into_pyobject) is often the expensive part, not the parsing — 100,000 JSON values means ~100,000 Python objects built after parsing's already done</li> <li>Errors cross the boundary too: Rust's typed errors convert into real Python exceptions (ValueError, FileNotFoundError) via From/?, so callers get clean Python semantics</li> <li>Takeaway for anyone porting Rust in: if you're returning a scalar, don't sweat it; if you're returning a big structure, profile the boundary — that's the real cost, not the algorithm</li> </ul> <p><strong>Michael #4:</strong> <a href="https://aws.amazon.com/blogs/big-data/aws-and-ducklabs-building-the-future-of-analytics-together/?featured_on=pythonbytes">AWS acquires DuckLabs</a></p> <p>Thank you Dylan McConnell.</p> <p>What does this mean for the DuckDB ecosystem?</p> <p><strong>DuckDB</strong> is the open-source in-process analytical SQL engine. MIT licensed. The IP is not owned by any company - it's held by the nonprofit DuckDB Foundation, which was created when the team spun out of CWI Amsterdam. Peter Boncz, the CWI representative on the Foundation board, describes it as the entity that holds all IP of open-source DuckDB.</p> <p><strong>DuckLabs</strong> (<a href="http://ducklabs.com?featured_on=pythonbytes">ducklabs.com</a>) is the company, formerly branded DuckDB Labs. Founded a little over five years ago by Hannes Mühleisen and Mark Raasveldt to give the DuckDB team a stable long-term home, bootstrapped deliberately instead of taking VC, grown to 30+ people in Amsterdam, funded by support and feature-prioritization contracts. It employs the core devs. It does not own DuckDB.</p> <p><strong>DuckLake</strong> is one of three projects DuckLabs builds, what they call the Duck Stack: DuckDB, DuckLake, and Quack. DuckLake is the lakehouse format that puts catalog metadata in a SQL database instead of in files on object storage. Quack is newer - an RPC-style protocol that turns DuckDB into a client-server system where both ends are DuckDB instances, slated to stabilize in DuckDB v2.0 in September 2026.</p> <p><strong>MotherDuck</strong> is a separate Seattle company, Jordan Tigani's, selling serverless hosted DuckDB. It was started in partnership with DuckDB Labs and has worked closely with Hannes and Mark for four years. It contracted DuckLabs for engineering work and contributes heavily upstream - three of its engineers are among the top 10 outside contributors to DuckDB. It also sells its own DuckLake offering. Customer and collaborator, never owner.</p> <p><strong>What the AWS post changes.</strong> Amazon bought the company, not the project. DuckLabs joined AWS effective September 1, with the process concluding August 31, 2026. Hannes and Mark keep leading the team and the project's technical direction, the team stays in Amsterdam, and DuckDB stays MIT under the Foundation. AWS gets the people and a direct line to the roadmap. The license protects your code, not your priorities.</p> <p><strong>Three second-order effects worth tracking</strong>:</p> <p>The Foundation board is the real question. It has three directors: Mühleisen, Raasveldt, and Boncz. Two now work for AWS. Commentary on the deal has focused on exactly this - the license protects the code, not the roadmap. The announced counterweight is governance: a technical advisory board on the Foundation, and opening the extension stack so extensions signed by other developers can run in DuckDB.</p> <p>MotherDuck immediately moved into the business DuckLabs vacated. It now sells DuckDB enterprise support, which it had avoided because it didn't want to compete with DuckLabs' business model, and says it has explicit blessing from Hannes and Mark now that they're joining Amazon. It also bought Tower.dev the day before the AWS announcement.</p> <p>Everyone expects an AWS DuckDB service. Tigani says Amazon will likely release one eventually, and welcomes the competition, citing Redshift's failure to slow Snowflake on AWS. The groundwork is already visible: Amazon Quick uses DuckDB to query S3 Tables and has processed over 2.5B queries with it since launching in October 2025.</p> <p>The DuckLake angle is the one to watch. AWS is heavily committed to Iceberg through S3 Tables, and it just acquired the team behind a competing lakehouse format. The stated plan is to use DuckDB, DuckLake, and Quack together to power a new generation of data services, but which format wins internal priority is unannounced.</p> <p><strong>Extras</strong></p> <p>Calvin:</p> <ul> <li><strong>astral-sh/uv 0.12.12: code-signed release binaries</strong> 🥳</li> </ul> <p>Michael:</p> <ul> <li><a href="https://forums.macrumors.com/threads/apple-releases-firmware-update-for-140w-usb-c-power-adapter.2488672/?featured_on=pythonbytes">My MacBook power supply rebooted to install updates</a> (?!?)</li> <li><a href="https://www.youtube.com/watch?v=kHL3XzjpT5w">The Story of VS Code | Official Documentary</a></li> <li><a href="https://aws.amazon.com/blogs/big-data/aws-and-ducklabs-building-the-future-of-analytics-together/?featured_on=pythonbytes">Amazon/AWS acquires DuckLabs</a> (see recent episode on DuckLake)</li> </ul> <p><strong>Joke: <a href="https://x.com/PR0GRAMMERHUM0R/status/2090076348114985385?featured_on=pythonbytes">We’re agentic now</a></strong></p>
September 15, 2026 03:24 PM UTC
Django Weblog
DjangoCon Europe 2027 is heading to Innsbruck, Austria! 🏔️⛷️🚠🇦🇹
We’re delighted to announce that DjangoCon Europe 2027 will take place in Innsbruck, Austria, from February 17–21, 2027!

Photo by Nicole Baster on Unsplash
Each year, DjangoCon Europe brings together people from across the Django community to learn, share ideas, contribute, and spend time together. In 2027, that community will come together in Innsbruck for five days of Django, Python, and community.
DjangoCon Europe is organized by community volunteers and has long been one of the highlights of the Django community calendar. Developers, contributors, newcomers, and long-time community members from around the world come together to exchange knowledge, make new connections, and help shape the future of Django.
Save the dates
📅 February 17–21, 2027
📍 Innsbruck, Austria
And there’s even more good news: the Call for Proposals is open, and tickets are now on sale!
Submit a proposal
Have something you’d like to share with the Django community? The Call for Proposals is open.
Whether you have a deep technical topic, a lesson you’ve learned from building with Django, an idea that could benefit the community, or something completely unexpected, we’d love to hear from you.
Get your ticket
Ready to join us in Innsbruck?
Tickets for DjangoCon Europe 2027 are now on sale.
Come spend five days learning, sharing, meeting fellow Djangonauts, and enjoying everything the Django community has to offer.
Volunteer at DjangoCon Europe
DjangoCon Europe is a community-run conference, and volunteers play an important part in making it happen.
If you’d like to help us make DjangoCon Europe 2027 a great experience for everyone, sign up to volunteer. Whether you’re a long-time member of the community or attending your first DjangoCon, we’d love to have you involved.
Sponsor DjangoCon Europe
Support the conference financially and gain visibility in the Django community.
Learn more about sponsorship →
Download the sponsorship brochure →
There’s plenty more to come as we get closer to the conference. Keep an eye on the DjangoCon Europe 2027 website for the latest news and updates:
Visit the DjangoCon Europe 2027 website →
See you in Innsbruck in 2027! 🇦🇹
September 15, 2026 01:20 PM UTC
Python Software Foundation
Announcing the PSF Strategic Plan 2026
In May, the Python Software Foundation (PSF) shared the high-level goals of our Strategic Plan. In June, we published the full draft and opened it for community feedback. Today we are sharing the outcome: at its July 8 meeting, the PSF Board adopted the PSF Strategic Plan 2026, covering 2026 to 2031.
Our new PSF Strategy page is the permanent home for the plan, with information about how the board sets priorities and how you can share feedback. We’ll publish annual review findings and future updates there so the community can follow our progress and any changes to the plan.
What the feedback changed
Feedback from PSF Staff and the Python community shaped real changes to the draft we published in June.
Staff feedback added a Security Baseline objective for all PSF projects and services and a Vulnerability Management objective in response to the rapid growth in security reports, and it sharpened wording across the organizational goals. Community feedback shaped the plan, too: translation and localization are now part of the accessibility work, companies that want to fund specific work get clearer pathways, and the ideas under financial sustainability now include growing the value of PSF membership.
The plan's "How This Plan Was Shaped" section documents the process, and we are grateful to everyone who took the time to share their perspectives.
The annual review
A five-year plan is only useful if it stays current. Alongside adoption, the board established an annual review of the strategic plan. Each year, the board will:
Assess progress against the goals
Evaluate whether priorities need to shift
Incorporate feedback from PSF staff and the community
Publish a summary of findings and any changes made
The plan is designed as a living document, and we will be sharing details on the first review cycle.
What happens next
Implementation of the Strategic Plan is the job of PSF staff, and it has already started. The 2026 Grants Program Funding Round announced in July is the first concrete step under the plan's grants reform direction, and awards will be announced later this month.
We also know the community is waiting for updated financial information. The PSF has engaged an external accounting firm to support this work, and we will share an update as soon as the numbers are ready for publication.
The plan now informs how the PSF allocates its budget and staff time, and there are several ways to stay involved. We welcome community feedback at strategy@python.org year-round, and the monthly PSF Board Office Hours on the PSF Discord are a good place to connect with the board and to ask questions about the plan. Input arriving now feeds the first annual review.
Thank you to everyone who read the draft, contributed feedback, joined the office hours, and talked with us at PyCon US. The PSF’s Strategic Plan is better for it.
Jannis Leidel, PSF Board Chair, on behalf of the PSF Board of Directors
September 15, 2026 01:15 PM UTC
LernerPython blog, from Reuven Lerner
Python += calls __add__ and rebinds to a new object
If you invoke +=, Python can use __add__.
MyClass implements __add__ (calling print for debugging):
m1 = MyClass(10)
m2 = MyClass(20)
m1 += m2 # prints "Now in MyClass.__add__"
m1 now refers to a new object, and its repr is:
MyClass instance, vars(self)={‘x’: 30}
The post Python += calls __add__ and rebinds to a new object appeared first on LernerPython.
September 15, 2026 06:00 AM UTC
September 14, 2026
LernerPython blog, from Reuven Lerner
Python in operator: How __contains__ speeds up membership tests
How does the “in” operator work in Python?
– If an object defines __contains__, then its (boolean) result is returned (coerced to bool).
– If not, then Python iterates over it with __iter__
Can you find and return a result faster than __iter__? Then define __contains__.
The post Python in operator: How __contains__ speeds up membership tests appeared first on LernerPython.
September 14, 2026 06:00 AM UTC
Graham Dumpleton
Hands-on learning in the age of AI
At PyCon AU 2026 I gave a talk in the DevRel track titled "Hands-on learning in the age of AI", with the subtitle "Are developer workshops still relevant?". The video is now up on YouTube if you want to watch it. What follows is the main points I was trying to make, along with something the talk didn't cover.
That something is that a couple of weeks or so after giving the talk, I released 24 free workshops for wrapture which anyone can run in their browser. If you watch the talk you will notice I spent the last part of it explaining why that sort of workshop is the kind most under threat from AI. I don't think the two are in conflict in this case, but it takes some explaining, so I will get to it at the end.
For most of the time I have worked on mod_wsgi and wrapt, the bulk of my effort didn't go into writing code. It went into answering the same questions over and over on mailing lists, Stack Overflow and GitHub issues. Every one of those now gets answered in seconds by an AI, and a lot of the answers it gives are probably mine, since it has read everything I ever wrote. The loss of that contact with the people using what you build is a separate topic though, and not what the talk was about. I touched on it in Developer Advocacy in 2026.
Where AI does a better job
Before making any case for workshops, I wanted to be straight about the things AI simply does better than we do, because some of us are still putting effort into content where our time would be better spent elsewhere.
The tutorial that walks you through configuring something, the getting-started guide, the blog post that takes you through a setup step by step. I don't think we should be writing those any more. An AI will give you the version for the release you are actually running, on your operating system, in the context of your own project, at two in the morning when there is nobody around to ask. Reference documentation still needs to exist, as that is what the AI learned from, but the walkthrough where the reader is a passive observer is done.
The thing is, explaining something clearly was never the hard part. Think about the last tutorial you read that was genuinely well written. Did you come away able to do it, or just confident that you could? Unless you sat down and worked through it, probably the second. Getting someone to actually do the thing, keeping them going when it breaks, and knowing which bit will confuse them before it does, that was always the hard part. AI has got dramatically better at the explaining, but that was the half we had already worked out.
Why doing is different from reading
So what does the doing actually give you? I argued three things, and that all three get more valuable as AI gets better, not less.
The first is that being wrong has to cost something. In a chat window, being wrong costs nothing. You ask, you get an answer, you nod, you move on, and half the time you never find out you were wrong at all. In a live environment, wrong means something is broken, and when something is broken you have to work out why, which means understanding what you actually did. Nobody gets paid to know the right answer. They get paid to work out why the thing in front of them isn't working, and you can't practise that by reading.
The second is that the struggle is the part that works. Think about something you genuinely know well, as opposed to something you have read about. My guess is you learned it because something went wrong and you had to sort it out. Nobody remembers the explanation that made sense at the time, but everybody remembers the bug that cost them a day. This is where it gets awkward, because an AI assistant exists to remove exactly that friction. That is the right thing when you are trying to get work done and the wrong thing when you are trying to learn, so the better these tools get at their job, the worse they are at teaching you anything.
The third is that you can't ask about what you don't know exists. An AI answers the question you asked, but it has no way of telling you about the thing you never thought to ask. Ask how to deploy your web application and you will get a good answer. What you won't get is a warning that your application isn't thread safe, because you didn't ask, and you had no reason to think you needed to. You find out in production six months later. In a workshop somebody else picked the steps, so you end up in front of the problem you would never have gone looking for.
Where the work actually is
That is the case for a workshop, meaning something people do rather than read. The catch is that plenty of people won't finish it, and when they don't, it is usually not the tooling that lost them.
People give up on workshops for four reasons. One is the environment. Something didn't install, the versions don't match, and twenty minutes in they have had enough. The other three are all content. The steps are too big a jump, they are told what to type but never why, or there is no way to tell whether what they just did actually worked. The environment problem can be solved with tooling. The other three, no platform will fix for you.
Step size is the one you can't feel, because you already know the answer. Write a step that says "now put it behind nginx" and to you that is one thing. For the person doing the workshop it is four. Install it, write a config file, work out why every redirect is coming back as http instead of https, and discover that X-Forwarded-Proto exists, which nobody has ever told them about. That step looked reasonable when you wrote it and it looks reasonable now. The only way you find out is by watching somebody try it. It goes the other way too. If every step is trivial, people stop reading them, and when a step actually matters they have already learned not to pay attention.
How much to tell them is the second. Write "run this exact command" and they will run it, it will work, and they will learn nothing, because they were typing rather than thinking. Write "now configure the server for production" and you have lost everyone who doesn't already know what that means, which is everyone, or they wouldn't be doing your workshop. What works is in between. Give them the command, let it work, then ask them to change one thing and say what they think will happen. Start the server with four workers. Now set it to one worker, send two requests at the same time, and before you run it, what do you reckon happens? That question is the whole trick. If they are wrong they find out in about two seconds, and now they actually understand what a worker is, which no amount of me explaining would have achieved.
Verification is the third, and the one people underestimate. Picture someone on step three. They edit a config file and make a small typo. Everything still starts, nothing looks wrong, and they keep going. At step eight the login flow fails for no obvious reason. They didn't quit at step three when they made the mistake. They quit at step eight, when they can't work out what is wrong and have no way to go back and find it. The part that should bother you is that they don't conclude they made a typo. They conclude your workshop is broken. So after anything that could silently go wrong, have them verify it worked before they move on, and show them what the output should look like. It is not much work, and it is the difference between finishing and giving up.
All three of these are doing the same job. When a workshop is working there is a rhythm to it. Read a bit, do a bit, check it worked, move on. Everything above is in service of not breaking that rhythm.
Give them somewhere to work
The environment problem is the one where tooling does solve it. Everyone has been in the room where a third of the people still aren't running forty minutes in, and the ones who did get it working are bored. Worse than the lost time, you have no idea what state anyone is in, so everything I said about checking their work goes out the window.
The answer is to host it, so every person gets a browser tab with the environment already in it, the instructions beside the terminal rather than in a separate window, an editor and a console in the same place, and all of it running before they arrive. I work on Educates, which is open source, but Killercoda, Strigo and Instruqt do this too, and the point is the category rather than the product. What matters is that everyone starts from the same place, which is what makes the checking possible at all.
Once you are hosting it, something more interesting opens up. You can hand someone an environment that is already broken and make them work out why, which is the only practical way to teach diagnosis, since if they build it themselves they only ever meet their own mistakes. You can start them at step seven with the first six already done, so the time goes on the part worth teaching. You can give them three services and a database with realistic data in it. The setup friction was never just a tax on the workshop. It was a limit on what the workshop could be about.
Using AI to write it
Yes, I see the irony. I spent the first part of the talk saying AI is why nobody needs our written walkthroughs, and I use AI to help write workshops. I don't think those are in conflict, but it pays to be specific about where it helps.
It is useful for getting a first draft down when the alternative is staring at an empty file, for generating variations on an exercise so you can pick the one that works, and for the tedious parts such as formatting, boilerplate and checking whether the commands you wrote three years ago still work. There is a pattern to that list. None of it requires knowing anything about the person who will do the workshop. All of it requires knowing the subject, because if you don't, you can't judge whether what comes back is right.
Where it falls down is exactly the three content problems, and for the same reason each time. It can't feel step size because, like you, it already knows the answer. It defaults to telling you exactly what to type because that is what documentation looks like, and documentation is what it learned from. It won't put checks in because the material it learned from doesn't have checks in it either. Some of that will change, since these tools move quickly, but the underlying cause doesn't. It has never watched anybody get stuck.
The way I think about it is that correct and teachable are different axes. AI is optimising for correct, meaning the commands work and the explanation is accurate. Teachable means paced for a real person who doesn't already know the answer, and nothing in how these tools are built is aiming at that. So let it draft, you shape it, and then you test it on an actual person. That third step is the one everyone skips and the only one that catches the problem, because the whole issue with AI-written material is that it reads fine. You can't catch it by rereading. You catch it by watching somebody try.
Getting to the starting line
Everything so far assumes that someone is already sitting in front of the workshop, ready to go. You can build the best workshop in the world and none of it matters if nobody starts it, and I think getting people to start is getting harder.
Workshops mostly get used in six situations. A conference booth, a conference workshop session, online and on demand, training at a customer site, a one-on-one demo for someone evaluating a product, and running it locally on their own machine. Sort those by why the person is there. At one end are the sales demo and the customer training, where there is a business reason behind attending, and that motivation doesn't disappear because AI got good. At the other end are online self-serve and the local download, where the only thing bringing someone is their own curiosity. Curiosity is exactly what a chat window satisfies, faster and for nothing. So the pressure isn't spread evenly. It is concentrated at the voluntary end.
That end has a second problem, which is that if you are an individual rather than a company, you have no way of telling anyone the workshop exists. The blogs that used to carry this sort of thing don't get read. The forums people used to hang around in have emptied out. Social media is so full of AI slop that anything real gets lost in it. Unless you already have a YouTube following, there is no route from "I made this" to "somebody knows about it". If you work somewhere that can put money behind getting the word out, you can buy the reach an individual doesn't have. Which means these workshops don't stop existing. They end up belonging to whoever can pay to be found.
I don't know how that resolves, and this is what I have seen rather than anything I have measured, so I am not going to claim self-serve workshops are finished. But I think we have been asking the wrong question. It isn't "are workshops still relevant". It is "relevant to somebody arriving how".
So why release 24 more
Which brings me back to wrapture. As I described in Trying out wrapture, there are now 24 workshops which run in JupyterLab on mybinder. Free, self-serve, click a link and start. That is precisely the end of the spectrum I just said is in trouble.
The difference is that wrapture is barely a month old. It isn't in any model's training data. Ask an AI how to use it and you will get either an admission that it doesn't know, or something confidently made up. The only way to get useful help from an AI is to deliberately feed it the documentation first, which you can do, since ReadTheDocs makes the complete wrapture documentation available as a single PDF. Short of that, the workshops are competing with a chat window that can't actually answer the question. That window will close as the models catch up, but it is open today, and while it is open a workshop is still the best way to learn wrapture by doing rather than by reading.
The other difference is one I glossed over in the talk. The AI will need material like these workshops to learn from. Whatever it can tell people about wrapture next year will be drawn in part from these posts and workshops, so even on the pessimistic reading, the effort isn't wasted. The value just shows up somewhere other than in people doing the workshops. That is a strange thing to be building for, but it is where things are.
There is a third reason as well, which is that creating the workshops was a way of further exploring how effective AI can be at producing hands-on material. I said in the talk that it fails at step size, at setting problems rather than regurgitating the documentation, and at adding verification, because it has never watched anybody get stuck. The question I wanted to test is whether it can be guided well enough to get those three right anyway. Whether that means being explicit about how big each step should be, insisting on the change one thing and predict what happens pattern, and requiring a check after anything that can silently go wrong. The wrapture workshops are the result of trying that, and by my own argument I can't yet say how well it worked, since the only way to find out is to watch people do them.
The discovery problem, this time, got partly solved by luck. Simon Willison wrote about wrapture twice, the Python Bytes podcast covered it, and the stars on GitHub jumped. That is the reach I said an individual doesn't have, borrowed rather than bought. Without it the workshops would most likely have sat there unnoticed.
What remains is the problem I have no solution for. Even when people know a workshop exists, sitting down and working through one is now seen as a chore next to asking an AI or watching a YouTube video. People have become accustomed to getting the answer without the doing. You do what you can. If people don't want to make use of what you provide, there isn't much more you can do about it.
Four things to take away
Reading and doing are different things. That hasn't changed, and it is the part AI hasn't touched. Three of the four reasons people give up are about your content rather than your tooling, and no platform fixes those. AI will make your content correct but it won't make it teachable, and that is still you. And know which channel you are building for, because whether workshops are still relevant turns out to depend entirely on who is sitting down and why they are there.
September 14, 2026 01:39 AM UTC
Armin Ronacher
Interpreting Pangram
Yesterday David Sacks wrote a tweet and within a few minutes people did, what they usually do, and they asked Pangram if it was AI. And Pangram said it’s entirely AI generated. To which David replied that these AI detectors are bogus.
Now Pangram has a pretty low false positive rate, but if you have ever used an LLM as a writing assitant, you will have probably noticed that it claims your posts 100% AI, even though you don’t feel like they are.
Pangram itself is a trained model, that attempts to detect segments of text as being definitely human, definitely AI and a mixture of the two. If you want to know how it works, they published a paper. The short summary is that they are manufacturing its own training data by starting from collections of known human authored text. An LLM is then tasked to understand the text and write a fresh new text on the same topic. They also let the LLM perform partial edits on that original human text and through that they can pick up on these co-authored details. Pangram claims their model to have rates of 0.0041% false AI accusations and 0.34% missed AI text.
So now that we know this I figured it might be fun to have an LLM re-create David’s tweet. I first came up with a prompt. And when I say I came up with that prompt I in fact used an LLM to propose to me from that tweet what I might want to say for the structure. I’m sure if you ask Pangram about if the above text is AI, it will probably say so, but that’s not really the point. The point is that I then used Opus 5 to generate a text which reads entirely AI generated.
If you are curious, this is the prompt I used:
Original prompt to generate a David Sacks like tweetPlease write an 8 paragraph tweet about “Pacing the Frontier”, not longer than 350 words.
Context of this tweet is this post by Dario: https://darioamodei.com/post/we-must-pace-the-frontier
And this tweet by Sam Altman: “I agree with Dario that we need to pace the frontier. This has been a primary topic of discussions we’ve had at OpenAI in recent weeks.
Committing to having independent evaluators with employee-like access is a great idea, and we will do the same. We’ll have more to share soon.”
Structure for the post:
- Open with a concession: Note that Dario (“We Must Pace the Frontier”) and Sam Altman both want to slow down at the frontier. Say “go ahead” which reads as a surprising response.
- Establish they own the frontier: Argue OpenAI and Anthropic hold a duopoly on frontier intelligence by all kind of metrics (market share, capability, revenue etc.) and that they themselves claim the lead is widening via RSI.
- Concede you can’t see their unreleased models, so if those are scary, being responsible is their call.
- Follow with a list of a rapid-fire paragraph (“stop pretending”) rejecting the asks from Dario’s post.
- Name the commercial self-interest (Slowing down isn’t purely altruistic)
- Close with the challenge that pacing buys room for a smarter regulatory debate than Bernie Sanders “shut it all down”. Say something about China and Open Weight models.
The text that Opus 5 then wrote, was this one:
Dario published “We Must Pace the Frontier.” Sam agreed, and said OpenAI will also commit to independent evaluators with employee-like access. My response is not the one they expect: go ahead. Please do.
Because you two are the frontier. OpenAI and Anthropic hold a duopoly on frontier intelligence by every metric anyone actually uses: capability, revenue, enterprise share, developer mindshare. And you both keep telling us the lead is widening, that models are starting to improve models. If that’s true, pacing costs you a quarter you can reclaim whenever you like.
I also can’t see your unreleased checkpoints. If what’s sitting behind the curtain genuinely scares the people who trained it, then slowing down is your call to make, and the right one. I’m not going to second-guess a risk assessment I have no access to.
But stop pretending the rest of us are the problem. Stop pretending this is a collective action failure in which a handful of startups are the defectors.
Stop pretending open weights are the dangerous frontier while the two of you ship the most capable model in the world every few months. Stop pretending third-party evals require new law, when you both just committed to them voluntarily, in public, on a blog.
And stop pretending it’s purely altruism. Pacing the frontier is also margin repair. It slows the price war, stabilizes the capex story, stretches depreciation schedules across a longer product cycle, and makes life considerably harder for fast followers who survive by distilling whatever you shipped last month. Safety and self-interest are pointing the same direction here. That’s worth saying out loud.
Still, the upside is real. Pacing buys room for a smarter regulatory debate than the one on offer, where the loudest alternative is Bernie Sanders’ “shut it all down.” That’s a slogan, not policy. Meanwhile China keeps shipping competitive open weights, and any rule that binds two American labs while the rest of the world downloads its capability for free isn’t safety, it’s theater with a very expensive ticket price.
And well, Pangram agrees that this is 100% AI. So far, so uninteresting. It does read somewhat like David’s tweet, but obviously not entirely. Given that the original prompt does not have enough information to re-create the tweet entirely you would expect some divergences.
The actual thing that interests me is if you can take this output at all, and then rewrite it from scratch, but by sticking to the general structure and ideas. Will Pangram give us a AI or human rating?
I read the generated text. Then I read each paragraph and decided to rewrite and rephrase it without an LLM. According to some similarity checkers, they the final texts are 50% similar which seems about right. But strictly speaking, not a single sentence is the same. Here is the 100% human rewritten text of the above one. No LLM was used to write it, but an LLM was used to fix up typos in the end. That from my experience really does nothing to tick off an LLM detector.
Dario has written “We Must Pace the Frontier,” and Sam from OpenAI has agreed. My response might surprise people: go ahead, please.
You two are the frontier! Your companies, OpenAI and Anthropic, are at the frontier by all metrics: revenue, developer mindshare, adoption, capabilities. And yet you both claim that your lead is widening as a result of recursive self-improvement as models are improving models. You currently are the duopoly of self-improving models!
I am unable to see what unreleased models you have. When what you have behind those doors really scares your folks, then you should slow down. I’m not going to tell you otherwise and I support you.
But please don’t pretend we are the problem. Stop pretending you need our permission. Stop pretending this is all a collective issue when in reality this is all on you. Stop pretending open weights are the problem here. Stop pretending pulling third-party evaluators in requires lawmaker involvement. And for the love of all the good things in the world: stop pretending this is all about altruism.
Pacing the frontier is also about your margins, and it makes it harder for fast followers. And it patches up your capex story and has the potential for slowing down the price war ahead of the IPOs.
But yes: pacing might give us the space for a better debate than Bernie Sanders’ “shut it all down.” There is no policy there. And while we’re having fights at home, China will keep shipping competitive open-weight models and won’t adhere to any American agreements.
This is all regulatory capture hiding behind a safety debate, and the rest of the world is watching.
So what does it say? Well this text too comes back as 100% slop. And it does not surprise me all that much. I have generally noticed that if you rely on an LLM to give your text structure, it will score badly on Pangram even if you do plenty of edits over it. In fact, it’s quite unlikely you’re going to get a post that starts out as slop into a structure that will make it appear that it’s not.
I came to quite appreciate the existance of Pangram because at the very least it has made me quite aware of some of the effects that using LLMs for writing blog posts has. This blog has been AI supported for about two years (as you can see from the AI transparency link on the bottom but I did notice that I became both more reliant on those tools and that they have become much more aggressive editors and it gave me pause.
Yet, I also think that plenty of people will find a “100% AI” rating misleading when in fact the author has done plenty of editing. But maybe it’s fair to have this to show up as entirely AI?
September 14, 2026 12:00 AM UTC
September 13, 2026
Bob Belderbos
How Libraries Run Rust Inside Python (with PyO3)
Every time you validate data with Pydantic v2, the data-validation library most Python apps reach for, a Rust extension does the work. Its core, pydantic-core, is built with PyO3, the same toolchain we'll use here.
This post builds that same kind of bridge, small enough to read in one sitting: a JSON parser written in Rust, exposed to Python, so you can import it like any other package. The last step, turning the Rust result into Python objects, is the one to understand before you port anything: for a parser like this, it can cost more than the parsing itself.
The four steps from Rust to import
Getting Rust code into Python takes four steps:
- Write a normal Rust module.
- Annotate it with PyO3 macros.
- Let maturin compile and install it.
- Import the result.

#[pyfunction] and #[pymodule] are the two Rust macros that do the wiring. A Rust attribute macro is close to a Python decorator: it rewrites the function it sits on, here adding the glue that lets Python call it and handles the type conversions and reference counting at the boundary.
Maturin then compiles the crate to a shared library (.so, .dylib, .dll) and drops it into your virtual environment, so import just works. I walk through this whole setup, from cargo new to the first import, in How to run Rust in Python with PyO3 and Maturin.
That first tutorial returns a single number. This one picks up where it left off, because the interesting part starts once you return a structure instead of a scalar.
The parser produces a Rust value first
The structure this parser returns is a JSON tree, and it's the running example for the rest of this post. In our Python to Rust cohort, students spend six weeks writing a JSON parser from scratch in Rust, a hand-rolled tokenizer and recursive-descent parser with no serde, then expose it to Python through PyO3.
The code I'll walk through here is my own implementation.
The parser produces a plain Rust enum. A Rust enum holds one of several shapes, and each variant can carry data, so it maps a JSON tree cleanly:
pub enum JsonValue {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<JsonValue>),
Object(HashMap<String, JsonValue>),
}
That tree lives entirely in Rust. Python never sees it. The PyO3 layer is a thin adapter on top.
Exposing one function
Exposing a function to Python takes two lines:
#[pyfunction]
fn parse_json<'py>(py: Python<'py>, input: &str) -> PyResult<Bound<'py, PyAny>> {
parse(input)?.into_pyobject(py)
}
For a Python reader, the signature is the most interesting part:
py:Python<'py>is a token representing access to the Python interpreter and is what you pass to PyO3 APIs that need access to Python objects. On traditional Python builds, this access is associated with holding the GIL. PyO3 hands it to you and you pass it along wherever you touch a Python object.Bound<'py, PyAny>is a handle to a Python object of any type, the Rust side of what you'd think of as aPyObject.PyResult<T>isResult<T, PyErr>: return the value, or an error PyO3 raises as a Python exception.?propagates that error. If parse fails, the function returns early and Python sees an exception; otherwise it unwraps theJsonValueand moves on.
So parse(input)? does the real work, and .into_pyobject(py) builds the Python objects the caller asked for. That last call is where the cost lives: it has to create Python objects for the nodes in the tree, and on a large document that can add up to more work than the parse itself.
The return trip is the expensive part
Here is why that conversion is not free. .into_pyobject walks the entire JsonValue tree and rebuilds it as native Python objects: a dict per object, a list per array, a float or str per leaf. You provide that translation by implementing the IntoPyObject trait, which PyO3 calls to convert a Rust value into a Python one:
impl<'py> IntoPyObject<'py> for JsonValue {
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
match self {
JsonValue::Null => Ok(py.None().into_bound(py)),
JsonValue::Number(n) => Ok(n.into_pyobject(py)?.to_owned().into_any()),
JsonValue::Object(obj) => {
let py_dict = PyDict::new(py);
for (k, v) in obj {
py_dict.set_item(k, v.into_pyobject(py)?)?; // recurses
}
Ok(py_dict.into_any())
}
// ...arrays, strings, booleans
}
}
}
A document with 100,000 values means on the order of 100,000 Python objects being created at the boundary, all after parsing is completely done. On a large document this materialization loop, not the parsing, can dominate the end-to-end time.
Errors cross the boundary the same way
The return value is not the only thing that has to translate. A parse failure is a typed Rust error, and Python wants an exception. One From impl, the trait Rust uses to convert one type into another, lets ? do the work:
impl From<JsonError> for PyErr {
fn from(err: JsonError) -> PyErr {
match err {
JsonError::UnterminatedString { position } => PyValueError::new_err(
format!("Unterminated string starting at position {position}")
),
// ...one arm per error variant, position preserved
}
}
}
Now malformed input raises a ValueError carrying the offset where parsing broke. The file-reading path gets the same treatment for free: std::io::Error already converts to the matching Python exception, so a missing path raises FileNotFoundError.
The caller gets Python semantics without the Rust layer leaking through.
What this means for your own port
If the Rust function you're porting returns a scalar, port it and move on. The boundary is usually small enough to ignore.
If it returns a large structure, the conversion is your real cost, and it is the next thing to optimize once the parser itself is fast. Preallocating the PyDict can help at the margins, but the bigger win is architectural: don't materialize the whole tree if the caller won't touch all of it. Hand back a lazy, Rust-backed view and build Python objects on demand.
So when you reach for PyO3, profile the boundary, not just the algorithm. Getting Rust to run fast is the easy half. What you build on the way out, the trip from Rust values to Python objects, is the half that decides whether the port was worth it.
September 13, 2026 12:00 AM UTC
Graham Dumpleton
Trying out wrapture
Since introducing wrapture at the end of August I have been putting out a post every day or so, first working through how it is used in unit tests and then how the same bindings trace a running application. With the last of the tracing posts now done, this is the point where I wanted to stop, take stock, and pull everything together in one place for anyone who wants to try it out.
Where things stand
The short version is that wrapture has been bumped to version 1.0.0b1. That move from alpha to beta is deliberate. I am happy with the APIs as they are and I am not seeing a need to change them, so what the beta series needs now is not more features from me but people using it on real code and reporting back. Reports of it working, or not working, on something I never thought to test, and of what confused you or what you found missing, are what will decide whether anything changes before a release candidate. They go to the issue tracker on GitHub.
One area where feedback would be especially useful is the OpenTelemetry export. The events wrapture records are mapped through to spans, attributes and metrics, and the intent was for that mapping to follow the OpenTelemetry semantic conventions. As I explained in the introductory post, the code was written by an AI under my direction rather than by me, so I cannot claim to have checked every attribute against the conventions myself, and can only say I hope the information is being mapped through okay. If you spend your days looking at traces in an OpenTelemetry backend and find that what arrives is not named or shaped the way the conventions say it should be, or the way your backend expects, I would like to hear about it while it is still cheap to change.
The companion instrumentation packages have been bumped to 1.0.0b1 at the same time. Until 1.0.0 is final a plain pip install wrapture picks up the latest pre-release automatically, so there is no need to pin a specific version to try it.
The links you need are:
- wrapture on GitHub, and the documentation on ReadTheDocs. The getting started page is the place to begin, and if you are coming from
unittest.mockthere is a comparison page mapping each mock idiom to its wrapture counterpart. - wrapture-instrumentation, the core collection of packaged instrumentation. It covers the web frameworks Flask, Django, FastAPI, Starlette and aiohttp, the servers uvicorn, werkzeug and
wsgiref, the HTTP clients requests, httpx, urllib3, aiohttp,urllib.requestandhttp.client, XML-RPC on both ends, gRPC, SQLAlchemy andsqlite3, and Jinja2 templates. - wrapture-instrumentation-aws, for the AWS SDK (boto3 and botocore), with every AWS API call recorded as one event.
- wrapture-instrumentation-postgresql, for the PostgreSQL drivers psycopg, psycopg2 and asyncpg.
- wrapture-instrumentation-mysql, for the MySQL drivers PyMySQL, mysqlclient and aiomysql.
The split between the core collection and the separate packages is that the core package only covers targets which can be exercised in-process, with no backend product or service needed to test against. Anything that needs a real server to test against, which the database and AWS packages do (their test suites run the real thing in a container), lives in a package of its own with its own release cadence. I also plan to have packages for Redis and MongoDB. Beyond that it will depend on what people are interested in seeing instrumentation for, with the only other thing I can see at the moment being the LangChain packages.
The posts so far
The posts are collected on the testing and tracing with wrapture guide page, which is the list that will keep growing, but for the record the reading order is as follows. First the starting point, on what wrapture is, why I built it, and how it was written:
Then the unit testing side, where the difference from unittest.mock is that the real code still runs and everything that flows through it is recorded:
- Unit testing with wrapture.
- Recording calls with wrapture.
- Phased behaviour in wrapture.
- Beyond callables in wrapture.
And the tracing side, where the same bindings observe a running application instead, from a live call tree in a terminal through to spans in an OpenTelemetry backend:
- Live tracing with wrapture.
- Zero-code tracing with wrapture.
- Tracing Flask with wrapture.
- Finding slow code with wrapture.
- OpenTelemetry export in wrapture.
Learning it by doing
Reading about a library only gets you so far, so alongside the posts there is now a wrapture-workshops repository on GitHub containing 24 workshops you can work through to learn wrapture in an interactive workshop format. Each takes one thing you might want to do with wrapture and walks you through doing it in a live JupyterLab session, with the instructions in a side panel whose actions drive the session and check your work as you go. The early workshops track the blog posts, one per post, and the later ones go into areas the posts have not covered yet, such as using wrapture with pytest properly, converting an existing mock based test suite, async code, patching third party libraries, distributed tracing across two processes, and writing an instrumentation package of your own.
You need nothing installed to try them. The workshops can be run in a hosted environment on mybinder.org, a free public service that builds the repository into a temporary JupyterLab running in your browser. Building takes a minute or two. A session is discarded when it ends, so finish a workshop in the session you started it in. If you would rather run them locally, the repository README has the steps for doing that under any JupyterLab.
Each workshop installs wrapture into a virtual environment of its own inside the workshop directory, the way a project would, so nothing is left behind in the JupyterLab environment. The workshops are pinned to a released version of wrapture, so the documentation may at times describe something newer than what a workshop uses.
Beaten to the punch
My intention was always to write this summary post once the testing and tracing posts were done, but Simon Willison got there first with Don't sleep on wrapture, which was his second post on wrapture after covering the initial announcement back in August. I am not complaining. His reach on social media is a great deal larger than mine, so a lot more people will have seen his summary than would have seen this one, and that has been reflected in the way the number of stars on the GitHub repository jumped after his post went out. The Python Bytes team also featured wrapture on episode 494 of their podcast, which helped as well. Thanks to all of them.
Either way, this post still serves a purpose, since it is the one place with all the links, the package list and the workshops together, and it will be the post I point people at when they ask where to start.
A side note on the workshops
I do hope people try the workshops on mybinder, and not only for what they teach about wrapture. I have worked on Educates for many years as a way of hosting online interactive workshops, and it remains the platform I would reach for when a workshop needs a full Kubernetes-backed environment. Being able to deliver workshops inside JupyterLab, with instructions in a side panel that drive the session and check what you have done, is something completely new which I only wrote in the past week, as a JupyterLab extension called jupyterlab-workshop. A workshop is nothing more than a directory with a manifest and some Markdown pages, and it runs wherever JupyterLab runs, so mybinder can host it with no container or cluster of my own behind it. I will do some followup posts on that in the coming week or so, since it deserves more than a paragraph at the end of a post about something else.
September 13, 2026 12:00 AM UTC
September 12, 2026
Mike C. Fletcher
Hardware Accelerated Video Capture for PyOpenGL
The pyopengl-video library is a small hack that lets you pass FrameBufferObjects (FBOs) directly to the local platform video encoding library (or allows you to use your existing video buffer as an FBO, depending on the platform). The purpose of the library is to allow an off-screen renderer to generate h264 in mp4 video previews of that off-screen rendering. I find that useful to allow an agent to capture e.g. gameplay demos or the like so that you can see what it is working on while you are not at your desk.
On Linux with nVidia uses NVENC. For linux with Intel or AMD uses vaapi. Windows uses D3D11 and COM. Requires PyOpenGL 4.0.0a5 or above. So far no OS-X solution.
September 12, 2026 01:05 AM UTC
Graham Dumpleton
OpenTelemetry export in wrapture
Everything in the last four posts rendered a trace for a person to read or wrote it to a file for later. The other destination is a tracing backend, fed while the application runs, and OpenTelemetry is the one that the ecosystem has converged on. wrapture treats it as a first-class destination rather than something you bolt on: the wrapture.otel subpackage ships in every wheel, and the otel extra brings the SDK and the OTLP exporter with it.
$ pip install "wrapture[otel]"
A plain install pays nothing for this, since nothing in base wrapture imports the subpackage until a config asks for it.
One table
Export is switched on by a top-level [otel] table in the same config file the Flask shop has been using. The presence of the table opts in, a service name identifies the process, and each signal's tuning nests beneath it. I shortened the metrics export interval so the demonstration would not have to wait a minute for a data point.
[otel]
service_name = "webshop"
[otel.metrics]
export_interval = 2
[[instrument]]
name = "flask"
ignore_paths = ["/health"]
[[observe]]
target = "shop:OrderService"
name = "place"
redact = ["card"]
[[observe]]
target = "shop:Gateway"
name = "charge"
redact = ["card"]
[[observe]]
target = "shop:Ledger"
name = "record"
Where the spans go is decided by the standard OpenTelemetry environment variables, so with a collector listening on the usual port nothing more is needed. For a look without a collector, the console exporters print the spans and metrics to standard output instead, which is what I used here:
$ OTEL_TRACES_EXPORTER=console OTEL_METRICS_EXPORTER=console \
python -m wrapture -m flask --app webshop run --port 5003
One event becomes one span. A request becomes a SERVER span, a call or a block becomes an INTERNAL span beneath it, and the tree the printer drew is the tree the backend receives. For the quote of an item that is not in the catalog, the view's span arrives with error status and the exception recorded on it (trimmed here to the parts that matter; the real output includes the stack trace and the resource attributes):
{
"name": "quote",
"context": {
"trace_id": "0x02f1fd262a7817d4f8b42fb0f6a30db3",
"span_id": "0x5b94b62db087bbc9"
},
"kind": "SpanKind.INTERNAL",
"parent_id": "0xfba0a55ab3a322f0",
"status": {
"status_code": "ERROR",
"description": "KeyError"
},
"attributes": {
"wrapture.path": "webshop:quote",
"wrapture.kind": "call",
"wrapture.arg.item": "missing"
},
"events": [
{
"name": "exception",
"attributes": {
"exception.type": "KeyError",
"exception.message": "'missing'",
"exception.escaped": "True"
}
}
]
}
And the request span it is parented under:
{
"name": "GET /quote/<item>",
"context": {
"trace_id": "0x02f1fd262a7817d4f8b42fb0f6a30db3",
"span_id": "0xfba0a55ab3a322f0"
},
"kind": "SpanKind.SERVER",
"parent_id": null,
"status": {
"status_code": "ERROR"
},
"attributes": {
"http.request.method": "GET",
"url.path": "/quote/missing",
"http.route": "/quote/<item>",
"http.response.status_code": 500,
"wrapture.data.endpoint": "quote",
"wrapture.data.remote": "127.0.0.1"
},
"events": [
{
"name": "exception",
"attributes": {
"exception.type": "KeyError",
"exception.message": "'missing'",
"exception.escaped": "False"
}
}
]
}
A few things in there are worth pointing at. The request span is named GET /quote/<item>, by the route pattern rather than the URL, because the Flask instrumentation annotates the request with its matched route once routing has run, and the exporter reads that as the semantic-convention http.route. A backend then groups by endpoint rather than seeing every URL as a distinct operation. The captured arguments and anything added with annotate() become span attributes under wrapture.arg.* and wrapture.data.*, with the card number already redacted before it got anywhere near the exporter. And the KeyError appears on both spans, once as the exception that escaped the view and once as the one noted against the request after Flask caught it, so the request span shows the 500, the error status and the reason together rather than a status with no explanation.
One trace across two processes
A trace within one process is only half of what a tracing backend is for. The trace-propagation example in the wrapture repository is two processes: a client that places orders against a quote service over HTTP, and the service itself, both observed by wrapture and both writing JSON Lines files. Every tree wrapture records carries a W3C trace id, minted at its root, and on the client side an instrumentation for urllib puts that id into the traceparent header of each outbound request. On the server side the WSGI middleware parses the header at the boundary, so that process's trees join the client's trace instead of minting their own.
The join needs no backend at all. Printing the first eight characters of the trace id from every line of both files, with the file it came from:
0b2ad016 server.jsonl backend:app
8f19b8c6 client.jsonl frontend:fetch_quote
8f19b8c6 client.jsonl frontend:fetch_quote
8f19b8c6 client.jsonl frontend:fetch_quote
8f19b8c6 client.jsonl frontend:place_order
8f19b8c6 client.jsonl urllib.request:OpenerDirector.open
8f19b8c6 server.jsonl backend:app
8f19b8c6 server.jsonl backend:quote
b1a416b0 client.jsonl frontend:fetch_quote
b1a416b0 client.jsonl frontend:fetch_quote
b1a416b0 client.jsonl frontend:place_order
b1a416b0 client.jsonl urllib.request:OpenerDirector.open
b1a416b0 server.jsonl backend:app
b1a416b0 server.jsonl backend:quote
...
Each order is one id across both files, client half and server half of one distributed trace. The repeated fetch_quote lines are the two blocks the client marks inside that function, which record under its path, and the lone server-only line at the top is a request that arrived with no traceparent header, which minted an id of its own at the boundary. The whole public surface the client instrumentation needed for this was wrapture.trace_headers(), which returns the pairs an outbound message made right now should carry, and is empty when nothing is being recorded, so injecting it is always safe.
Switching on [otel] in both processes changes nothing about the ids. The exporter claims the identity wrapture minted rather than minting one of its own, so the JSON Lines files, the outbound headers and the exported spans all read the same trace id, and the server's request span is created with the arrived identity as a remote parent. In the console output from the same run, the server's GET /quote/widget span carries the client's trace id and names the client's urllib.open span as its parent:
{
"name": "GET /quote/widget",
"context": {
"trace_id": "0xcdde803e61b96f52e2eb3820c7004df0",
"span_id": "0x9bb64b3bad9a6848"
},
"kind": "SpanKind.SERVER",
"parent_id": "0x670e42eb0ac7690a",
...
}
{
"name": "urllib.open",
"context": {
"trace_id": "0xcdde803e61b96f52e2eb3820c7004df0",
"span_id": "0x670e42eb0ac7690a"
},
"kind": "SpanKind.INTERNAL",
"parent_id": "0x0e06d5674120b1db",
...
}
In a viewer, each order is one distributed trace with the service's request span attached beneath the outbound call that made it. One invariant governs the header handling and it is worth stating because it is the thing that makes this safe to switch on in a service that sits between other people's systems: never break a trace you do not understand. A header wrapture parses but nothing claims is forwarded verbatim, so an upstream product sees this service as a transparent hop, and headers wrapture does not parse are never touched at all.
Metrics for free
The traces signal exports events individually. The metrics signal aggregates the same events instead, and both were on above since the default is all signals. Request durations go into the semantic-convention http.server.request.duration histogram, attributed by method, route and status code, and observed calls go into a per-path wrapture.call.duration histogram whose error series split out by exception type. From the same run, the attribute sets on the request histogram's data points were:
{
"http.request.method": "POST",
"http.route": "/order",
"http.response.status_code": 200
}
{
"http.request.method": "GET",
"http.route": "/quote/<item>",
"http.response.status_code": 500,
"error.type": "KeyError"
}
So per-endpoint latency and error rate read straight off the histogram with no code involved. The reason the bound path is safe as a metric attribute where a raw URL would not be is that the config chose the bindings, so the set of values is closed; requests are attributed by route pattern for the same reason, never by URL. The design is the Aggregate collector from the previous post with the aggregation handed to the SDK: bounded memory, no values captured, nothing retained.
What it costs
The usual objection to instrumenting Python is the overhead, so this is worth a paragraph. Exporting through wrapture costs about the same as instrumenting with the OpenTelemetry SDK directly, and on a call that raises it costs noticeably less, because the SDK's record_exception formats the stack trace through traceback.format_exception, which on current Pythons parses each frame's source to draw caret underlines that no backend renders, and wrapture's sink formats the same frames without them. The design point behind the rest is that the sink does not use the SDK's tracer at all. Everything a span needs is known when its event closes, so the sink builds the finished span at that moment and hands it to the SDK's own processor, skipping the tracer's mutable span object with its validated attribute store and locks, which is where most of the per-span cost used to go. The measured figures, with the methodology, are in the cost section of the OpenTelemetry export page, and I would rather point there than quote numbers that will be out of date by the time anyone reads this.
The thread from the beginning
When I introduced wrapture I said there were two interests behind it, correctness in testing and instrumenting programs for tracing, and that underneath they wanted the same thing: a way to see the real calls as they happen. This is where the second one ends up. The three bindings on the shop have not changed since the first testing post. In a test they feed a tape and the assertions read off it. In production they feed a backend, with a request as one tree, a trace id that survives crossing to another service, and metrics aggregated from the same events. The only thing that changed along the way is who was listening.
The OpenTelemetry export page has the rest of the table, including sampling, the logs signal and how wrapture's pipelines coexist with an application that already uses the OpenTelemetry API on its own account.
September 12, 2026 12:00 AM UTC
Armin Ronacher
P(doom)
This week some flavor of “AI is going to kill us all” went viral. In particular one where an employee put his personal probability of that happening above 10%. Which made me go to the Wikipedia page of P(doom) and I realized that Dario Amodei’s apparent probability of something bad happening seems to be between 10-25%. And well, Dario then wrote about pacing the frontier . And Sam read it and wants to pace too. And well, so does Musk.
I encourage you strongly to read the post, because I think it’s a good one. And yet, when I read the post I could not help but feel in strong opposition to it, despite the fact that I think I’m on the same page with regard to all observations and, to a large degree, the concerns.
I thought it might be interesting to write down my present-day thoughts on this, even if for no other reason than for myself to look back at it a year or two from now.
What Is Doom?
What I really appreciate about Dario’s post is that he lays out a scenario that is not a huge stretch but also one that describes a clear, unfortunate outcome we should fight: persistent botnets and other forms of nuisance. And well, we don’t have to look very far to see the issues left and right. Wikipedia has a page called 2026 OpenAI agent cyberattacks which gives you at least some overview of what we figured out agents have hacked up to this point. Except I know it’s not up to date, because for instance they also poisoned RubyGems.
Today these systems might be annoying, but they can be turned off when we figure out where they are. Except, it seems like OpenAI and Anthropic are operating at such a scale that they seemingly can be completely blind to what their systems are doing.
I don’t think we are anywhere close to a world where an agent might decide to hack into core inference infrastructure to upload weights to other GPUs to survive. But simultaneously it’s entirely in the realm of possibility and primarily curtailed by the labs probably being particularly careful about their IP.
For me the scenario I primarily worry about is what it does to us. And by us I mean anyone who is not currently working on closed weight, dopamine-loaded, subsidized token faucet. I really don’t worry about someone using these models to build a nuke, or to control some rockets in the Middle East, or that America would lose against China in some international culture war. I almost exclusively worry about what this does to us as humans.
What Needs To Be Paced?
What I find absolutely hilarious and simultaneously entirely frustrating about this conversation is that there is this idea that there is something to be paced. First of all, we should really talk about who Dario is talking about here. There are really only two companies: Anthropic and OpenAI. Nobody else matters in this space right now (this might change, but we’re talking about the right now). Both of those companies are basically coming from the same origin. The solution that Dario proposed, at least in part, is a third-party evaluator that in this case is METR. Which, unsurprisingly, also has strong ties to both OpenAI and Anthropic. Sure, there are some philosophical differences between the companies, but they are much more alike than they are different.
Both those companies greatly benefited from being able to train on public data that we all generated in one form or another over the last decades. They are also both increasingly causing strain on public resources, though it seems that OpenAI has their shit way less under control. But now we are presented with the idea that what these models are being trained on is so dangerous that it really should be in the hands of very few American corporations to decide who can do what and when and how.
But behold, Dario is also very worried about China. It starts with using AI for “democracy and freedom” and then it asks for ensuring that a gap with China exists. All new recent shenanigans on the Anthropic API are fully there to prevent the distillation by the Chinese, and they are not at all hiding it.
Automatic Pacing
I can tell you when the topic of AI safety and pacing is much less of a concern: if we actually were forced to have open weight models to begin with. A powerful technology that is out there for everyone to use comes with built-in pacing. In a way it’s the truest form of MAD or proliferation. I would argue we are in this pickle in the first place because right now the public is massively supporting (indirectly) the development of these models but simultaneously has to buy back the economic benefits that they might create from very few labs who have significant power. And their power is also seen as a geopolitical power, at least in the US, and maybe to some lesser degree in China.
And I know I use “public” loosely here. PyPI is not a public project, nor are RubyGems or GitHub. But they’re part of the Open Source commons and large AI companies are currently doing a tremendous job at stressing these in an effort to train ever more powerful models.
We should be glad that China is currently massively bailing out the world. If it were not for Chinese labs distilling American models, we would be in a pretty awful situation right now, particularly as Europeans. The open weight models are driving innovation and the diffusion of capabilities, and are leveling the playing field.
If we greatly restrain our AI capabilities in the belief that China will do the same, and then China defects, AI could be so powerful that such a defection could lead to their geopolitical dominance. Therefore any agreement must either have ironclad verifiability, or must be limited enough that defection would not be militarily existential.
— Dario Amodei
I am assuming Dario has reasons to believe this, but the models that are actually causing issues right now are all closed weight American models. I’m fairly certain if they were open weight models, we would not have that issue. Why? Because for a start, the economics of serving up these models are only that distorted due to how the big labs can operate. OpenAI is casually burning 18 million USD to brute force a problem on a whim. They are operating subscriptions at a massive loss, distorting the market everywhere. If we had mass accessibility on somewhat equal terms, a lot of the crazy issues we are seeing today would not be taking place.
A Total Regulatory Failure
From where I sit, what we observe right now is a total regulatory failure everywhere. In Europe you have some whacky AI regulation that is two years old and completely misses the problems that we actually have and focuses on problems that nobody has. In the US we’re seeing a system that is probably best described as turbo capitalism paired with sinophobia and erratic decision-making. In the chaos in which we find ourselves, the reality emerges. And the reality is, even today, really problematic.
Whatever laws and regulations already exist are largely completely ignored. Plenty of companies are buying data from all over the place that people never agreed could be used for training of AI models. The token economy that is emerging is one that looks like a drug market where you don’t know where the requests are going, what model is served up to you, where the GPUs are even running, let alone what you pay for all of this.
We now have mathematicians who are scared that their use of ChatGPT leads to future models being trained on their ideas, and OpenAI apparently can’t even rule it out.
Ideally the regulators would have forced these models to actually benefit the commons if they are from the commons. The internet has, for instance, greatly benefited from very liberal rulings in the US that permitted scraping. Learning on public data could have been regulated in a way that labs would have to actively support and enable certain forms of distillation. That alone would dramatically change how these models are trained.
What Might Happen?
As I said before, I don’t think AI is going to usher in an extinction event. In fact, even if nobody were to slow down, I really don’t think humanity would have much to worry about. I tend to think it would actually be the large labs that have much more to lose there in reputation and legal responsibilities. I find it preposterous that OpenAI’s agents are committing actual crimes out there, but we’re just shrugging our shoulders and moving on as if nothing happened. But I’m sure executives in those companies are waking up to the reality that this is not at all popular with a lot of their potential consumers.
I also think that this entire recursive self-improvement business has a good chance of being a problem. But not necessarily in that it will cause the end of humanity or societies, but that it will just do massive damage everywhere.
And really, it will just make a lot of the things we are doing much more expensive. Software engineering is an early victim of that. The newfound powers so far have resulted in a new tax that companies need to pay to the model providers, both to keep up with the new speed and to deal with the problem of these machines finding security issues left and right.
And presumably what is going on in software will happen to more industries. Universities and research groups will have to pour a lot of money into the closed models as well, to keep up with others who do.
In a way, I’m really confused that society is taking all of this so well.
September 12, 2026 12:00 AM UTC
September 11, 2026
LernerPython blog, from Reuven Lerner
Newsprint: Turning e-mail newsletters into a personal PDF
I really enjoy reading e-mail newsletters. They’re clever, informative, and funny, and provide me with lots of food for thought — as well as professional information that is crucial to my work.
The thing is, I don’t have time to read them during the week. And on Saturday, when I do have time, I don’t use my computer. So I have a funny way of reading them:
- When a newsletter arrives, a filter automatically moves it into my “toprint” folder.
- My very favorite newsletters are not only moved into “toprint”, but also get stars.
- On Friday afternoon, I go to my “toprint” folder, scoop up all of the starred newsletters, and print them out — 4 pages/side, and double sided. I then choose a number of the unstarred newsletters, and print them, too.
- Saturday morning, I get up super early (typically about 4:30 a.m.) and spend several hours reading through what I collected during the week.
I’ve been doing this for a few years, and like this system. But I don’t relish the idea of using so much paper, and I increasingly saw examples of a page that didn’t have to be printed, because it contained nothing more than the newsletter’s subscription information or self promotion. No reason to keep or print that. Plus, I don’t need all of the promotional images. Plus, sometimes my computer (or I) will fail to print the articles 4x/page, and the results are quite annoying.
Also? Why am I spending so much time on this, going through each message and printing it? Can’t my computer grab all of the starred messages and print them?
That was the genesis of newsprint, a new Python package that does all of this for me:
- I run newsprint from the command line. It takes all of the starred messages in my toprint folder, and creates a PDF file from them, 4 pages/side.
- Along the way, it strips out all of the promotional, subscribe/unsubscribe stuff.
- It removes images, unless it thinks that the images are useful and pertinent to the story. And if the image is very dark (like a dark-mode data plot), it reverses the colors.
- It puts footers at the bottom of each cell, indicating what newsletter this is and what page we’re on for that newsletter, as well as the overall newsletter.
- It creates a table of contents
- With a Claude API key, it’ll summarize the entire set of newsletters, telling you what topics appeared across all of them
- You can optionally ask for Claude to create a second, personal summary page. For example, I ask it to tell me if there are any topics with public data sets that might be appropriate for my Bamboo Weekly newsletter.
- You can choose from the unstarred messages in your inbox, and have them added to the PDF output.
- You can choose different paper sizes and orientation.
- Starred messages are deleted (“retired” in the package’s language) after they have been put into a newsletter. But they’re also recorded — so if you made a mistake, you can unretire them
- Everything is configurable from the command line and via a config file. Run newsprint –setup to create the config file for the first time.
Newsprint has already saved me lots of time and paper, and makes my Saturday-morning reading more enjoyable. I’m still iterating and improving on it; if you have suggestions, please send them my way!
Meanwhile, check it out on PyPI: https://pypi.org/project/newsprint/
The post Newsprint: Turning e-mail newsletters into a personal PDF appeared first on LernerPython.
September 11, 2026 03:03 PM UTC
Python | operator: Bitwise or and dict merging with __or__
In Python, we use “or” for conditions. | is bitwise (not boolean) “or”:
x = 10 # 0b1010
y = 13 # 0b1101
x | y # 15, or 0b1111
| runs __or__. On dicts, | combines.
d1 = {'a':10, 'b':2}
d2 = {'b':10, 'c':30}
d1 | d2 # {'a': 10, 'b': 10, 'c': 30}, right side wins
The post Python | operator: Bitwise or and dict merging with __or__ appeared first on LernerPython.
September 11, 2026 06:00 AM UTC
Bob Belderbos
Why Learn to Code If AI Can Code? 6 Reasons
Stanford's Chris Piech runs Code in Place, an intro programming class with 17,000 students and over 1,000 teachers. They've run it both before and after Cursor and Claude Code arrived, and enrollment basically doubled. It turns out that more people, not less, want to learn to code now that AI can write it.
Why learn the thing the machine can now do almost flawlessly? Six reasons out of an interview with him.
1. Syntax is the disposable half; problem-solving is the durable one
Piech splits learning to program into two parts: the syntax (how you tell a computer to do things) and the problem-solving (how you break a big problem into small pieces and set up data to talk to algorithms). AI is very good at syntax. There is less need to memorize it.
The half worth building is the one that transfers: problem solving. Decomposition, naming the sub-problems, deciding what data structure the algorithm needs. That is the problem solving part you carry into every language and every tool, including AI. The syntax is disposable; the problem-solving is durable.
2. Without foundations, you can't catch AI's bad decisions
Piech says he constantly ships with AI, and he's explicit about why it works for him: he already knows architecture. Strip that away and the model starts making poor structural calls you often only notice later on in the app's lifecycle.
I see this all the time working with LLMs: happy path and call it done. The edge cases show up when real users start hitting it, or when you step back from the first prototype and look hard at what you actually built. At that point you really need to understand the architecture underneath.
This is the whole argument for keeping fundamentals. AI is an accelerator, not a compass. It can get you to a solution faster, but it can't tell you if the solution is good.
3. Outsource too much and the muscle atrophies
"If you have AI write too much of your code, at what point can you no longer do that valuable piece of the architecture?"
And the insidious thing is that this will happen quietly. Using AI is fine, Piech says, as long as you stay self-aware about whether you're growing alongside it or handing away the growth. I made the same case from a different angle in Guardrails Protect Your Codebase. What Protects Your Judgment?: tooling can protect the coding part (e.g. with harnesses), but only making the hard decisions yourself protects your skill and judgment.
4. Coding is the best problem-solving gym you have
Code gives you immediate, falsifiable feedback. Your logic is wrong, the thing breaks, you see it, you iterate, you learn.
Apply problem-solving to life and the feedback cycle is much slower: you make a decision, and the consequences show up months later, often entangled in a complex web of other decisions.
Coding on the other hand is the rare domain where you can run the loop hundreds of times a week. (Deliberate practice is how you get the reps.) That's what trains judgment, and judgment is the skill AI can't hand you.
5. The high-order skill is knowing what's worth building
Piech frames the durable skill as interfacing between what computers can do and what humans actually need. What's the valuable problem? What feature helps a user make progress?
It has always been a critical high-order skill. What's changed is that more junior engineers can engage with it now, and if you're a junior his advice is to start on it today. You don't need the senior title first.
It's the same reason I keep arguing design beats code: I once reduced a 1,069-line AI-built app down to 156 lines because I put design before code.
6. You can now learn faster than ever
This is the flip side of reason 3: used well, AI can be an extraordinarily powerful tutor.
Piech's own move if he were starting today: build a lot of prototypes with Claude Code, then ask it to teach him the most important concepts behind each one, and iterate.
Foundations first, then learn to code with AI, in that order. That's why I'm building a Python Foundations course: the workflow layer around the language, learned by shipping one real tool. More soon.
The tool that can erode your skills can also compound them, depending entirely on whether you let it think for you or make it teach you.
None of this depends on where AI lands in 2030. Self-driving cars looked close in 2012, and their progress was badly overestimated. It's the last 1% that resists the machine: the odd thing on the highway, the judgment call no one scripted. Coding is no different. Decomposition, judgment, taste, knowing what to build; those turn out to be the most human-dependent parts of the job and the ones that will be most valuable in the AI era.
September 11, 2026 12:00 AM UTC
Graham Dumpleton
Finding slow code with wrapture
The /order endpoint of the Flask shop is slow. The view calls the order service, the service calls the gateway and then the ledger, and the question is which of those the time is going to. To give the question a real answer for this post I put a time.sleep(0.03) in Ledger.record, and the rest of the post pretends I did not know that.
The usual move is a stopwatch. A perf_counter() before and after the service call, a log line with the difference, another pair around the gateway, another around the ledger. Each of those is a code change in a layer that should not know it is being measured, the numbers arrive as separate log lines that you correlate by eye, and none of them are tied to the request they belong to, so one slow request among fast ones is invisible in the average. A profiler has the opposite problem: it sees every frame in the process, most of them framework internals, and cannot tell one request from the next.
The tree with times on it
The config from last time already prints an elapsed time on every closing line, so the first order through the server is most of the answer:
POST /order (webshop.wsgi_app)
order()
shop:OrderService.place(amount=500, card='<redacted>', tenant='acme')
shop:Gateway.charge(amount=500, card='<redacted>')
shop:Gateway.charge -> {'id': 'ch_500', 'amount': 500} [8us]
shop:Ledger.record(entry="<dict {'id': 'ch_500', 'amount': 500}>")
shop:Ledger.record -> 'led_ch_500' [35.1ms]
shop:OrderService.place -> {'id': 'ch_500', 'amount': 500} [35.9ms]
order -> '<Response 29 bytes [200 OK]>' [36.3ms]
webshop.wsgi_app -> '200 OK' [37.3ms, body 10us over 1 chunk]
Reading up from the bottom, the request took 37.3ms, the view 36.3ms, the service 35.9ms, and the ledger 35.1ms, with the gateway at 8us. The figures are from one run, and they vary, but the shape does not. The ledger accounts for essentially all of the service, which accounts for essentially all of the view. The service and the view are slow because of what they call. The ledger is slow in its own right.
That distinction, slow itself versus slow because of a child, is the one a wall-clock timer around the service call cannot express, and it has a name. Self time is an operation's duration minus the time its observed children account for, and wrapture computes it from the parent links as events close. In a test, tape.tree(times=True) prints both figures and tape.self_time() gives it for one event, so the same observation can be turned into an assertion that will catch the next regression:
import wrapture
from shop import Gateway, Ledger, OrderService
from webshop import app
def test_where_the_time_goes():
place = wrapture.binding(OrderService, "place", capture=wrapture.redact("card"))
charge = wrapture.binding(Gateway, "charge", capture=wrapture.redact("card"))
record = wrapture.binding(Ledger, "record")
with wrapture.instrumentation("flask"), wrapture.timeline(place, charge, record) as tape:
client = app.test_client()
response = client.post("/order", json={"amount": 500, "card": "4111-1111-1111-1111", "tenant": "acme"})
assert response.status_code == 200
print()
print(tape.tree(times=True))
order = place.events.assert_once()[0]
ledger = record.events.assert_once()[0]
assert tape.self_time(order) < 0.1 * order.duration
assert tape.self_time(ledger) > 0.9 * order.duration
The wrapture.instrumentation("flask") context applies the same Flask instrumentation the config file named, scoped to the block, and the timeline records what the three bindings see. Running it with pytest -s prints the tree:
shop:OrderService.place(amount=500, card='<redacted>', tenant='acme') -> {'id': 'ch_500', 'amount': 500} [31.0ms, self 173us]
shop:Gateway.charge(amount=500, card='<redacted>') -> {'id': 'ch_500', 'amount': 500} [7us]
shop:Ledger.record(entry={'id': 'ch_500', 'amount': 500}) -> 'led_ch_500' [30.8ms]
The service spent 173us of its 31.0ms doing anything itself. No external profiler can produce that number for an arbitrary handful of methods, because a profiler only sees whole call stacks; wrapture can, because the events know their parents.
Across many requests
One request is an anecdote. The Aggregate collector keeps one row per bound location, with how many operations began and completed, how many raised, and the total, self, fastest and slowest times, sorted by self time, which is the column profilers rank by. It retains no events, so its memory is bounded by the number of bindings however much traffic flows, and it asks for no argument or result values, so the recording skips capture entirely while it is the only thing listening. It can be registered as a sink in code, but the shape I wanted was a report for the whole run of the server, which is a window in the config file:
[[window]]
name = "stats"
report = "stats.txt"
[[window.collect]]
type = "aggregate"
A window with no trigger and no duration is one run for the whole process, opened when the config applies and closed at interpreter exit, one report. I ran the server under that config, sent it thirty requests from a loop (ten orders for one tenant, ten declined orders for another, and ten quotes), stopped it, and read the file:
aggregate "aggregate" run 1, 2026-09-01 14:57:29 to 14:57:31 +10:00 (1.6s), pid 87241
7 paths, 120 operations begun, 120 completed, 20 raised
calls total self per-call min max errors path
10 358.3ms 358.3ms 35.8ms 30.7ms 39.5ms shop:Ledger.record
30 385.2ms 11.9ms 12.8ms 534us 40.5ms flask.app:Flask.wsgi_app
20 369.8ms 5.7ms 18.5ms 296us 40.1ms webshop:order
20 364.1ms 5.7ms 18.2ms 105us 39.9ms 10 shop:OrderService.place
10 2.2ms 2.2ms 223us 63us 1.6ms flask:render_template
10 3.5ms 1.3ms 354us 188us 1.8ms webshop:quote
20 106us 106us 5us 4us 11us 10 shop:Gateway.charge
The ledger is the top row by a wide margin. The order view and place have large totals and small self times, which is the same story the single tree told, now over twenty orders with a minimum and maximum attached. The errors column shows the ten declined cards twice, once where the gateway raised and once where the service let it escape. The same report can be produced every hour on the hour with totals reset, from the same file, by giving the window a schedule; the scheduled tracing page covers that, and I will leave it there.
Slow for whom
An endpoint is often slow for one tenant, one account or one request id, and the middleware cannot know which header carries that. annotate() merges values into the in-flight event's data, and it is unconditionally safe to call, doing nothing when nothing is recording, which makes it reasonable to leave in application code permanently. In the shop a before_request hook is the natural place, since the request event is already open by the time it runs:
@app.before_request
def tag_tenant():
wrapture.annotate(tenant=request.headers.get("X-Tenant"))
This is the one edit to the application in this series, and it is the same annotate() the testing series used to attach what the code knows to an event. The tag rides on the request event, so with a jsonlines sink in the config beside the printer it is in the file, and the slow requests can be sliced by who they were for:
$ jq -c 'select(.kind=="request" and .data.tenant=="acme") | {tenant: .data.tenant, path: .data.path, ms: ((.duration*1000*10|round)/10)}' trace.jsonl
{"tenant":"acme","path":"/order","ms":35.7}
{"tenant":"acme","path":"/order","ms":32.5}
{"tenant":"acme","path":"/order","ms":35.9}
The other tenant's orders were all declined at the gateway and never reached the ledger, so they sit around a millisecond. The same expression selects the request to assert on in a test, through events.matching(), and a Filter around a printer narrows the live view to one tenant's requests.
The cheaper cousin
Everything above retained a duration. Sometimes the answer is just a number, and the Counter collector counts operations as they begin and keeps nothing else, which makes it cheap enough to leave running under a whole test suite. Bind a database layer's execute once, register a counter, and give every test a query budget in a fixture, and the classic N+1 regression fails with a number attached rather than slipping through as a test that merely got slower. The collectors section of the ad-hoc tracing page has that example in full.
So far every trace has been rendered for a person or written to a file. The remaining step is feeding the same events to a tracing backend while the shop runs.
September 11, 2026 12:00 AM UTC
September 10, 2026
Talk Python to Me
#562: DuckLake: The Lakehouse That's Just SQL and Parquet
How many files does your query read before it reads any data? On some data lakes, you go through JSON and metadata files first, just to learn which Parquet files matter. DuckLake asks one SQL question instead. The metadata lives in a real database. The data stays in plain Parquet. That's the entire format. <br/> <br/> Pedro Holanda joined DuckDB in 2018, when it was still a research prototype at CWI. He's the lead DuckLake developer. Guillermo Sanchez Dionis works on DuckLake and the new Quack protocol. <br/> <br/> With Quack as the catalog, DuckLake handles 200 transactions a second under heavy contention. No other open table format comes close.<br/> <br/> <strong>Episode sponsors</strong><br/> <br/> <a href='https://talkpython.fm/sixfeetup'>Six Feet Up</a><br> <a href='https://talkpython.fm/training'>Talk Python Courses</a><br/> <br/> <h2 class="links-heading mb-4">Links from the show</h2> <div><strong>Guests</strong><br/> <strong>Pedro Holanda</strong>: <a href="https://pedroholanda.org?featured_on=talkpython" target="_blank" >pedroholanda.org</a><br/> <strong>Guillermo Sanchez</strong>: <a href="https://www.linkedin.com/in/guillermo-s%C3%A1nchez-dionis/?featured_on=talkpython" target="_blank" >linkedin.com</a><br/> <br/> <strong>PhD on progressive indexes</strong>: <a href="https://ir.cwi.nl/pub/31048?featured_on=talkpython" target="_blank" >ir.cwi.nl</a><br/> <strong>SQLite</strong>: <a href="https://www.sqlite.org?featured_on=talkpython" target="_blank" >www.sqlite.org</a><br/> <strong>Litestream</strong>: <a href="https://litestream.io?featured_on=talkpython" target="_blank" >litestream.io</a><br/> <strong>boring hardware</strong>: <a href="https://talkpython.fm/episodes/show/531/talk-python-in-production" target="_blank" >talkpython.fm</a><br/> <strong>DuckDB</strong>: <a href="https://duckdb.org?featured_on=talkpython" target="_blank" >duckdb.org</a><br/> <strong>episode 491</strong>: <a href="https://talkpython.fm/episodes/show/491/duckdb-and-python-ducks-and-snakes-living-together" target="_blank" >talkpython.fm</a><br/> <strong>Iceberg</strong>: <a href="https://iceberg.apache.org?featured_on=talkpython" target="_blank" >iceberg.apache.org</a><br/> <strong>manifesto</strong>: <a href="https://ducklake.select/manifesto/?featured_on=talkpython" target="_blank" >ducklake.select</a><br/> <strong>DuckLake</strong>: <a href="https://ducklake.select?featured_on=talkpython" target="_blank" >ducklake.select</a><br/> <strong>spec</strong>: <a href="https://ducklake.select/docs/stable/specification/introduction?featured_on=talkpython" target="_blank" >ducklake.select</a><br/> <strong>this diagram</strong>: <a href="https://blobs.talkpython.fm/ducklake-architecture.png?cache_id=13839f" target="_blank" >blobs.talkpython.fm</a><br/> <strong>Data inlining</strong>: <a href="https://ducklake.select/2026/04/02/data-inlining-in-ducklake/?featured_on=talkpython" target="_blank" >ducklake.select</a><br/> <strong>ducklake-dataframe</strong>: <a href="https://github.com/pdet/ducklake-dataframe?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>Polars course</strong>: <a href="https://training.talkpython.fm/courses/polars-for-power-users" target="_blank" >training.talkpython.fm</a><br/> <strong>CSV parser</strong>: <a href="https://duckdb.org/2025/04/16/duckdb-csv-pollock-benchmark.html?featured_on=talkpython" target="_blank" >duckdb.org</a><br/> <strong>Zero-copy Arrow</strong>: <a href="https://duckdb.org/2021/12/03/duck-arrow.html?featured_on=talkpython" target="_blank" >duckdb.org</a><br/> <strong>ART index</strong>: <a href="https://duckdb.org/2022/07/27/art-storage.html?featured_on=talkpython" target="_blank" >duckdb.org</a><br/> <strong>async I/O</strong>: <a href="https://duckdb.org/2026/07/31/asynchronous-io?featured_on=talkpython" target="_blank" >duckdb.org</a><br/> <strong>v1.0</strong>: <a href="https://ducklake.select/2026/04/13/ducklake-10/?featured_on=talkpython" target="_blank" >ducklake.select</a><br/> <strong>Git-like branching</strong>: <a href="https://ducklake.select/roadmap.html?featured_on=talkpython" target="_blank" >ducklake.select</a><br/> <br/> <strong>Watch this episode on YouTube</strong>: <a href="https://www.youtube.com/watch?v=wEKbbY5tPtI" target="_blank" >youtube.com</a><br/> <strong>Episode #562 deep-dive</strong>: <a href="https://talkpython.fm/episodes/show/562/ducklake-the-lakehouse-thats-just-sql-and-parquet#takeaways-anchor" target="_blank" >talkpython.fm/562</a><br/> <strong>Episode transcripts</strong>: <a href="https://talkpython.fm/episodes/transcript/562/ducklake-the-lakehouse-thats-just-sql-and-parquet" 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>
September 10, 2026 02:59 PM UTC
Django Weblog
PyCharm & Django Fundraiser Extended to September 14
The second half of our annual JetBrains fundraiser has been extended through September 14, 2026. Thank you to JetBrains for the extra time. You still have time to renew your PyCharm license or give it a try. You get PyCharm at 30% off, and JetBrains donates 100% of your purchase or renewal to the DSF. This is one of the DSF's bigger fundraisers of the year, and we appreciate everyone who takes a look.
The Executive Director search also closes on September 14, and we are curious to see who reaches out.
JetBrains has been a steady sponsor for years, and it was great to see the chatter around the Django Developers Survey 2026 results we released with them last month. The full report is on the JetBrains site. If you are shopping for an IDE, AI-focused or not, they have a solid product.
If you would like to help with our fundraising goals, we would love to hear from you or your company. The board is happy to talk with individuals too, if you have ideas. Whatever you can do to support us, we appreciate it.
Ways to help
- Renew or start a PyCharm license through the fundraiser before September 14.
- Have your company sponsor the DSF directly.
- Donate on our website or give through GitHub Sponsors.
- Reach out to the board if you want to donate, sponsor, or volunteer, or if you have fundraising ideas.
September 10, 2026 11:00 AM UTC
Hugo van Kemenade
Soft-deprecating re.match()
Quick, without looking it up, what does re.match() do? Which of these return a match?
import re
re.match("pi", "pi")
re.match("pi", "pie")
re.match("pi", "api")
re.match("pi", "magpie")
How does it compare to re.search() and re.fullmatch()?
Whilst you’re (quickly) thinking about it, let’s introduce soft deprecation.
Soft deprecation #
Python’s backwards compatibility policy (PEP 387) introduced soft deprecation in 2023:
A soft deprecation can be used when using an API which should no longer be used to write new code, but it remains safe to continue using it in existing code. The API remains documented and tested, but will not be developed further (no enhancement).
A soft deprecation does not imply future removal of the API, nor does it issue a warning. It’s a docs-only recommendation to not use an API, ideally with a suggested replacement.
It’s a completely separate decision whether, if ever, to turn a soft deprecation into a regular “hard” deprecation (where removal may follow); soft deprecations don’t “graduate” into regular deprecations or removals.
re.match() #
Now the answer:
>>> import re
>>> re.match("pi", "pi") # ✅ Matches
<re.Match object; span=(0, 2), match='pi'>
>>> re.match("pi", "pie") # ✅ Matches
<re.Match object; span=(0, 2), match='pi'>
>>> re.match("pi", "api") # ❌ No match
>>> re.match("pi", "magpie") # ❌ No match
>>>
So re.match() only matches at
the beginning of a string! This can be surprising: why is the start of the string
special?
re.search() #
If you don’t want to anchor at the start, and want to match anywhere in the string, use
re.search():
>>> import re
>>> re.search("pi", "pi") # ✅ Matches
<re.Match object; span=(0, 2), match='pi'>
>>> re.search("pi", "pie") # ✅ Matches
<re.Match object; span=(0, 2), match='pi'>
>>> re.search("pi", "api") # ✅ Matches
<re.Match object; span=(1, 3), match='pi'>
>>> re.search("pi", "magpie") # ✅ Matches
<re.Match object; span=(3, 5), match='pi'>
>>>
re.fullmatch() #
If you want to anchor both the start and the end, and check the entire string matches,
use re.fullmatch():
>>> import re
>>> re.fullmatch("pi", "pi") # ✅ Matches
<re.Match object; span=(0, 2), match='pi'>
>>> re.fullmatch("pi", "api") # ❌ No match
>>> re.fullmatch("pi", "pie") # ❌ No match
>>> re.fullmatch("pi", "magpie") # ❌ No match
>>>
Introducing re.prefixmatch() #
Because of the surprising half-anchored behaviour,
we’ve introduced a new alias for re.match() in Python 3.15,
named re.prefixmatch():
Quoting from the Zen Of Python (
python3 -m this): “Explicit is better than implicit”. Anyone reading the nameprefixmatch()is likely to understand the intended semantics. When readingmatch()there remains a seed of doubt about the intended behavior to anyone not already familiar with this old Python gotcha.
Soft-deprecating re.match() #
And with a more explicit replacement, we’ve
soft-deprecated re.match() in Python 3.15:
We do not plan to remove the older
match()name, as it has been used in code for over 30 years. It has been soft deprecated: code supporting older versions of Python should continue to usematch(), while new code should preferprefixmatch().
Use re.prefixmatch() if you only really meant to use the half-anchor; otherwise use
re.search() or re.fullmatch().
Comparison #
| Function | Start anchor | End anchor | Added in | With special characters |
|---|---|---|---|---|
re.search() |
❌ | ❌ | 1.5 | re.search("pi", string) |
re.match() |
✅ | ❌ | 1.5 | re.search("^pi", string)re.search(r"\Api", string) |
re.prefixmatch() |
✅ | ❌ | 3.15 | re.search("^pi", string)re.search(r"\Api", string) |
re.fullmatch() |
✅ | ✅ | 3.4 | re.search("^pi$", string)re.search(r"\Api\z", string) |
The functions without special characters are generally a bit faster.
Lint #
You can avoid re.match() in your project with
Ruff:
# pyproject.toml
[tool.ruff]
lint.extend-select = [
"TID251", # flake8-tidy-imports: banned-api
]
lint.flake8-tidy-imports.banned-api."re.match".msg = "Use re.fullmatch() or re.search() instead"
Or run:
ruff check . --isolated --select TID251 \
--config 'lint.flake8-tidy-imports.banned-api."re.match".msg = "use re.fullmatch() or re.search() instead"'
See also #
Header photo: Double-exposure bike and pedestrian stencils (CC BY-NC-SA 2.0 Hugo van Kemenade).
September 10, 2026 08:08 AM UTC
EuroPython
EuroPython 2026: Videos Published
Hi all Pythonistas! 👋
EuroPython 2026 took over the ICE Congress Centre in Kraków from 13 - 19 July and it wouldn’t have been possible without all of our attendees, speakers, volunteers, and sponsors. You allowed us to showcase the Python community at its best once again, and the conference truly belongs to all of you 💚
We are now two months on, and we’ve just finished tying up the loose ends over here at EuroPython HQ. In this newsletter, let us look back at some of the highlights of the conference, from Guido van Rossum to gelato.
P.S. Yes, we’ve got the videos!
🙏 Thank You
Thank you to all of you who attended EuroPython 2026 and dedicated a week of your life to Python. We’re endlessly grateful to be host to some of the best technical presentations on the planet, which makes it all the more meaningful when people say what they remember is the community 💚
A very special thank you to:
- all of our speakers, tutorial and sprint leads, without whom there would be no EuroPython
- our team of more than fifty volunteer organisers, most of whom worked for months ahead of the conference
- on-site volunteers, who kept the conference running smoothly and took care of all of us
- our sponsors, without whom the conference would simply not be possible
Many thanks to the whole EuroPython 2026 TeamFinally, thank you to our faultlessly helpful onsite volunteers - those with the yellow T-shirts - for taking ownership of the conference during the week, and being the cheerful face of EuroPython, and taking care of us all in Kraków.
👉 The EuroPython 2026 Credits Reel: https://ep2026.europython.eu/thank-you
🎥 Talk Recordings & Photos
The video recordings from all of our tracks are already up on our YouTube channel, so you can catch the sessions you missed, or send that session to a friend.
Recording of the core.py podcast on the main stage: Pablo Galindo Salgado, Guido van Rossum, and Łukasz Langa👉 EuroPython 2026 on YouTube: https://www.youtube.com/watch?v=9ZuZfG8_jH8&list=PLd3Y9yzyC5Uo
👉 Conference photos on Flickr: https://www.flickr.com/photos/europython/collections/72157725522022865/
📊 Some interesting facts about this edition
EuroPython 2025 brought the community together in Kraków for another packed week, and the numbers tell a lovely story: a genuinely international crowd, heavily weighted towards experienced Pythonistas, with a healthy remote contingent tuning in from further afield. Here are the figures that stood out to us:
- 1,386 tickets in total — 1,263 onsite and 123 remote
- Attendees came from across the globe, with Poland (236), Great Britain (169) and Germany (154) leading the way
- 70.7% of attendees rated themselves as advanced or expert Python users
- Out of those who answered the question, 23% identified themselves as women, 1% as other, and 76% as men
- Core Python was the most popular topic (66.8%), followed by Web Development (51.0%) and Data Science & ML (49.4%)
Additionally, the Code of Conduct team provided the transparency report: https://www.europython-society.org/europython-2026-code-of-conduct-transparency-report/
🏆 Community Service Recognition
EuroPython Society Fellows
We&aposre delighted to announce two new EuroPython Society Fellows: Cristián Maureira-Fredes and Piotr Gnus, in addition to Martin Borus who was recognized recently. The Fellow Grant is the Society&aposs way of honouring members of the EPS and the EuroPython Workgroups whose contributions have significantly shaped our mission, the conference and the organisation itself.
Fellows are nominated by EPS members, confirmed by the Board, and receive lifetime free attendance at EuroPython alongside a permanent listing on our Fellows page. Our heartfelt thanks to all three for everything they&aposve given to this community.
👉 Find out more about EPS Fellowship: https://www.europython-society.org/europython-society-fellow-grant/
Python Software Foundation Community Service Award
For the first time ever, a Python Software Foundation (PSF) Community Service Award was handed over on the EuroPython stage. Rodrigo Girão Serrão was nominated last year, but the award was presented during the closing ceremony in Kraków.
The award recognises work that "significantly improves the Foundation&aposs fulfillment of its mission and benefits the broader Python community." Plenty of CSA recipients are based in Europe, so we hope this was the first of many.
👉 Read about the award: https://www.python.org/community/awards/psf-awards/#introduction
🎂 EuroPython’s 25th Birthday
Since its very first edition back in 2002, EuroPython has grown into the longest-running community Python conference in Europe. Over the years it has travelled across the continent, hosted by volunteers in city after city, bringing together thousands of Pythonistas to learn, share, and build the community we know today.
We looked for some of the people who have been with the conference the longest:
Jacob Hallén
Marc-André Lemburg and David Allan🍨 The Sprints Had a Gelato Truck
Genuinely. The Free Software Foundation Europe joined us for the sprints, and one of their volunteers, Luca Bonissi, drove his home-made ice cream up from Milano - along with the freezer and all the equipment needed to serve it to everyone sprinting. Reusable cups, fruit flavours reportedly around 70% fruit, and a queue that said everything.
Luca Bonissi serving gelato at EuroPython 2026 SprintsThis is the kind of thing that only happens when a community shows up for each other.
Thank you, Luca, and the Free Software Foundation Europe 🍨
🧠 How Well Do You Really Know Python?
One of the surprise hits of the week was Rodrigo Girão Serrão’s 15-minute Kahoot quiz in the main hall, covering the conference, the community and the language itself. It was hard: the top 8 players only managed 5 out of 10, and 9th place got 4.
The best moment? Rodrigo asked exactly how many commits Guido had made over the lifetime of Python. It was worth double points. Nobody got it right. Three questions later he asked the same thing again - and this time 20–25% of the room got it.
All the questions and answers are written up now, so you can find out how you&aposd have done. Participants rated the quiz at the very top of the sessions they attended, so yes - it&aposs coming back next year.
👉 Test yourself on Rodrigo’s quiz: https://mathspp.com/blog/python-quiz-europython-2026-edition
🎨 Made by You
We were sent some wonderful artwork from the week - including Michaela Dušková&aposs sketch of the core.py panel (three cats on stage, which feels about right) and Ava Katushka&aposs illustration of the PyLadies crew. Shared with permission, and both very much loved by the team.
Michaela Dušková&aposs sketch of the core.py panel
Ava Katushka&aposs illustration of the PyLadies crew👬 Community Partnerships
🌷 PyCon NL
PyCon NL has grown from a small meetup in 2019 to a thriving community and conference. After hosting the first official edition in 2024, they return in 2026 with the newly founded PyNetherlands Foundation and a conference fully organised by the Python Community in the Netherlands.
PyConNL is creating a program for every kind of Pythonista, whether you’re just starting out, exploring the world of data, or building with DevOps and architecture.
👉 Get your ticket: https://www.pycon-nl.org/
☀️ PyCon España
PyConES 2026 already has its programme and tickets are available. Don&apost miss the chance to be part of the most important Python conference in Spain in one of the most beautiful cities in the world.
👉 Don&apost have your ticket yet? Now is the time! 👉 https://pretix.eu/python-spain/pycones-2026/
🏖️ Django on the Med
Django on the Med is a free three-day Django sprint on the Mediterranean coast, bringing together seasoned contributors and first-timers to shape Django&aposs 6.x roadmap and get the work done. Mornings are for sprinting, the rest of the day for the coast. The second edition runs 23rd–25th September 2026 in Pescara, Italy
👉 Find more details at https://djangomed.eu/
💚 Thank You to Our Sponsors
EuroPython simply does not happen without our sponsors. Enormous thanks to our Platinum sponsors - Manychat, Microsoft and Vercel - and to every other sponsor who backed the conference this year.

Manychat builds AI-powered chat automation for 1M+ creators and brands at real production scale.

Open Source enables Microsoft products and services to bring choice, technology and community to our customers.

Vercel provies Agentic Infrastructure for every app and agent. They are the creators of AI SDK, Next.js, Turborepo, and v0.
👋 Stay Connected
Follow us on social media and subscribe to our newsletter for all the updates:
👉 Sign up for the newsletter: https://blog.europython.eu/portal/signup
- LinkedIn: https://www.linkedin.com/company/europython/
- X/Twitter: https://x.com/europython
- Mastodon: https://fosstodon.org/@europython
- Bluesky: https://bsky.app/profile/europython.eu
- Instagram: https://www.instagram.com/europython/
- YouTube: https://www.youtube.com/@EuroPythonConference
We hope that you&aposll enjoy relieving your favorite EuroPython 2026 moments as autumn approaches. Until next time! 🐍💚
Cheers,
The EuroPython Team
Sign up for EuroPython Blog
The official blog of everything & anything EuroPython! EuroPython 2026 13-19 July, Kraków
No spam. Unsubscribe anytime.
September 10, 2026 06:43 AM UTC
LernerPython blog, from Reuven Lerner
Python `__mod__`: Modulo for numbers, interpolation for str
The % in Python, on numbers, is modulo:
x = 10
y = 3
x % y # 1
x.__mod__(y) # same thing, 1
But str uses __mod__ for interpolation:
s = 'Hi, %d'
s % y # 'Hi, 3'
s.__mod__(y) # Same thing, 'Hi, 3'
Same operator, same magic method — but totally different.
The post Python `__mod__`: Modulo for numbers, interpolation for str appeared first on LernerPython.
September 10, 2026 06:00 AM UTC
Graham Dumpleton
Tracing Flask with wrapture
For a web application the natural unit of tracing is the request: one HTTP request, its method, path and status, and every observed call made while handling it, as one tree. The config file from last time cannot give you that on its own, and it is worth being clear about why before showing what does.
A WSGI application looks like any other callable, but it routes the interesting facts around the return value. The status and headers travel through the start_response callback rather than being returned. The body is an iterable that the server consumes after the call has returned, so a streaming application does most of its work after a call event would already have closed. And when a view raises, the framework catches the exception and turns it into a 500 response before any wrapper on the application ever sees it. A binding on the application callable would record a call that returned an iterable and raised nothing, which is true and useless.
The shop behind Flask
Here is the shop from the earlier posts behind a small Flask application. A /quote/<item> route renders a template, a /order route places an order through the OrderService from before, and a /health route exists because every deployed service has one.
from flask import Flask, jsonify, render_template, request
from shop import CardDeclined, OrderService
CATALOG = {"widget": 25, "gadget": 120}
app = Flask("webshop")
service = OrderService()
@app.get("/health")
def health():
return "ok\n"
@app.get("/quote/<item>")
def quote(item):
price = CATALOG[item]
return render_template("quote.html", item=item, price=price)
@app.post("/order")
def order():
data = request.get_json()
try:
charge = service.place(data["amount"], data["card"], tenant=data["tenant"])
except CardDeclined as exc:
return jsonify(error=str(exc)), 402
return jsonify(charge)
Nothing in it mentions wrapture. The quote view will raise a KeyError for an item that is not in the catalog, which Flask will turn into a 500, and that is the request I most want to see.
One entry
The Flask knowledge lives in an instrumentation package rather than in the config. With wrapture-instrumentation installed alongside wrapture, the config gains a single [[instrument]] entry naming Flask, and keeps the observe entries for the shop's own methods from last time:
[[instrument]]
name = "flask"
[[observe]]
target = "shop:OrderService"
name = "place"
redact = ["card"]
[[observe]]
target = "shop:Gateway"
name = "charge"
redact = ["card"]
[[observe]]
target = "shop:Ledger"
name = "record"
[[sink]]
type = "printer"
The development server runs under the runner exactly as the script did, with everything after -m flask belonging to Flask:
$ python -m wrapture -m flask --app webshop run --port 5001
Then from another shell, a quote, an order, a declined order and the item that does not exist:
$ curl http://127.0.0.1:5001/quote/widget
$ curl -X POST -H 'Content-Type: application/json' \
-d '{"amount": 500, "card": "4111-1111-1111-1111", "tenant": "acme"}' \
http://127.0.0.1:5001/order
$ curl -X POST -H 'Content-Type: application/json' \
-d '{"amount": 250, "card": "4000-0000-0000-0000", "tenant": "globex"}' \
http://127.0.0.1:5001/order
$ curl http://127.0.0.1:5001/quote/missing
In the server's log, interleaved with Flask's own access log lines which I have removed here, each request arrives as one tree. The quote:
GET /quote/widget (webshop.wsgi_app)
quote(item='widget')
flask:render_template(template_name_or_list='quote.html', context='<context>')
flask:render_template -> '<17 chars>' [1.5ms]
quote -> '<p>widget: 25</p>' [1.6ms]
webshop.wsgi_app -> '200 OK' [2.3ms, body 5us over 1 chunk]
The request line opens the tree, the view sits beneath it labelled by its endpoint, the template render sits beneath the view with the template's name and its context masked (it is arbitrary application data, and the render is captured only as its size), and the closing line carries the status as the request's result along with the time to the last byte of the body. The order, with the shop's own methods nesting beneath the view because their bindings fire while the request is in flight:
POST /order (webshop.wsgi_app)
order()
shop:OrderService.place(amount=500, card='<redacted>', tenant='acme')
shop:Gateway.charge(amount=500, card='<redacted>')
shop:Gateway.charge -> {'id': 'ch_500', 'amount': 500} [7us]
shop:Ledger.record(entry={'id': 'ch_500', 'amount': 500})
shop:Ledger.record -> 'led_ch_500' [4us]
shop:OrderService.place -> {'id': 'ch_500', 'amount': 500} [205us]
order -> <Response 29 bytes [200 OK]> [471us]
webshop.wsgi_app -> '200 OK' [922us, body 4us over 1 chunk]
The declined card, where the view caught the exception and answered 402, so the failure is on the gateway and the service but not on the request:
POST /order (webshop.wsgi_app)
order()
shop:OrderService.place(amount=250, card='<redacted>', tenant='globex')
shop:Gateway.charge(amount=250, card='<redacted>')
shop:Gateway.charge !! CardDeclined [6us]
shop:OrderService.place !! CardDeclined [74us]
order -> (<Response 38 bytes [200 OK]>, 402) [265us]
webshop.wsgi_app -> '402 PAYMENT REQUIRED' [673us, body 4us over 1 chunk]
And the one I wanted:
GET /quote/missing (webshop.wsgi_app)
quote(item='missing')
quote !! KeyError [4us]
webshop.wsgi_app -> '500 INTERNAL SERVER ERROR' !! KeyError [3.2ms, body 6us over 1 chunk]
The request line says two things at once. It answered 500, and the KeyError was the reason. That second half is the part a reader would not guess, because as far as the WSGI middleware recording the request is concerned, the application returned normally. Flask caught the exception on its way out of the view and handed it to handle_exception, which built the 500 response and returned it, so the request completed with a status and no exception. The only place the failure can be seen is inside that handler, where the exception arrives as an argument, and that is where the instrumentation looks. A binding on handle_exception notes the exception against the nearest enclosing request event, using the same note_exception() that the testing series used for a failure the code handled itself, aimed past the handler's own call with current_event(kind="request"). The view's event carries the KeyError as the exception that escaped it, the request's event carries it as a note, and both show up on their lines because two scopes failed for the same reason.
Keeping the noise out
Health checks and static assets make up most of the traffic on a lot of services and none of the interest. In the log above every /health probe printed its own tree, which after a day of a load balancer polling it is most of the file. The instrumentation takes a list of paths not to record:
[[instrument]]
name = "flask"
ignore_paths = ["/health"]
A matching request runs and answers as normal but records nothing at all, and the "at all" matters. Declining the request event alone would leave the view, any lifecycle callbacks and any template render it made on the trace as anonymous roots with no request above them, the same problem tree=True solved on a plain binding in the first post. The setting silences everything beneath an ignored request for its whole extent, so with it in place the health probe leaves only Flask's access log line behind and the next quote prints as before:
127.0.0.1 - - [01/Sep/2026 14:51:43] "GET /health HTTP/1.1" 200 -
GET /quote/gadget (webshop.wsgi_app)
quote(item='gadget')
flask:render_template(template_name_or_list='quote.html', context='<context>')
flask:render_template -> '<18 chars>' [1.5ms]
quote -> '<p>gadget: 120</p>' [1.7ms]
webshop.wsgi_app -> '200 OK' [2.3ms, body 5us over 1 chunk]
The other switch worth knowing about is lifecycle = false. Flask extensions register before_request and after_request callbacks liberally, for loading users, cleaning up sessions and stamping headers, and the instrumentation observes every one of them by default in the order Flask runs them. For an application with several extensions that is faithful but noisy, and switching it off leaves the callbacks running unobserved.
What the instrumentation is
There is no magic in the [[instrument]] entry. It names an Instrumentation class whose hooks run when Flask is imported, and those hooks apply bindings to three choke points in Flask, using the same bindings as everywhere else. Constructing a Flask instance installs the recording WSGI middleware on its wsgi_app attribute, so every application the process creates is covered however it was made, application factories included. Registering a route substitutes an observed version of the view function, since Flask captures views into its dispatch table the moment @app.route runs, before any binding on the module could have seen them. And handle_exception gets the binding that notes the failure described above. The flask-app example in the wrapture repository is that class written out in full, as a local file next to a config, for anyone who wants to do the same for a framework that has no package yet. The packaged version adds the lifecycle callbacks, error handlers, blueprints and template rendering on top.
The request as an event
Everything the tree shows is on the request event itself, which is what a sink or a test reads. The result is the status line, so every existing filter and assertion that works on a return value works on a request. The duration is wall time from the call to the close of the body, time to last byte, with the synchronous phase and the body's own share recorded separately. The HTTP details, method, path, query string with sensitive parameters already masked, scheme, remote address and the bytes actually served, sit in the event's data, and the instrumentation adds the matched route pattern and endpoint once routing has run, which are the low-cardinality keys a backend groups by. The WSGI request tracing page has the full event, the mode="wsgi" binding form for applications with no framework package, and the redaction rules; ASGI applications get the same treatment on the page beside it.
With a request as one tree and timings on every line, the next question is the one every web application eventually asks, which is where the time is going.
