Planet Python
Last update: June 02, 2026 04:44 PM UTC
June 02, 2026
Real Python
Structuring Your Python Script
You may have begun your Python journey interactively, exploring ideas within Jupyter Notebooks or through the Python REPL. While that’s great for quick experimentation and immediate feedback, you’ll likely find yourself saving code into .py files. However, as your codebase grows, knowing where things should go in your script becomes increasingly important.
Transitioning from interactive environments to structured scripts helps promote readability, enabling better collaboration and more robust development practices. This video course shows you the foundations of organizing a Python script: where the runnable bits go, how to arrange your imports, and how to refactor with constants and a fixed entry point.
By the end of this video course, you’ll know how to:
- Make a script directly executable on Unix-like systems with a shebang line
- Organize your import statements using standard grouping conventions
- Automatically sort imports and format your code with the
rufflinter - Replace hard-coded values with meaningful constants
- Define a clear script entry point using
if __name__ == "__main__"
Without further ado, it’s time to start working through a concrete script and progressively shape it into well-organized, shareable code.
[ Improve Your Python With đ Python Tricks đ â Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
PyCharm
Top Agentic Frameworks for Building Applications 2026
In 2026, the world of AI is changing at a serious pace. The days of AI systems dealing solely in single-prompt interactions are coming to an end. Instead, these models are evolving into agentic systems â long-running, goal-driven software enabled by agentic frameworks that are becoming a critical layer in modern application architecture.
This rapid shift means that Python developers building autonomous systems are increasingly relying on agentic frameworks to manage reasoning, memory, tools, and collaboration among multiple agents.
Youâve probably already heard of some of the most popular frameworks. LangChain and AutoGen have risen to prominence, but there are dozens more, many of them open-source and only one to two years old. With so many frameworks promising different agentic capabilities, the real challenge is knowing which ones are best suited for the kind of application you want to build.
Letâs take a closer look at some of the most important agentic frameworks on the market in 2026, comparing what each does best and rating them based on our key comparison criteria to help you discover which is best for your projects.
What are AI agents?
An AI agent is a piece of software capable of autonomously reasoning, setting goals, and performing tasks on behalf of a user or another system. As the name suggests, AI agents have a level of agency to learn, adapt, and make decisions independently. This means they can improve their behavior and, over time, choose their own actions to achieve specific goals or outcomes.
AI agents work by following a perceive, reason, act, reflect (PRAR) cycle, which allows them to:
- Perceive: Observe the environment, including user input, system state, tools, and memory, to understand the current context and constraints of the task.
- Reason: Plan, make decisions, and select actions using a large language model (LLM) or hybrid logic.
- Act: Execute actions like calling tools, updating memory, or triggering workflows.
- Reflect: Evaluate the outcome of previous actions and adjust future decisions, plans, or prompts to improve results.
AI agents rely on the natural language processing capabilities of large language models, but unlike traditional LLMs and AI chatbots, they donât require continuous user input to perform tasks. Agents are proactive, working autonomously to achieve a goal based on a specified set of rules and parameters.
What is an agentic framework?
An agentic framework provides the infrastructure needed to build, run, and control AI agents at scale. Most modern frameworks offer three core capabilities:
- Orchestration: Controls how agents are sequenced, coordinated, or allowed to collaborate.
- Tools: Define how agents interact with external systems like APIs or databases.
- Memory: Sets out how agents retain and retrieve information across steps or sessions.
While itâs possible to build an agent without a framework, theyâre vital in ensuring agents are reliable, scalable, and safe.
Agentic frameworks help turn experimental agent builds into maintainable software by facilitating:
- Multi-agent coordination: When multiple agents communicate to plan, work together, and specialize in different areas of a task.
- Human-in-the-loop (HITL) checkpoints: Intentional pause points where a human can review what an agent is about to do.
- Observability, control, and reproducibility: The ability to see what an agent is doing, guide agent behavior, or re-run an agent and receive the same results.
Core orchestration paradigms
Before comparing individual frameworks, itâs important to understand how they operate. Letâs look at the three most commonly used orchestration models in 2026.
Graph-based orchestration
Graph-based orchestration provides maximum control by organizing agents and tools as nodes in a directed graph. Instead of letting an agent freely decide what to do next, the flow that agents are allowed to follow is clearly defined.
Strengths
- More deterministic control: Predictable behavior is critical for production systems that require reliable results.
- Easier debugging: Pinpoint exactly which node failed thanks to clear checkpoints and boundaries.
- Production-grade reliability: This approach is ideal for customer-facing applications, enterprise systems, or regulated environments.
Limitations
- More upfront design: The workflow must be defined in advance, which slows initial development.
- Less âemergentâ behavior: Agents are constrained by the graph, leaving less room for experimentation and creativity.
Role-based orchestration
Role-based orchestration is most effective when simplicity is a priority. Agents are assigned specific roles, such as âPlannerâ, âResearcherâ, or âBuilderâ, and collaborate by sending messages to one another.
Strengths
- Intuitive mental model: This type of operation is easy to understand because it effectively mirrors how human teams work.
- Rapid prototyping: Minimal setup is required, allowing more time to explore outcomes.
Limitations
- Harder-to-constrain behavior: Because agents have the freedom to decide what to do next, itâs difficult to enforce strict execution paths.
- Limited determinism: The same input can yield different outcomes, making it tricky to reproduce results and achieve consistency.
Chain-based orchestration
Chain-based orchestration, also known as adaptive orchestration, arguably offers the greatest flexibility. Agents in this model operate in dynamic chains or loops, deciding the next step autonomously.
Strengths
- Flexible workflows: Agents are not constrained to a pre-defined path and can freely explore different strategies.
- Suitability for creative tasks: This approach is ideal for research, discovery, and experimentation, as agents can iteratively explore ideas, pivot strategies, and adapt their approach.
Limitations
- Less predictability: Testing and debugging are more challenging because execution paths are harder to reproduce and trace.
- More difficult governance at scale: This unpredictability grows as tasks become more complex.
Best agentic frameworks for your projects
Now that we’re familiar with the key orchestration paradigms of agentic frameworks, itâs time to compare some of the most popular frameworks on the market in 2026. Below, we evaluate each frameworkâs performance against our key comparison criteria:
- Primary orchestration model.
- Multi-agent support.
- Memory capabilities.
- Human-in-the-loop (HITL) support.
- Best-fit applications.
| Framework | Orchestration model | Multi-agent support | Memory capabilities | HITL support | Best used for |
| LangChain | Chain-based | Partial | Moderate | Limited to moderate | Rapid LLM app development |
| LangGraph | Graph-based | Yes | Strong | Strong | Production-grade agent workflows |
| LlamaIndex | Retrieval-centric | Limited | Strong | Moderate | Knowledge-heavy agents |
| Haystack | Pipeline-based/modular | Moderate | Strong | Moderate | Production RAG and context-heavy AI systems |
| AutoGen | Role-based | Strong | Moderate | Limited | Conversational multi-agent systems |
| CrewAI | Role-based | Strong | Light | Limited | Task-oriented agent teams |
| Semantic Kernel | Planner-based | Moderate | Moderate | Strong | Enterprise AI |
| smolagents | Minimalist | Limited | Light | Minimal | Lightweight experiments |
| OpenAI Agents SDK | Graph-based | Yes | Managed | Strong | Hosted agent applications |
| Phidata | Agent-centric | Limited to moderate | Strong | Moderate | Data and tool-heavy agents |
Letâs take a closer look at the strengths and weaknesses of each framework, along with the applications theyâre most suited to.
LangChain
- Core design: Chain-based orchestration.
- Philosophy: Developer velocity and flexibility.
Launched in 2022, LangChain is one of the most widely adopted frameworks due to its broad ecosystem of integrations. It serves as an accessible interface for nearly any LLM and is an ideal starting point for enthusiasts or startups looking to explore agentic AI. While not strictly âagent-firstâ, it provides the building blocks for agentic behavior.
LangChain provides less control than other frameworks, but itâs still a fantastic entry point into agentic systems, especially for projects where speed and creativity take precedence over enforcing strict workflows.
Strengths
- Huge ecosystem.
- Easy tool integration.
- Rapid prototyping.
Limitations
- Less control than graph-based systems.
- Agent logic that can be difficult to understand as it grows in complexity.
Best applications
- Prototyping of agentic features.
- Tool-augmented chatbots.
- LLM-powered backend services.
If you want to go beyond the basics, read our LangChain Python Tutorial: A Complete Guide for 2026. It takes a deeper look at what LangChain offers and walks through real-world use cases for building AI agents in Python.
LangGraph
- Core design: Graph-based orchestration.
- Philosophy: Explicit control over agent behavior.
LangGraph has emerged as the leading standard for production-grade agent systems. Built on top of LangChain, it replaces implicit chains with explicit graphs, providing strict control over workflows and excellent HITL support via interrupts.
While the graph structure itself can actually make debugging easier by clearly mapping how agents and tools interact, LangGraph does come with a learning curve. Much of this complexity comes from designing the graph and managing explicit state between nodes. Once you understand these concepts, the framework becomes a powerful option for building predictable and controllable agent systems.
Strengths
- Deterministic workflows.
- Native state management.
- Excellent HITL support via interrupts.
- Suitability for regulated or mission-critical systems.
Limitations
- Higher upfront design effort.
- Steeper learning curve due to explicit graph and state management.
- Reduced flexibility for open-ended tasks.
Best applications
- Autonomous customer support systems.
- AI-driven DevOps workflows.
- Multi-step decision engines.
LlamaIndex
- Core design: Retrieval-centric orchestration.
- Philosophy: Data-first agents.
LlamaIndex is a Python framework designed to help AI systems understand, store, and retrieve information from large amounts of documents and data.
Rather than starting with agents and adding data later, LlamaIndex takes the opposite approach â it starts with data and then builds agent behavior around it. This is why it is often described as data-first or retrieval-centric.
Because it operates in this way, LlamaIndex excels at indexing, memory, and retrieval, making it ideal for building agents whose intelligence depends on accessing the right information rather than executing complex actions.
Strengths
- Advanced document indexing.
- Strong long-term memory patterns.
Limitations
- Limited suitability for complex, action-heavy orchestration.
- Limited support for multi-agent orchestration.
Best applications
- Research assistants.
- Knowledge base agents.
- Enterprise document intelligence.
Haystack
- Core design: Modular pipeline orchestration.
- Philosophy: Context engineering and production-ready AI systems.
Haystack is an open-source AI orchestration framework created by deepset for building production-ready AI agents, retrieval-augmented generation (RAG) systems, and multimodal applications.
Instead of focusing purely on agent behavior, Haystack structures applications as explicit pipelines composed of retrievers, routers, memory layers, tools, evaluators, and generators. This modular architecture gives you control over how information flows through a system, allowing each component to be tested and improved independently.
Haystack is particularly strong in applications where the quality of retrieved information determines the quality of the modelâs output. Its design also makes it well-suited for enterprise environments that require transparency and reliability in production systems.
Strengths
- Highly modular pipeline architecture.
- Excellent support for RAG and document processing.
- Strong ecosystem, particularly in search and RAG-focused enterprise use cases.
- Flexible integrations with models and vector databases.
Limitations
- More infrastructure and setup than lightweight frameworks.
- Less focus on emergent multi-agent collaboration.
Best applications
- Retrieval-augmented generation (RAG) systems.
- Enterprise document intelligence.
- Data-heavy AI applications.
- Production AI pipelines that require strong context control.
AutoGen
- Core design: Role-based multi-agent collaboration.
- Philosophy: Conversation-driven autonomy.
AutoGen, an open-source Microsoft framework, popularized the idea of agents collaborating through structured conversation, organizing systems as teams of agents, each with its own specific role. Unlike in other frameworks, thereâs no central controller enforcing a strict execution path â the collaboration itself drives progress.
This approach makes AutoGen ideal for exploratory, creative, and research-driven multi-agent systems, at the cost of predictability, HITL, and strict execution control.
Strengths
- Natural multi-agent interaction.
- Minimal orchestration overhead.
- Suitability for emergent problem-solving.
Limitations
- Limited execution control.
- Weak HITL support.
Best applications
- Coding agents.
- Brainstorming systems.
- AI research experiments.
CrewAI
- Core design: Role-based task delegation.
- Philosophy: Teams of specialized agents.
CrewAI is centered around building simple, structured multi-agent systems. It is similar to AutoGen, modeling AI agents as members of a âcrewâ where each agent has a clearly defined role. The goal is to make multi-agent systems approachable, even if you are new to agentic AI.
CrewAI prioritizes simplicity and speed over deep memory and production controls, making it easy to learn and a strong option for prototypes and small teams. However, its limited toolset for observability, HITL, and error handling at scale makes it less suited for larger systems.
Strengths
- Very approachable API.
- Clear role separation.
- Fast setup.
Limitations
- Lightweight memory.
- Limited production controls.
Best applications
- Content pipelines.
- Market research automation.
- Simple workflow agents.
Semantic Kernel
- Core design: Planner-based orchestration.
- Philosophy: Enterprise-grade AI integration.
Semantic Kernel is another open-source Microsoft framework, designed for building AI-powered applications that integrate with existing enterprise systems.
It was created with production concerns in mind from the start, emphasizing governance, safety, observability, and human oversight. Rather than maximizing agent autonomy, it focuses on making AI predictable, controllable, and auditable.
By combining structured workflows with LLM reasoning, it trades flexibility and emergent behavior for trust, safety, and operational reliability.
Strengths
- Strong HITL support.
- Enterprise-friendly architecture.
- Good observability.
Limitations
- Heavier upfront structure.
- Less flexibility for open-ended autonomy.
- Steeper learning curve.
Best applications
- Internal enterprise tools.
- AI copilots.
- Business process automation.
smolagents
- Core design: Minimalist chain-based.
- Philosophy: Simplicity over scale.
smolagents is a bare-bones framework designed to make agentic AI as straightforward and transparent as possible. It prioritizes simple, readable code that makes it easy to understand how an agent works without needing to learn a large framework.
smolagents aims to make agent behavior accessible and easy to experiment with by keeping abstractions minimal and logic transparent. It offers first-class support for code-based and tool-calling agents, broad model and tool compatibility, and lightweight CLI utilities, while intentionally trading large-scale orchestration and production features for simplicity and clarity.
Strengths
- Extremely lightweight design.
- High degree of transparency.
- Fast experimentation.
Limitations
- Limited suitability for scaling
- Minimal production features.
Best applications
- Educational projects.
- Proofs of concept.
- Lightweight local agents.
OpenAI Agents SDK
- Core design: Managed workflow-driven orchestration (often graph-based).
- Philosophy: Hosted, production-ready agents.
Thanks to ChatGPTâs explosion in popularity, weâve all heard of OpenAI. The Agents SDK is the companyâs effort to provide a managed platform for building and running agents without having to maintain your own orchestration infrastructure.
Rather than assembling agents from scratch, you define agent behavior and workflows, while OpenAI provides orchestration, memory management, monitoring, and safety controls. This makes the Agents SDK particularly attractive for teams that want production-ready agents quickly.
Strengths
- Minimal infrastructure burden.
- Built-in safety and observability.
- Strong multi-agent support.
Limitations
- Reduced customization and control.
- Limited suitability for experimental research.
Best applications
- SaaS agent features.
- Customer-facing autonomous systems.
- Teams prioritizing speed over customization.
Phidata
- Core design: Agent-centric, tool-heavy.
- Philosophy: Practical agents for real-world data tasks.
Phidata is designed for building practical, tool-driven AI agents that operate on real-world data.
Rather than focusing on abstract orchestration patterns, Phidata centers the agent around direct interaction with systems such as APIs, databases, and internal services.
Its design reflects the fact that many agents spend most of their time fetching, transforming, and acting on data.
Strengths
- Strong tool integration.
- Suitability for data-centric workflows.
Limitations
- Less emphasis on orchestration.
- Limited multi-agent capabilities.
Best applications
- Data analysis agents.
- Finance and ops automation.
- Tool-driven decision systems.
Choosing the right framework
Now that youâre familiar with many of the most popular frameworks in 2026, itâs time to choose the right one for your project. Letâs take a look at some of the key use cases, along with the frameworks that fit them best.
| Orchestration model | Where to use | Recommended frameworks |
| Graph-based | Projects involving complex branching logic and requiring high levels of reliability, auditability, and control. | LangGraph, OpenAI Agents SDK |
| Role-based | Projects involving rapid development and intuitive design that benefit from emergent collaboration between agents. | AutoGen, CrewAI |
| Chain-based | Projects requiring maximum flexibility, where agents need to adapt dynamically and determine next steps autonomously. | LangChain |
| Retrieval-based | Projects where deep, reliable access to knowledge matters more than high levels of autonomy. | LlamaIndex, Haystack |
| Enterprise-oriented | Projects where strong governance and human-in-the-loop processes are non-negotiable requirements. | Semantic Kernel |
| Lightweight | Rapid prototyping, educational use, and simple local agents where transparency and control matter more than orchestration complexity. | smolagents |
| Tool-centric | Building production agents that primarily interact with APIs, databases, and external systems rather than complex multi-step orchestration. | Phidata |
In 2026, agentic frameworks have evolved from experimental tools into foundational infrastructure for many applications. The key decision is no longer whether to use agents, but how much control, autonomy, and governance your systems require.
Real Python
Quiz: Python's Format Mini-Language for Tidy Strings
In this quiz, you’ll test your understanding of Python’s Format Mini-Language for Tidy Strings.
By working through this quiz, you’ll revisit how format specifiers work inside f-strings and str.format(), including alignment and width fields, decimal precision, type representations, thousand separators, sign handling, dynamic specifiers, and percentage formatting.
[ Improve Your Python With đ Python Tricks đ â Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Quiz: Structuring Your Python Script
In this quiz, you’ll test your understanding of the video course Structuring Your Python Script.
By working through this quiz, you’ll revisit how to make a Python script executable with a shebang, organize your imports per PEP 8, automatically sort imports with ruff, and define a clear entry point using if __name__ == "__main__".
These habits help you transition from quick experiments in the REPL to writing Python scripts that are easy to read, share, and grow.
[ Improve Your Python With đ Python Tricks đ â Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Python Software Foundation
No Starch Press Humble Bundle: Grab a Deal and Support the PSF!
Curious about leveling up your Python skills, or just getting your feet wet? Pick up a whole set of solid Python books at a great price and support the Python Software Foundation (PSF) at the same time!
No Starch Press, an indie tech-book publisher and long time supporter of the PSF, just announced a new Python-themed Humble Bundle. Grab âPython: The Good Stuff by No Starchâ and pay what you want for all-Python DRM-free ebook titles for Python beginners to pros. And a share of the proceeds from the bundle goes to the PSF! This bundle runs now through June 18th, 2026, so make sure to grab it and share the link with your friends.
âPython: The Good Stuff by No Starchâ includes 15 titles for $36 USD ($583 value đ«š), including Automate the Boring Stuff with Python, 3rd Edition (Al Sweigart), Python Crash Course, 3rd Edition (Eric Matthes), and Practical Deep Learning (Ronald T. Kneusel).
Humble Bundle Pro Tips:
- The promotion has a pay-what-you-want model, so you can choose your preferred pricing tier. Pay less to get fewer items, or pay extra to give more to publishers, Humble, and charity.
- You can customize how your money is disbursed through your Humble Bundle purchase! Scroll down and click Adjust Donation, then click Custom Amount to edit what percentage of your contribution is split between the publishers, Humble Bundle, and charity. This means you can increase the percentage of the proceeds that go to the PSF by up to 14x!
Make sure to grab this awesome bundle of Python books for yourself (or a friend!), and help support the PSF. Thank you, No Starch and Humble Bundle, for making Python education more accessible and supporting the PSF. Happy reading, everyone!
About the Python Software Foundation
The Python Software Foundation is a US non-profit whose mission is to promote, protect, and advance the Python programming language, and to support and facilitate the growth of a diverse and international community of Python programmers. The PSF supports the Python community using corporate sponsorships, grants, and donations. Are you interested in sponsoring or donating to the PSF so we can continue supporting Python and its community? Check out our sponsorship program, donate directly, or contact our team at sponsors@python.org!
Tryton News
Tryton News June 2026
In the last month we focused on fixing bugs, improving the behaviour of things, speeding-up performance issues - building on the changes from our last release. We also added some new features which we would like to introduce to you in this newsletter.
For an in depth overview of the Tryton issues please take a look at our issue tracker or see the issues and merge requests filtered by label.
Changes for the User
Accounting, Invoicing and Payments
We now add an optional journal column on the invoice list view.
Now we add a relate to the invoice model from the period and fiscal year to be able to export or print invoices per period.
We add a delay to the PEPPOL e-document rendering and processing for each service to allow after posting an invoice to record payments which are later rendered in the UBL invoice.
We now raise a generic user error message when failing to parse an imported AEB43 account statement.
Stock, Production and Shipments
Now we can manage products directly in the category form. So we think it is better to now have dedicated views at all but to ensure that we can manage such large Many2Many (also with #14782 (closed)).
Now we let Tryton calculate average lead time for product suppliers based on the effective date of incoming stock moves and the purchase date of the last year.
Parties
Now we make Tryton try to guess the type of contact mechanism when changing value for the standardised types like email, phone, mobile and URL.
User Interface
We now use the search dialogue popup window for deleting records in One2Many or removing records from Many2Many widgets. The remove (delete) button shows a search popup when no records are selected or when more than 20 records are selected. In the search popup are the identical records preselected. Users can refine the search using the filter and the sort order of the popup. And once the popup is validated, the selected records are removed (deleted) from the X2Many field.
We now display the number of records being deleted in the confirmation message. We think it helps the user to realise that they are deleting many records.
Now we allow users to mark notifications as read.
System Data and Configuration
Now we support the country organization (Like EU, ASEAN, âŠ) as a criteria for tax rules.
New Releases
We released bug fixes for the currently maintained long term support series
8.0 and 7.0, and for the penultimate series 7.8.
There are no new release for 6.0 and 7.6 series as they entered their end of life period.
Changes for the System Administrator
We now remove the dependencies to pytz and backports.entry-points-selectable.
Now we update the version of Stripe to 2026-04-22.dahlia.
Changes for Implementers and Developers
We now add support for the age-functionality to SQLite. The age-function returns a time interval instead of an integer (of days) when calculating duration between dates.
1 post - 1 participant
June 01, 2026
The No TitleÂź Tech Blog
Just updated - both Optimize Images and Optimize Images X
This release represents a significant milestone for both Optimize Images and Optimize Images X, marking a coordinated step forward in modernization, dependency cleanup, and internal architecture improvements across the ecosystem.
death and gravity
DynamoDB crash course: part 3 â design patterns
This is the last part of a series covering core DynamoDB concepts. The goal is to help you understand idiomatic usage and trade-offs in under an hour.
In the first part, I summarized DynamoDB's main proposition to its users like so:
data modeling complexity is always preferable to complexity coming from infrastructure maintenance, availability, and scalability
Today, we're looking at the design patterns that help manage this complexity, making the most of its data model and features and working around its limits.
Contents- Composite keys
- Single table design
- GSI overloading
- Partition key sharding
- Sparse indexes
- Base table indexes
- Optimistic locking
Composite keys #
Composite (aka synthetic) keys underpin most other patterns.
The idea is simple: keys don't have to be natural attributes of your data, they can be composed of other attributes that enable specific access patterns. This works both with table and index keys.
How do you compose keys? By string concatenation, of course! Careful with numbers though, they need padding to be useful in sort keys.
Example
To sort lexicographically by more than one attribute,
you group them in a sort key, e.g. {Album}#{Song}.
Or, in single table design,
you distinguish between item types
by prefixing keys with the type,
e.g. album#{Album}.
Or, in partition key sharding,
you spread the load on a GSI partition by splitting one partition key
into multiple ones, e.g. {Genre}#{shard}.
But denormalization has its trade-offs.
For sort key {Album}#{Song},
should Album and Song also be separate attributes?
If yes,
you need to ensure they never change,
but you can use them in indexes
(e.g. a GSI with Album as primary key).
If no,
items can't become inconsistent,
but you need to parse the key to get them.
This was inconvenient enough that DynamoDB finally added multi-attribute keys support to GSIs in 2025 (although not inconvenient enough to also add it to tables).
See also
Single table design #
The AWS guidance is to use as few tables as possible:
As a general rule, you should maintain as few tables as possible in a DynamoDB application. [...] A single table with inverted indexes can usually enable simple queries to create and retrieve the complex hierarchical data structures required by your application.
This culminates in single table design, where you put all entities in the same table, and tell them apart based on the key format, usually using a prefix. With this pattern, one DynamoDB table corresponds to a whole relational database.
The easiest way is to put items related to a top-level entity on the same partition. The main benefit is that joins with the top-level entity become trivial. A second one is that you can sometimes get different entity types in a single query, which can be both faster and cheaper (fewer queries; small items pack into fewer capacity units).
Example
You can group items related to an Artist on the same partition,
with sort keys like
artist, album#{Album}, and song#{Album}#{Song}.
# table Music (partition key: Artist, sort key: sk)
Solar Fields: !btree
'album#Leaving Home': { Genre: Electronic }
'artist': { Variations: [ Solarfields ] }
'song#Leaving Home#Air Song': { Duration: 741 }
'song#Leaving Home#Monogram': { Duration: 944 }
Besides getting items of a single type,
you can also get artist details and albums in a single query
(sk BETWEEN "album#" AND "artist").
But choose wisely
â queries can have only one sort key condition,
so you can't also get album details and songs
in a single query with this schema;
sort keys {Album} and {Album}#{Song} would do it,
at the expense of the first query.
Sometimes, it can be useful to put some sub-entities on dedicated partitions, accepting that joins will have to be done in code.
Example
In the example above, a popular artist with lots of songs can lead to:
- throttling due to partition throughput limits
- slow list songs for artist due to sequential paginated queries
Perhaps it's better to put the songs in each album on separate partitions:
- partition key
artist#{Artist}, sort keyartistoralbum#{Album} - partition key
song#{Artist}#{Album}, sort key{Song}
# table Music (partition key: pk, sort key: sk)
'artist#Solar Fields': !btree
'album#Leaving Home': { Genre: Electronic }
'artist': { Variations: [ Solarfields ] }
'song#Solar Fields#Leaving Home': !btree
'Air Song': { Duration: 741 }
'Monogram': { Duration: 944 }
This spreads the load onto multiple partitions, which should fix throttling.
The downside is that list songs for artist is now a two-step operation: first one query for the albums, then one query per album for the songs. The upside is that the per-album queries can be done in parallel, which wasn't possible before.
A consequence of this design is that you need a GSI to list items of a specific type (otherwise, you have to do a full table scan). Of note, exceeding the GSI partition throughput limit will cause write throttling on the base table; in the absence of a natural high-cardinality GSI partition key, sharding or some other composite key can help.
A final benefit of using a single table is better utilization with provisioned mode: usage gets averaged across entities and tends to be smoother, and spikes can share the same spare capacity.
See also
- NoSQL design
- Data modeling foundations # Single table design
- Relational modeling # JOIN operations
- (blog) Single-table vs. multi-table design in Amazon DynamoDB
- (unofficial) The What, Why, and When of Single-Table Design with DynamoDB
GSI overloading #
GSI overloading is just single table design for indexes â you put different values in the GSI key attributes, depending on item type. This way you can index more attributes than the 20 GSIs per table quota, and it can be cheaper too, since, like with tables, fewer indexes make better use of spare provisioned capacity.
Example
For a table that contains both artist and album items, a single GSI can be used for entirely different purposes:
- artist: partition key
artist#{Country}â list artists by country - album: partition key
album#{Genre}â list albums by genre
# table Music (partition key: Artist, sort key: sk)
2 Bit Pie: !btree
'album#2 Pie Island': { gsi1pk: 'album#Electronic' }
'artist': { gsi1pk: 'artist#United Kingdom' }
Ishome: !btree
'album#Confession': { gsi1pk: 'album#Electronic' }
'artist': { gsi1pk: 'artist#Russia' }
# GSI GSI1 (partition key: gsi1pk, sort key: Artist)
'artist#United Kingdom': !btree
2 Bit Pie: { sk: 'artist' }
'artist#Russia': !btree
Ishome: { sk: 'artist' }
'album#Electronic': !btree
2 Bit Pie: { sk: 'album#2 Pie Island' }
Ishome: { sk: 'album#Confession' }
See also
Partition key sharding #
Sometimes, a partition key composed of multiple natural attributes is not enough to spread the load evenly across partitions; you can deal with this by putting items with the same natural attributes on multiple partitions.
So, what partition key should you use? One option is to use a random suffix from a known range; this allows you to list items for a natural attribute value by doing multiple queries, one for each suffix.
Example
For a table of songs, using Album as the partition key won't work, since not all songs are released on an album; Artist always has a value, but some artists have hundreds or even thousands of songs, which can lead to throttling.
Instead, we can use {Artist}#{randrange(10)} as partition key,
which allows ten times as many items
before we reach throughput limits.
To list an artist's songs:
for shard in range(10):
for item in dynamodb.query(f"{artist}#{shard}"):
yield item
A downside of random suffixes is that you can't get a specific item, because you don't know what its suffix is. A better option is to calculate the suffix from an attribute that you do know, for example using its hash modulo N.
Example
With primary key {Artist}#{hash(Song) % 10)},
we can get a song like this:
def hash(s):
return int.from_bytes(sha256(s.encode()).digest())
shard = hash(song_title) % 10
dynamodb.get_item(f"{artist}#{shard}", song_title)
A lot of times you need to list items by a low-cardinality attribute, so sharding may be even more important for GSIs.
Example
Assuming dedicated album items,
you can list all the albums by putting them
in a single GSI partition key called albums,
but this will definitely cause throttling.
To avoid it,
you can use GSI partition key album#{hash(Album} % 100}
if you don't care about the order,
or something like album#{Album[:2].lower()} if you do
(but likely more sophistication is needed â
th will be a very common album title prefix,
and some album titles don't contain letters at all).
Even if throttling is not an issue (e.g. single infrequent reader), sharding allows you to query multiple partitions in parallel, which can speed up getting the entire result set.
So, how many shards should you have? That depends on the number, size, and how often you access the items, and is also a trade-off â too many shards means additional queries and latency, too few shards means you still overload the partitions sometimes.
Importantly, increasing the number of shards is non-trivial. For tables, you usually need to rebalance the items in place. For indexes, it's cleaner to move to a new index, or if you just need to list items by type, you can put all new items on new shards.
Regardless, you have to support it in code, do a backfill, and orchestrate the migration, which all become more complex if downtime and inconsistencies are not acceptable (e.g. if you expose a pagination token based on LastEvaluatedKey, you may want to support both versions during the switch).
See also
Sparse indexes #
An item with missing index partition/sort key attributes won't appear in the index, and you won't pay for it. This can be used deliberately to query a subset of the items in the table, like those of a specific type or in a specific state.
Example
Assuming dedicated album items,
an alternative way to list all the albums
is to have a GSI with {Album} as partition key,
and just scan the entire index
(the primary key has to be a dedicated attribute
that only albums have,
so that only album items appear in the index).
Or, you can use a dedicated GSI with CoverOf as primary key to list cover songs.
See also
Base table indexes #
In some cases, GSIs won't cut it â maybe you need a strongly consistent index, or need to model a many-to-one relationship (indexes map one item in the base table to one item in the index).
Instead, you can maintain an index in the base table by having additional index items associated with the main item; to guarantee atomic updates, use transactions. You then go from the main item to the index items via a main item attribute, and from the index items to the main item via their partition key.
Example
Songs have different identifiers in external systems, such as ISRC, ISWC, or MBID. To query songs by multiple external ids, you'd structure your database like this:
- song
- partition key
song#{Artist}#{Album} - sort key
{Song} external_{type}:id, ...
- partition key
- external ids
- partition key
external#{type}#{id} - sort key
song#{Artist}#{Album}#{Song}
- partition key
(Alternatively, you could have one sparse index per external id type, but then you lose strong consistency, and risk running out of GSIs).
Note that modeling one-to-many relationships isn't this involved, since it fits neatly into the related-items-same-partition variant of single table design.
See also
- Working with item collections (modeling one-to-many relationships)
- Many-to-many relationships
Optimistic locking #
Optimistic locking is a concurrency control method useful when conflicts are rare, so instead of acquiring a lock to do changes, you check if someone else changed the data right before commiting, as part of an atomic operation.
In DynamoDB, that operation is a conditional write; items get an integer version attribute, and every time you want to update an item, you:
- read the item, including the version
- increment the version and modify the item
- update the item, using a condition expression to ensure the version matches
- if successful, you're done
- else, start over from the beginning
You can also do this in transactions to update groups of related items, like in the base table index pattern above, with only the main item needing a version.
The upside of optimistic locking is that it is faster on average, since updates usually succeed on the first try; for fewer conflicts, use strongly consistent reads.
The downside is that it requires explicit support â it must be possible to start over from the beginning, which complicates logic, especially if you need to interact with other systems besides updating the item (e.g. to send a notification).
See also
- Implementing version control via optimistic locking (Python example)
- Optimistic locking with version number (Java example)
Anyway, that's it for now.
See also
For mode details and examples, check out the official documentation:
- Data modeling
- Data modeling schemas (worked examples)
Learned something new today? Share it with others, it really helps!
Want to know when new articles come out? Subscribe here to get new stuff straight to your inbox!
Real Python
Python sleep(): How to Add Time Delays to Your Code
Sometimes you need to make Python sleep, wait, or pause before running the next line of code. Whether youâre spacing out API requests, pacing a thread, or adding a delay to terminal output, Pythonâs time.sleep() function is the standard tool:
from time import sleep
sleep(3) # Pause execution for 3 seconds
Beyond time.sleep(), Python provides different ways to add time delays depending on the context, including threads, async code, and GUI applications.
By the end of this tutorial, youâll understand that:
time.sleep()suspends execution for a given number of seconds, including fractional values like milliseconds.- Retry decorators use
time.sleep()to add a delay between failed attempts. Event.wait()is the preferred way to add delays in threads because it can be interrupted cleanly.asyncio.sleep()pauses a single coroutine without blocking the rest of your async code.- GUI frameworks like Tkinter provide scheduling methods such as
.after()to avoid freezing the event loop.
The following sections cover each of these approaches with working code examples.
Get Your Code: Click here to download the free sample code youâll use to add time delays to scripts, threads, async code, and GUI apps.
Take the Quiz: Test your knowledge with our interactive âPython time.sleep()â quiz. Youâll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python time.sleep()In this quiz, you'll revisit how to add time delays to your Python programs.
Pause Execution With Python sleep()
Python has built-in support for making your program wait. The time module has a sleep() function that you can use to add a delay by suspending execution of the calling thread for the number of seconds you specify:
>>> import time
>>> time.sleep(3) # Sleep for 3 seconds
Hereâs a quick example of time.sleep() in action:
coffee.py
import time
print("Brewing coffee...")
print("This would take like 3 secs...")
time.sleep(3)
print("Done! Your coffee is ready!")
If you run this script, youâll see a three-second pause between the messages while time.sleep() suspends execution.
You can also pass fractional seconds to time.sleep() for finer-grained durations. Here are some common values:
import time
time.sleep(0.5) # Wait 500 milliseconds
time.sleep(0.001) # Wait 1 millisecond
time.sleep(1.5) # Wait 1.5 seconds
time.sleep(60) # Wait 1 minute
The time.sleep() function isnât perfectly precise. The specified value acts as a minimum delay. The actual pause will almost always be slightly longer in practice due to operating system scheduler overhead and current system load.
You can test how long the sleep lasts by using Pythonâs timeit module:
$ python -m timeit -n 3 "import time; time.sleep(3)"
3 loops, best of 5: 3 sec per loop
Here, you run the timeit module with the -n parameter, which tells timeit how many times to run the statement per repeat. With the default of five repeats, the statement runs 15 times in total (3 Ă 5). timeit then reports the best time across all repeats, which is three seconds per loop, as expected.
For a more realistic example, say you need to monitor whether a website is up. You want to check its status code periodically, but querying the server too often could overload it or get you rate-limited. You can use time.sleep() to space out the checks:
uptime_bot.py
import time
import urllib.request
import urllib.error
CHECK_INTERVAL = 60 # Seconds between checks
def uptime_bot(url):
while True:
try:
urllib.request.urlopen(url)
except urllib.error.HTTPError as e:
# Email admin or log
print(f"HTTPError: {e.code} for {url}")
except urllib.error.URLError as e:
# Email admin or log
print(f"URLError: {e.reason} for {url}")
else:
# Website is up
print(f"{url} is up")
time.sleep(CHECK_INTERVAL)
if __name__ == "__main__":
url = "https://www.google.com/py"
uptime_bot(url)
Read the full article at https://realpython.com/python-sleep/ »
[ Improve Your Python With đ Python Tricks đ â Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Quiz: Regular Expressions: Regexes in Python (Part 1)
In this quiz, you’ll test your understanding of Regular Expressions: Regexes in Python (Part 1).
By working through this quiz, you’ll revisit how to use the re module to search
for patterns, build character classes and anchors, group and capture substrings,
and apply flags like re.IGNORECASE to control matching behavior.
[ Improve Your Python With đ Python Tricks đ â Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Python Bytes
#482 Mr. Beast's episode
<strong>Topics covered in this episode:</strong><br> <ul> <li><strong><a href="https://marcelotryle.com/blog/2026/05/28/cve-2026-48710-a-maintainers-perspective/?featured_on=pythonbytes">CVE-2026-48710: A Maintainer's Perspective</a></strong></li> <li><strong><a href="https://github.com/emanuelef/daily-stars-explorer?featured_on=pythonbytes">daily-stars-explorer</a></strong></li> <li><strong><a href="https://testandcode.org/posts/writing/markdown-to-pdf/?featured_on=pythonbytes">Markdown to pdf with pandoc and typst</a></strong></li> <li><strong><a href="https://github.com/golikovichev/postman2pytest?featured_on=pythonbytes">postman2pytest</a></strong></li> <li><strong>Extras</strong></li> <li><strong>Joke</strong></li> </ul><a href='https://www.youtube.com/watch?v=kNEoGGXppe4' style='font-weight: bold;'data-umami-event="Livestream-Past" data-umami-event-episode="482">Watch on YouTube</a><br> <p><strong>About the show</strong></p> <p><strong>Brian #1: <a href="https://marcelotryle.com/blog/2026/05/28/cve-2026-48710-a-maintainers-perspective/?featured_on=pythonbytes">CVE-2026-48710: A Maintainer's Perspective</a></strong></p> <ul> <li>Marcelo Trylesinski</li> <li>suggested by Lee Luocks</li> <li>Short version: <ul> <li>users of Starlette: upgrade to Starlette 1.0.1</li> <li>security professionals: we canât treat open source projects like corporations</li> </ul></li> <li>This top link is a Starlette security advisory with the title <ul> <li>Missing Host header validation poisons request.url.path, bypassing path-based security checks</li> </ul></li> <li>The CVE apparently caused some negative press targeting starlette.</li> <li>However, âthe vulnerability came from the application pattern and the deployment, never from something Starlette intended.â</li> <li>A quote from an OSTIF article: âThis bug is a classic âresponsibility gapâ where if this maintainer didnât patch, thousands of exposed projects would have to individually secure their projects. In doing this work, theyâve voluntarily taken on the responsibility to protect the ecosystem from long-term systemic harm. As with all open source projects, they owed us nothing and could have left this to be everyone elseâs problem and took the extraordinary steps of helping the ecosystem.â</li> <li>Both X40 D-Sec and Ars Technica expected immediate fixes and responses from Starlette.</li> <li>Thatâs not good. We can do better.</li> </ul> <p><strong>Michael #2: <a href="https://github.com/emanuelef/daily-stars-explorer?featured_on=pythonbytes">daily-stars-explorer</a></strong></p> <ul> <li>Explore the full history of any GitHub repository.</li> <li>đ Full Star History - Complete daily star counts for any repo</li> <li>â° Hourly Stars - Hour-by-hour activity with timezone support</li> <li>đ Compare Repos - Side-by-side comparison of any two repositories</li> <li>đ Activity Timelines - Commits, PRs, Issues, Forks, Contributors over time</li> <li>đ Pin Favorites - Bookmark repos for quick access without retyping</li> <li>đ° Feed Mentions - See when repos were mentioned on HN, Reddit, YouTube, GitHub</li> <li>đŸ Export Data - Download as CSV or JSON</li> <li>đ Dark Mode - Easy on the eyes</li> <li>Try/use it online at <a href="https://emanuelef.github.io/daily-stars-explorer/#/helm/helm"><strong>emanuelef.github.io/daily-stars-explorer</strong></a> or install it for yourself.</li> </ul> <p><strong>Brian #3: <a href="https://testandcode.org/posts/writing/markdown-to-pdf/?featured_on=pythonbytes">Markdown to pdf with pandoc and typst</a></strong></p> <ul> <li>typst suggestion from Matt Harrison</li> <li>Markdown is awesome</li> <li>Pandoc is great for converting markdown to tons of stuff <ul> <li>but for pdf, it goes through LaTeX, which is ⊠yuk (my opinion)</li> </ul></li> <li>Pandoc also can convert to typst</li> <li>And typst creates beautiful pdfs and is way easier (my opinion) to deal with than LaTeX.</li> <li>New tools <ul> <li><code>brew upgrade pandoc</code></li> <li><code>brew install typst</code></li> </ul></li> <li>Now convert <ul> <li><code>pandoc something.md --to typst -o something.typ</code></li> <li><code>typst compile something.typ something.pdf</code></li> </ul></li> </ul> <p><strong>Michael #4: <a href="https://github.com/golikovichev/postman2pytest?featured_on=pythonbytes">postman2pytest</a></strong></p> <ul> <li>via Mikhail</li> <li>Based on <a href="https://www.postman.com/downloads/?featured_on=pythonbytes">postman app</a></li> <li>Convert Postman Collection v2.1 JSON into executable pytest test suites</li> <li>Postman collections document your API. <code>postman2pytest</code> turns that documentation into executable regression tests that run in CI. No manual rewriting, no drift.</li> </ul> <p><strong>Extras</strong>:</p> <ul> <li><a href="https://testandcode.org/posts/meta/a-place-to-write/?featured_on=pythonbytes">New blog, who dis?</a> - <a href="https://testandcode.org?featured_on=pythonbytes">testandcode.org</a> is now on .org and a blog and soon to be a âpublisherâ.</li> </ul> <p><strong>Joke: <a href="https://x.com/PR0GRAMMERHUM0R/status/2059001594662846751?featured_on=pythonbytes">Centering a div</a></strong></p>
Speed Matters
Scandir Rs
layout: post title: scandir-rs tagline: Blazing-fast directory traversal for Python â up to 70Ă faster than os.walk. date: 2026-06-01 08:40:00 +0100 categories: posts ââââââ
scandir-rs: High-Performance Directory Traversal for Python
File system traversal is often a hidden bottleneck.
Whether youâre indexing files, collecting statistics, searching large directory trees, or building developer tools, performance matters.
Thatâs why I created scandir-rs: a Rust-powered Python library designed to be a drop-in replacement for os.walk() and os.scandir(),
while delivering dramatically better performance and additional functionality.
A new version (2.9.9) is available with following changes compared to the version Iâve introduced here the last time (2.7.1):
- Add support for Python 3.13 and 3.14
- Add support for MacOS on ARM64
- Add support for Linux on PPC64 and S390
- Add SSE builds (for older CPUs which have no AVX) for Linux and Windows
- Add optional argument
follow_links. - Changed
skip_hiddento false by default
Why scandir-rs?
Because speed mattersâŠ
đ Significant Performance Improvements
Compared to Pythonâs built-in implementations:
- Walk is up to 13Ă faster on Linux
- Walk is up to 70Ă faster on Windows
- Scandir is up to 6.5Ă faster on both Linux and Windows
When processing millions of files, these speedups can turn minutes into seconds.
Benchmarks results for running scandir in linux-5.9 folder
scandir-rs Walk benchmark on Linux (kernel 5.9)
scandir-rs Walk benchmark on Windows (kernel 5.9)
đ Richer Metadata
Beyond the standard os.walk() and os.scandir() APIs, scandir-rs can return:
- Extended file metadata
- Hardlink detection
- Additional file type classification
- Error collection without interrupting traversal
⥠Background Processing
Long-running scans can run asynchronously in the background, allowing your application to process results while scanning is still in progress.
Installation
pip install scandir-rs
Usage Examples
Directory Statistics
Get fast statistics for an entire directory tree:
import scandir_rs as scandir
print(scandir.Count("/usr").collect())
Extended Statistics
Include additional metadata and hardlink detection:
import scandir_rs as scandir
print(
scandir.Count(
"/usr",
return_type=scandir.ReturnType.Ext
).collect()
)
Background Scanning
Process results while scanning continues in the background:
import scandir_rs as scandir
counter = scandir.Count("/usr")
with counter:
while counter.busy:
results = counter.results()
# Process intermediate results
# Final results as JSON
results = counter.to_json()
Faster os.walk()
A familiar interface with significantly better performance:
import scandir_rs as scandir
for root, dirs, files in scandir.Walk("/usr"):
# Process files
Extended Walk Information
Retrieve additional file categories and error information:
import scandir_rs as scandir
for root, dirs, files, symlinks, other, errors in scandir.Walk(
"/usr",
return_type=scandir.ReturnType.Ext
):
# Process files
On Unix systems, other includes special file types such as pipes and devices.
Faster os.scandir()
Collect all entries at once:
import scandir_rs as scandir
entries, errors = scandir.Scandir("/usr").collect()
Or iterate lazily:
import scandir_rs as scandir
for entry in scandir.Scandir("/usr"):
# Process entry
Extended Metadata
Request detailed information for each directory entry:
import scandir_rs as scandir
for entry in scandir.Scandir(
"/usr",
return_type=scandir.ReturnType.Ext
):
# Process entry
Entries are returned as DirEntryExt objects. Errors are reported as tuples containing:
(relative_path, error_message)
allowing scans to continue even when individual files cannot be accessed.
Benchmark Results
Walk Performance
| Operation | Linux | Windows |
|---|---|---|
Walk vs os.walk |
Up to 13Ă faster | Up to 70Ă faster |
Scandir Performance
| Operation | Linux | Windows |
|---|---|---|
Scandir vs os.scandir |
Up to 6.5Ă faster | Up to 6.5Ă faster |
For detailed benchmark data and methodology, see the benchmark documentation:
https://github.com/brmmm3/scandir-rs/blob/master/pyscandir/doc/benchmarks.md
Get Started
If your application spends time traversing large directory trees, scandir-rs can provide substantial performance improvements with minimal code changes.
The API is intentionally familiar, making migration from os.walk() and os.scandir() straightforward
while unlocking additional capabilities and significantly faster execution.
Source code, documentation, and issue tracker:
https://github.com/brmmm3/scandir-rs
Licensed under the MIT License.
Stéphane Wirtel
PyCon Ireland 2026: The Call for Proposals is Open
![[pycon-ireland-2026-cfp-banner.png]]
TL;DR
PyCon Ireland 2026 takes place on 17 October at Trinity College Dublin. The Call for Proposals is open until 30 August. Two tracks get special focus this year: Python security and AI with Python. First-time speakers are welcome. Financial aid up to âŹ350 is available. Submit at 2026.pycon.ie/cfp.
I’m part of the team organising PyCon Ireland 2026, and the Call for Proposals opened on 25 May. If you’ve been carrying a Python idea around (something you built, broke, learned, or want to share), now is the time to write it up.
Bob Belderbos
AI Human-in-the-loop: News Digest Triage Telegram Bot
In my trend digest article I shared a quick tool to keep on top of tech trends, but it's a one-way street: the model gives information, but I still have to decide what to do with it. Let's build the second half: a Telegram bot that shows me each story, guesses a tag, and lets me confirm or overrule it with one tap.
Human-in-the-loop (HITL): the model proposes, you decide
AI makes suggestions but it can hallucinate, so it's important to have a human in the loop to catch mistakes. The model does the work of categorizing, the human makes the final decision. This is a good example of the control layer above the model and it's where you can make AI more reliable.
This is what we teach in week 4 of our Agentic AI cohort where things come together: expense parsing, AI category suggestion, and the human in the loop to confirm it. This requires the bot to keep state, route responses, and a way to be wrong gracefully. Below is a smaller version so you can get a taste for how this works.
We'll build it in seven steps. Grab the full script up front, or follow along piece by piece.
Step 1: create the bot and get a token
Telegram bots are created by another bot. Open Telegram and search for @BotFather (it has a blue checkmark):
- Send
/newbot. - Give it a display name (anything).
- Give it a username ending in
botthat is globally unique, e.g.alice_trend_bot.
BotFather replies with a token like 123456789:ABCdef.... Treat it like a password. Put it in a .env file next to your script (or export in your shell), together with your OpenAI key:
TELEGRAM_BOT_TOKEN=123456789:ABCdef...
OPENAI_API_KEY=sk-...
If the token ever leaks, send /revoke to BotFather for a fresh one.
Step 2: the dependencies
The whole thing is one file. I use a PEP 723 header so uv run resolves everything into its own environment, no virtualenv to manage. Put this at the top of trend_triage_bot.py:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "python-telegram-bot>=21",
# "openai>=1.40",
# "httpx",
# "python-decouple",
# "pydantic",
# ]
# ///
If you would rather build this inside an existing project, the equivalent is:
uv init && uv add python-telegram-bot openai httpx python-decouple pydantic
Then the imports and a few constants:
import json
import logging
from pathlib import Path
from typing import Literal, Protocol
import httpx
from decouple import config
from openai import AsyncOpenAI
from pydantic import BaseModel
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Message, Update
from telegram.ext import (
Application,
CallbackQueryHandler,
CommandHandler,
ContextTypes,
)
logger = logging.getLogger(__name__)
TAGS = ["read", "lib", "tool", "skip"]
DEFAULT_TOPIC = "rust"
READING_LIST = Path("reading_list.jsonl")
LOBSTERS_FEED = "https://lobste.rs/t/{tag}.json"Step 3: fetch the stories
Lobsters has a per-tag JSON feed, no auth required: https://lobste.rs/t/rust.json returns the latest Rust-tagged stories, .../t/python.json the Python ones, and so on. It's a tighter, more engineering-focused signal than a broad keyword search, and parameterizing the tag is what lets /digest rust and /digest python hit the same code. A Story is just a title and a URL.
Let's set up the model and fetch the latest five stories for a given tag:
from pydantic import BaseModel, HttpUrl
class Story(BaseModel):
title: str
url: HttpUrl
async def fetch_stories(tag: str, *, limit: int = 5) -> list[Story]:
async with httpx.AsyncClient(
timeout=10, headers={"User-Agent": "trend-triage-bot"}
) as http:
response = await http.get(LOBSTERS_FEED.format(tag=tag))
response.raise_for_status()
return [
Story(
title=story["title"],
url=story["url"] or f"https://lobste.rs/s/{story['short_id']}",
)
for story in response.json()[:limit]
if story.get("title")
]
Two small details: Lobsters expects a User-Agent header, and a text/discussion post has an empty url, so we fall back to its comments page (/s/{short_id}), the same pattern you'd use for an HN self-post.
The * in the function signature makes the limit keyword-only, so you have to call fetch_stories("rust", limit=10), which is a nice safeguard against accidentally changing the default.
Step 4: let the LLM propose a tag
The model picks one of TAGS. As the digest topic is variable (/digest rust, /digest python), the tags have to be topic-agnostic, so they describe what a story is (read / lib / tool), not anything Rust- or Python-specific.
Content-type beats intent here: "is this a tool or a library" is answerable from a headline, whereas "will I read this or build with it" depends on me, not the title. And a tag the model can't infer is a tag you end up correcting every time.
And using structured outputs I get typed values back, not strings I have to parse and second-guess; consistent data types are the foundation of reliable AI.
SYSTEM = (
"Tag this software/tech headline with one of: "
"read (an article, post, or tutorial), "
"lib (a library, framework, or package you import), "
"tool (a CLI, app, or utility you run). "
"Use 'skip' only if it is off-topic or clickbait."
)
class TagChoice(BaseModel):
tag: Literal["read", "lib", "tool", "skip"]
class Classifier(Protocol):
async def tag(self, story: Story) -> str: ...
class OpenAIClassifier:
def __init__(self, api_key: str, model: str = "gpt-4o-mini") -> None:
self._client = AsyncOpenAI(api_key=api_key)
self._model = model
async def tag(self, story: Story) -> str:
completion = await self._client.beta.chat.completions.parse(
model=self._model,
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": story.title},
],
response_format=TagChoice,
)
choice = completion.choices[0].message.parsed
return choice.tag if choice else "skip"
def _build_classifier() -> Classifier:
return OpenAIClassifier(config("OPENAI_API_KEY"))
_build_classifier() constructs the client at runtime, not at import; we call it once in main() and stash the result (more on that in step 7).
This decoupling allows a test to inject a fake tagger without touching OpenAI. It's the same lazy-wiring trick I used demonstrating the repository pattern. The Protocol means any class with an async tag() method drops in. Protocols are more flexible here, because they don't require inheritance like ABCs do, so the test double doesn't have to know about the real classifier at all.
Filing a tagged story is a one-liner to a JSONL file. JSONL (or JSON Lines) is a way to store structured data; each line contains a single, valid JSON object.
def save_to_reading_list(story: Story, tag: str) -> None:
with READING_LIST.open("a") as f:
f.write(json.dumps({"tag": tag, **story.model_dump(mode="json")}) + "\n")
Note that model_dump() hands back a pydantic Url object (HttpUrl) that json.dumps can't serialize; mode="json" coerces it to a string first.
Step 5: the keyboard that highlights the guess
The AI's pick is prefixed with >>, but every other tag is one tap away. The callback_data stays plain (tag:read) so the handler never has to strip the decoration:
def triage_keyboard(suggested: str) -> InlineKeyboardMarkup:
buttons = [
InlineKeyboardButton(
f">> {tag}" if tag == suggested else tag,
callback_data=f"tag:{tag}",
)
for tag in TAGS
]
rows = [buttons[i : i + 3] for i in range(0, len(buttons), 3)]
return InlineKeyboardMarkup(rows)
The marker goes in front, and that detail matters: Telegram clips long button labels from the end, so my first attempt, wrapping the tag in >> tool <<, showed up as >> tool⊠with the closing marker eaten. The kind of bug you only catch by testing it on a real phone.
> marker" />
Step 6: two steps, one stashed queue
A simple bot is stateless: message in, reply out. This one is not. Step one (the /digest command) fetches and shows the first story; step two fires later, when I tap a button, and needs the queue from step one. context.user_data is a per-user dict the library keeps between handler calls, so I park the queue there:
async def start_digest(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if update.message is None:
return
topic = context.args[0].lower() if context.args else DEFAULT_TOPIC
await update.message.reply_text(f"Fetching today's {topic} stories...")
try:
context.user_data["queue"] = await fetch_stories(topic)
except httpx.HTTPStatusError:
await update.message.reply_text(
f"No feed for '{topic}'. Try a Lobsters tag like rust, python, or go."
)
return
except httpx.RequestError:
await update.message.reply_text(
"Couldn't reach Lobsters right now â try again in a bit."
)
return
await show_next(update.message, context)
async def show_next(message: Message, context: ContextTypes.DEFAULT_TYPE) -> None:
queue: list[Story] = context.user_data.get("queue", [])
if not queue:
await message.reply_text("Inbox zero. That's all the trends today.")
return
story = queue[0]
suggested = await context.bot_data["classifier"].tag(story)
await message.reply_text(
f"{story.title}\n{story.url}",
reply_markup=triage_keyboard(suggested),
)
context.args is whatever followed the command: /digest python gives ["python"], a bare /digest gives [] and falls back to DEFAULT_TOPIC.
A typo'd topic is a 404 from Lobsters, so I catch HTTPStatusError and reply with a helpful message, otherwise the user would just stare at a digest that never arrives. Validate at the boundary where untrusted input enters.
The second except covers the other failure mode: the request never gets an HTTP response at all. HTTPStatusError only fires once Lobsters answers with a 4xx/5xx â a connect timeout, read timeout, or DNS failure is an httpx.RequestError, which is a sibling of HTTPStatusError, not a subclass. Miss it and a flaky network crashes the handler with a traceback instead of a friendly reply. Catching RequestError covers every transport-level failure (ConnectTimeout, ReadTimeout, ConnectError) in one branch.

Read user_data with .get(...), never [...]. It lives in memory, so if the bot restarts mid-flow the dict is empty and you want a graceful reply, not a KeyError.
context.bot_data is its per-bot sibling: one dict shared across all users. That makes it the right home for the classifier, which holds no per-user state. We build it once in step 7 and read it back here, so every story reuses the same OpenAI client instead of constructing a fresh one each time.
Step 7: the callback, then wire it up
When I tap a button Telegram sends a callback query, not a message. Three rules keep it sane, numbered in the code:
async def on_tag(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
if query is None or query.data is None:
return
await query.answer() # 1. stop the spinner, first thing
_, tag = query.data.split(":", 1) # 2. "tag:read" -> "read"
queue = context.user_data.get("queue", [])
if not queue: # stale button after a restart
await query.edit_message_text("Session expired, send /digest again.")
return
story = queue.pop(0)
if tag != "skip":
save_to_reading_list(story, tag) # the human's final say
await query.edit_message_text( # 3. edit, don't reply
f"Filed under {tag}: {story.title}"
if tag != "skip"
else f"Skipped: {story.title}"
)
await show_next(query.message, context)
Call await query.answer() first or the loading spinner on the button never stops, even when everything else works. Edit the original message instead of replying, or the dead keyboard sits there inviting a second tap on a story you already filed. The same .get(...)-not-[...] rule applies here: an old keyboard from before a restart can still send a tap, and you want a "send /digest again" nudge, not a KeyError.
Routing is by prefix. The pattern="^tag:" is why a future second keyboard (say setcurrency:EUR) would not trip this handler:
async def on_error(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
logger.exception("Handler failed", exc_info=context.error)
def main() -> None:
logging.basicConfig(
format="%(asctime)s %(name)s %(levelname)s %(message)s",
level=logging.INFO,
)
logger.info("Starting trend triage bot, polling for updates")
app = Application.builder().token(config("TELEGRAM_BOT_TOKEN")).build()
app.bot_data["classifier"] = _build_classifier()
app.add_handler(CommandHandler("digest", start_digest))
app.add_handler(CallbackQueryHandler(on_tag, pattern="^tag:"))
app.add_error_handler(on_error)
app.run_polling()
if __name__ == "__main__":
main()
Three things to notice here in main():
-
logging.basicConfig(...)turns on output:run_polling()blocks silently otherwise, so without it a freshly started bot looks dead in the terminal even though it's happily polling. -
app.bot_data["classifier"]builds the OpenAI client once instead of per story. -
add_error_handlermeans a network blip or a rate limit gets logged throughon_errorrather than vanishing into the framework.
Run it
$ export OPENAI_API_KEY=sk-proj-...
$ export TELEGRAM_BOT_TOKEN=...
$ uv run trend_triage_bot.py
2026-06-01 13:19:42,866 __main__ INFO Starting trend triage bot, polling for updates
2026-06-01 13:19:43,190 httpx INFO HTTP Request: POST https://api.telegram.org/bot.../getMe "HTTP/1.1 200 OK"
2026-06-01 13:19:43,242 httpx INFO HTTP Request: POST https://api.telegram.org/bot.../deleteWebhook "HTTP/1.1 200 OK"
2026-06-01 13:19:43,244 telegram.ext.Application INFO Application started
Open your bot in Telegram and send /digest for the default topic, or use a tag like /digest python, /digest rust or any Lobsters tag.
The bot walks you through today's stories one at a time. Tap the highlighted tag to accept the model's guess, or any other tag to overrule it, until you hit inbox zero:

Same bot, any topic: finish the Rust queue, then /digest python and triage that, no code change:

Filed stories land in reading_list.jsonl:
{"tag": "read", "title": "One year of Roto, the compiled scripting language for Rust", "url": "https://blog.nlnetlabs.nl/one-year-of-roto-the-compiled-scripting-language-for-rust/"}
{"tag": "lib", "title": "Announcing Rust 1.96.0", "url": "https://blog.rust-lang.org/2026/05/28/Rust-1.96.0/"}
{"tag": "read", "title": "What kache actually caches", "url": "https://kunobi.ninja/blog/what-kache-actually-caches"}
{"tag": "read", "title": "Creusot helps you prove your Rust code is correct", "url": "https://github.com/creusot-rs/creusot/tree/master"}
{"tag": "tool", "title": "uv must be installed to build a standalone Python distribution", "url": "https://github.com/astral-sh/python-build-standalone/commit/c9c40c56eb53136587f0a32382cad9e5cd8d184a"}
{"tag": "tool", "title": "SPy: an interpreter and a compiler for a statically typed variant of Python", "url": "https://github.com/spylang/spy"}
{"tag": "read", "title": "Opaque Types in Python", "url": "https://blog.glyph.im/2026/05/opaque-types-in-python.html"}
{"tag": "read", "title": "uv is fantastic, but its package management UX is a mess", "url": "https://www.loopwerk.io/articles/2026/uv-ux-mess/"}
That file is the actual output; one JSON object per line, ready to feed into whatever reads it next. The whole script is in this gist.
The interesting question is not whether the model can tag a headline. It's pretty accurate, but it can get it wrong, and that's where you want to have a human in the loop. This has been a simple example to show the flow, but real workflows might involve more interesting things like approving trades, triaging support tickets, or moderating content. The model can do the heavy lifting of making a guess, but the human gets the final say, and that's where the value is.
Keep reading
- Build a daily AI digest in 200 lines of Python
- The control layer is the product, not the model
- How an AI expense agent is actually structured
May 31, 2026
Paolo Melchiorre
My PyCon Italia 2026
A timeline of my PyCon Italia 2026 journey, in Bologna (IT), told through the Mastodon posts I shared along the way.
Kay Hayen
Nuitka Release 4.1
This is to inform you about the new stable release of Nuitka. It is the extremely compatible Python compiler, âdownload nowâ.
This release adds many new features and corrections with a focus on async code compatibility, missing generics features, and Python 3.14 compatibility and Python compilation scalability yet again.
Bug Fixes
Python 3.14: Fix, decorators were breaking when disabling deferred annotations. (Fixed in 4.0.1 already.)
Fix, nested loops could have wrong traces lead to mis-optimization. (Fixed in 4.0.1 already.)
Plugins: Fix, run-time check of package configuration was incorrect. (Fixed in 4.0.1 already.)
Compatibility: Fix,
__builtins__lacked necessary compatibility in compiled functions. (Fixed in 4.0.1 already.)Distutils: Fix, incorrect UTF-8 decoding was used for TOML input file parsing. (Fixed in 4.0.1 already.)
Fix, multiple hard value assignments could cause compile time crashes. (Fixed in 4.0.1 already.)
Fix, string concatenation was not properly annotating exception exits. (Fixed in 4.0.2 already.)
Windows: Fix,
--verbose-outputand--show-modules-outputdid not work with forward slashes. (Fixed in 4.0.2 already.)Python 3.14: Fix, there were various compatibility issues including dictionary watchers and inline values. (Fixed in 4.0.2 already.)
Python 3.14: Fix, stack pointer initialization to
localspluswas incorrect to avoid garbage collection issues. (Fixed in 4.0.2 already.)Python 3.12+: Fix, generic type variable scoping in classes was incorrect. (Fixed in 4.0.2 already.)
Python 3.12+: Fix, there were various issues with function generics. (Fixed in 4.0.2 already.)
Python 3.8+: Fix, names in named expressions were not mangled. (Fixed in 4.0.2 already.)
Plugins: Fix, module checksums were not robust against quoting style of module-name entry in YAML configurations. (Fixed in 4.0.2 already.)
Plugins: Fix, doing imports in queried expressions caused corruption. (Fixed in 4.0.2 already.)
UI: Fix, support for
uv_buildin the--projectoption was broken. (Fixed in 4.0.2 already.)Compatibility: Fix, names assigned in assignment expressions were not mangled. (Fixed in 4.0.2 already.)
Python 3.12+: Fix, there were still various issues with function generics. (Fixed in 4.0.3 already.)
Clang: Fix, debug mode was disabled for clang generally, but only ClangCL and macOS Clang didnât want it. (Fixed in 4.0.3 already.)
Zig: Fix,
--windows-console-mode=attach|disablewas not working when using Zig. (Fixed in 4.0.3 already.)macOS: Fix, yet another way self dependencies can look like, needed to have support added. (Fixed in 4.0.3 already.)
Python 3.12+: Fix, generic types in classes had bugs with multiple type variables. (Fixed in 4.0.3 already.)
Scons: Fix, repeated builds were not producing binary identical results. (Fixed in 4.0.3 already.)
Scons: Fix, compiling with newer Python versions did not fall back to Zig when the developer prompt MSVC was unusable, and error reporting could crash. (Fixed in 4.0.4 already.)
Zig: Fix, the workaround for Windows console mode
attachordisablewas incorrectly applied on non-Windows platforms. (Fixed in 4.0.4 already.)Standalone: Fix, linking with Python Build Standalone failed because
libHacl_Hash_SHA2was not filtered out unconditionally. (Fixed in 4.0.4 already.)Python 3.6+: Fix, exceptions like
CancelledErrorthrown into an async generator awaiting an inner awaitable could be swallowed, causing crashes. (Fixed in 4.0.4 already.)Fix, not all ordered set modules accepted generators for update. (Fixed in 4.0.5 already.)
Plugins: Disabled warning about rebuilding the
pytokensextension module. (Fixed in 4.0.5 already.)Standalone: Filtered
libHacl_Hash_SHA2from link libs unconditionally. (Fixed in 4.0.5 already.)Debugging: Disabled unusable unicode consistency checks for Python versions 3.4 to 3.6. (Fixed in 4.0.5 already.)
Python3.12+ Avoided cloning call nodes on class level which caused issues with generic functions in combination with decorators. (Added in 4.0.5 already.)
Python 3.12+: Added support for generic type variables in
async deffunctions. (Added in 4.0.5 already.)UI: Fix, flushing outputs for prompts was not working in all cases when progress bars were enabled. (Fixed in 4.0.6 already.)
UI: Fix, unused variable warnings were missing at C compile time when using
zigas a C compiler. (Fixed in 4.0.6 already.)Scons: Fix, forced stdout and stderr paths as a feature was broken. (Fixed in 4.0.6 already.)
Fix, replacing a branch did not accurately track shared active variables causing optimization crashes. (Fixed in 4.0.7 already.)
macOS: Fix, failed to remove extended attributes because files need to be made writable first. (Fixed in 4.0.7 already.)
Fix, dict
popandsetdefaultusing with:=rewrites lacked exception-exit annotations for un-hashable keys. (Fixed in 4.0.8 already.)Python 3.13: Fix, the
__parameters__attribute of generic classes was not working. (Fixed in 4.0.8 already.)Python 3.11+: Fix, starred arguments were not working as type variables. (Fixed in 4.0.8 already.)
Python2: Fix,
FileNotFoundErrorcompatibility fallback handling was not working properly. (Fixed in 4.0.8 already.)Compatibility: Fix, loop ownership check in value traces was missing, causing issues with nested loops.
Windows: Improved
--windows-console-mode=attachto properly handle console handles, enabling cases likeos.systemto work nicely.Python2: Fix, there was a compatibility issue where providing default values to the
mkdtempfunction was failing.Windows: Fix, there were spurious issues with C23 embedding in 32-bit MinGW64 by switching to
coff_objresource mode for it as well.Plugins: Fix, the
post-import-codeexecution could fail because the triggering sub-package was not yet available insys.modules.UI: Fix, listing package DLLs with
--list-package-dllswas broken due to recent plugin lifecycle changes.UI: Fix,
--list-package-exewas not working properly on non-Windows platforms failing to detect executable files correctly.UI: Handled paths starting with
{PROGRAM_DIR}the same as a relative path when parsing the--onefile-tempdir-specoption.Plugins: Followed multiprocessing
forkserverchanges for newer Python versions.Python 3.12+: Fix, generic class type parameters handling was incorrect.
Python 3.12: Fix, deferred evaluation of type aliases was failing.
Python 3.12+: Aligned
sumbuilt-in float summation with CPythonâs compensated sum for better accuracy.Python 3.10+: Fix, uncompiled coroutine
throw()return handling was incorrect, restoring completed coroutine results viaStopIteration.valuerather than exposing them as ordinary return values to the outer await chain.Python 3.13+: Fix, uncompiled coroutine
cancel()/awaitsuspension handling was incorrect, improved to ensure integration compatibility.macOS: Made finding
create-dmgmore robustly by also checking the Homebrew path for Intel and fromPATHproperly.Compatibility: Fix, class frames were not exposing frame locals.
UI: Detected
static-libpythonproblems, which affected some forms of Anaconda.Distutils: Rejected
--projectmixed with--mainarguments as it is not useful.macOS: Fix,
zigfromPATHor fromziglangwas not being used.Distutils: Fix, the wrong
module-rootconfig value was being checked foruvbuild backend.macOS: Fix, was attempting to change removed (rejected) DLLs, which of course failed and errored out.
Python 3.14: Fix, tuple reuse was not fully compatible, potentially causing crashes due to outdated hash caches.
Fix, fake modules were still being attempted to located when imported by other code, which could conflict with existing modules.
Python 3.5+: Fix, failed to send uncompiled coroutines the sent in value in
yield from.Fix, older
gcccompilers lacking newer intrinsic methods had compilation issues that needed to be addressed.Standalone: Fix, multiphase module extension modules with post-load code were not working properly.
Fix, Avoid using the non-inline copy of
pkg_resourceswith the inline copy of Jinja2. These could mismatch and cause errors.Fix, loops could make releasing of previous values very unclear, causing optimization errors.
Fix,
incbinresource mode was not working with oldgccC++ fallback.Python 3.4 to 3.6: Fix, bytecode demotion was not working properly for these versions, also bytecode only files not working.
Plugins: Added a check for the broken
patchelfversions 0.10 and 0.11 to prevent breaking Qt plugins.Android: Allowed
patchelfversion 0.18 on Android.Windows: Fix, the header path for self uninstalled Python was not detected correctly.
Release: Fix, inclusion of the
pkg_resourcesinline copy for Python 2 to source distributions was missing.UI: Detected the OBS versions of SUSE Linux better.
Suse: Allowed using
patchelf0.18.0 there too.Python 3.11: Fix, package and module dicts were not aligned close enough to avoid a CPython bug.
Fix, unbound compiled methods could crash when called without an object passed.
Standalone: Fix, multiphase module extension modules with postload. (Fixed in 4.0.8 already.)
Onefile: Fix, while waiting for the child, it may already be terminated.
macOS: Removed existing absolute rpaths for Homebrew and MacPorts.
Python 3.14: Avoided warning in CPython headers.
Python 3.14: Followed allocator changes more closely.
Compatibility: Avoided using
pkg_resourcesfor Jinja2 template location for loading.No-GIL: Applied some bug fixes to get basic things to work.
Package Support
Standalone: Add support for newer
paddleversion. (Added in 4.0.1 already.)Standalone: Add workaround for refcount checks of
pandas. (Fixed in 4.0.1 already.)Standalone: Add support for newer
h5pyversion. (Added in 4.0.2 already.)Standalone: Add support for newer
scipypackage. (Added in 4.0.2 already.)Plugins: Revert accidental
os.getenvoveros.environ.getchanges in anti-bloat configurations that stopped them from working. Affected packages arenetworkx,persistent, andtensorflow. (Fixed in 4.0.5 already.)Standalone: Added missing DLLs for
openvino. (Added in 4.0.7 already.)Enhanced the package configuration YAML schema by adding the
relative_toparameter forfrom_filenamesDLL specification, avoiding error-prone purely relative paths.Standalone: Fix,
flet_desktopapp assets were missing, now preserving the packaged runtime and sidecar DLLs.Standalone: Added support for the
tyropackage.Standalone: Added data files for the
perfettopackage.Standalone: Added support for
anyioprocess forking.Standalone: Added support for the
plotly.graphpackage.Anaconda: Fix, dependencies for the
numpyconda package on Windows were incorrect.Plugins: Enhanced the auto-icon hack in PySide6 to use compatible class names.
Standalone: Fix, Qt libraries were duplicated with
PySide6WebEngine framework support on macOS.Plugins: Fix, automatic detection of
mypycruntime dependencies was including all top level modules of the containing package by accident. (Fixed in 4.0.5 already.)Anaconda: Fix,
delvewheelplugin was not working with Python 3.8+. This enhances compatibility with installed PyPI packages that use it for their DLLs. (Fixed in 4.0.6 already.)Plugins: Fix, our protection workaround could confuse methods used with
PySide6.
New Features
UI: Added the
--recommended-python-versionoption to display recommended Python versions for supported, working, or commercial usage.UI: Add message to inform users about
Nuitka[onefile]if compression is not installed. (Added in 4.0.1 already.)UI: Add support for
uv_buildin the--projectoption. (Added in 4.0.1 already.)Onefile: Allow extra includes as well. (Added in 4.0.2 already.)
UI: Add
nuitka-project-setfeature to define project variables, checking for collisions with reserved runtime variables. (Added in 4.0.2 already.)Scons: Added new option to select
--reproduciblebuilds or not. (Added in 4.0.6 already.)Python 3.10+: Added support for
importlib.metadata.package_distributions(). (Added in 4.0.8 already.)Plugins: Added support for the multiprocessing
forkservercontext. (Added in 4.0.8 already, for 4.1 Python 3.6 and earlier, as well as 3.14 support were added too.)Reports: Added structured resource usage (
rusage) performance information to compilation reports.Reports: Included individual module-level C compiler caching (
ccache/clcache) statistics in compilation reports.Added support for detecting and correctly resolving the Python prefix for the
PyEnv on HomebrewPython flavor.macOS: Added support for
rusageinformation for Scons.UI: Added the
__compiled__.extension_filenameattribute to give the real filename of the containing extension module.Windows: Added support for
--clangor ARM. (Added in 4.0.8 already.)Windows: Added support for resources names as not just integers, important when we copy them from template files.
MacPorts: Added basic support for this Python flavor. More work will be needed to get it to work fully though.
Optimization
Avoid including
importlib._bootstrapandimportlib._bootstrap_external. (Added in 4.0.1 already.)Linux: Cached the
syscallused for time keeping during compilation to avoid loadinglibcfor each trace. (Added in 4.0.8 already.)UI: Output a warning for modules that remain unfinished after the third optimization pass.
Added an extra micro pass trigger when new variables are introduced or variable usage changes severely, ensuring optimizations are fully propagated, avoiding unnecessary extra full passes.
Provided scripts to compile Python statically with PGO tailored for Nuitka on Linux, Windows, and macOS.
Added support for running the Data Composer tool from a compiled Nuitka binary without spawning an uncompiled Python process.
Enhanced the usage of
vectorcallforPyCFunctionobjects by directly checking for its presence instead of relying purely on flags, allowing more frequent use of this faster execution path.Cached frequently used declarations for top-level variables to speed up C code generation.
Sped up trace collection merging by avoiding unnecessary set creation and using a set instead of a list for escaped traces.
Optimized plugin hook execution by tracking overloaded methods and added an option to show plugin usage statistics.
Improved performance of module location by avoiding unnecessary module name reconstruction and redundant filesystem checks for pre-loaded packages.
Improved the caching of distribution name lookups to effectively avoid repeated IO operations across all package types.
Plugins: Cached callback plugin dispatch for
onFunctionBodyParsingandonClassBodyParsingto skip argument computation when no plugin overrides them.Python 3.13: Handled sub-packages of
pathlibas hard modules.Handled hard attributes through merge traces as well.
Made constant blobs more compact by avoiding repeated identifiers and unnecessary fields.
Enhanced Python compilation scripts further. (Fixed in 4.0.8 already.)
Recognized late incomplete variables better. (Fixed in 4.0.8 already.)
Made constant blobs more compact. (Fixed in 4.0.8 already.)
Optimized calls with only constant keywords and variable posargs too.
Anti-Bloat
Fix, memory bloat occurred when C compiling
sqlalchemy. (Fixed in 4.0.2 already.)Avoid using
pydocinPySimpleGUI. (Added in 4.0.2 already.)Avoided using
doctestfromzodbpickle. (Added in 4.0.5 already.)Avoided inclusion of
cythonwhen usingpyav. (Added in 4.0.7 already.)Avoided including
typing_extensionswhen usingnumpy. (Added in 4.0.7 already.)
Organizational
UI: Relocated the warning about the available source code of extension modules to be evaluated at a more appropriate time.
Debian: Remove recommendation for
libfuse2package as it is no longer useful.Debian: Used
platformdirsinstead ofappdirs.Debugging: Removed Python 3.11+ restriction for
clang-formatas it is available everywhere, even Python 2.7, and we still want nicely formatted code when we read things. (Added in 4.0.6 already.)Removed no longer useful inline copy of
wax_off. We have our own stubs generator project.Release: Added missing package to the CI container for building Nuitka Debian packages.
Developer: Updated AI instructions for creating Minimal Reproducible Examples (MRE) to skip unneeded C compilation.
Debugging: Added an internal function for checking if a string is a valid Python identifier.
AI: Added a task in Visual Studio Code to export the currently selected Python interpreter path to a file, making it available as âpythonâ and âpipâ matching the selected interpreter. This makes it easier to use a specific version with no instructions needed.
AI: Updated the rules to instruct AI to only generate useful comments that add context not present in the code.
Containers: Added template rendering support for Jinja2 (
.j2) container files in our internal Podman tools.Projects: Clarified the current status and rationale of Python 2.6 support in the developer manual.
Debugging: Added experimental flag
--experimental=ignore-extra-micro-passto allow ignoring extra micro pass detection.Visual Code: Added integration scripts for
bashandzshautocompletion of Nuitka CLI options. These are now also integrated into Visual Studio Code terminal profiles and the Debian package.RPM: Included the Python compile script for Linux.
RPM: Removed the requirement for
distutilsin the spec.
Tests
Install only necessary build tools for test cases.
Avoided spurious failures in reference counting tests due to Python internal caching differences. (Fixed in 4.0.3 already.)
Fix, the parsing of the compilation report for reflected tests was incorrect.
Python 3.14: Ignored a syntax error message change.
Python 3.14: Added test execution support options to the main test runner to use this version as well.
Fix, the runner binary path was mishandled for the third pass of reflected compilations.
Removed the usage of obsolete plugins in reflected compilation tests.
Debugging: Prevented boolean testing of
namedtuplesto avoid unexpected bugs.Added the
Testsuffix to syntax test files and disabled âpythonâ mode and spell checking for them to resolve issues reported in IDEs.Fix, newline handling in diff outputs from the output comparison tool was incorrect.
Covered
post-import-codefunctionality with a new subpackage test case.Prevented the program test suite from running an unnecessary variant to save execution time.
macOS: Ignored differences from GUI framework error traces in headless runs in output comparisons.
Reflected test for Nuitka, where it compiles itself and compares its operation has been restored to functional state.
Used the new method to clear internal caches if available for reference counts.
Disabled running nested loops test with Python 2.6.
Containers: Detected Python 2 defaulting containers in Podman tooling.
Cleanups
UI: Fix, there was a double space in the Windows Runtime DLLs inclusion message. (Fixed in 4.0.1 already.)
Onefile: Separated files and defines for extra includes for onefile boot and Python build.
Scons: Provided nicer errors in case of âunsetâ variables being used, so we can tell it.
Refactored the process execution results to correctly utilize our
namedtuplesvariant, that makes it easier to understand what code does with the results.Quality: Enabled automatic conversion of em-dashes and en-dashes in code comments to the autoformat tool. AI wonât stop producing them and they can cause
SyntaxErrorfor older Python versions, nor is unnecessarily using UTF-8 welcome.Ensured that cloned outline nodes are assigned their correct names immediately upon creation, that avoids inconsistencies during their creation.
Quality: Updated to the latest versions of
blackand adopted a fasterisortexecution by caching results.Quality: Modified the PyLint wrapper to exit gracefully instead of raising an error when no matching files require checking.
Quality: Avoided checking YAML package configuration files twice, since autoformat already handles them.
Quality: Ensured that YAML package configuration checks output the original filename instead of the temporary one when a failure occurs.
Quality: Prevented pushing of tags from triggering git pre-push quality checks.
Quality: Silenced the output of
optipngandjpegoptimduring image optimization auto-formatting.Visual Code: Added the generated Python alias path file to the ignore list.
Quality: Enabled auto-formatting for the Nuitka devcontainer configuration file.
Watch: Avoided absolute paths in compilation to make reports more comparable across machines.
Quality: Changed
mdformatchecks to run only once and silently.Scons: Disabled format security errors in debug mode and moved Python-related warning disables into common build setup code.
Quality: Updated to the latest
deepdiffversion.Scons: Avoided MSVC telemetry since it can produce outputs that break CI.
Debugging: Enhanced non-deployment handler for importing excluded modules.
Split import module finding functionality into more pieces for enhanced readability.
Debugging: Added more assertions for constants loading and checking.
macOS: Dropped the
universaltarget arch.Debugging: Added more traces for deep hash verification.
Summary
This release builds on the scalability improvements established in 4.0, with enhanced Python 3.14 support, expanded package compatibility, and significant optimization work.
The --project option seems usable now.
Python 3.14 support remains experimental, but only barely made the cut, and probably will get there in hotfixes. Some of the corrections came in so late before the release, that it was just not possible to feel good about declaring it fully supported just yet.
May 30, 2026
Talk Python to Me
#550: AI Contributions and Maintainer Load in Open Source
You wake up, brew the coffee, open GitHub, and there it is. Another pull request on your open source project. Thirteen thousand lines added. No issue filed first. No discussion. Just "here, please review this for me." <br/> <br/> Over the past year, GitHub activity has spiked roughly twelve times in a few short months, and a huge chunk of that signal is landing on the same small group of maintainers who were already stretched thin. The curl bug bounty got buried under AI-generated noise. Jazzband, the home of Django classics like pip-tools and the Django debug toolbar, hit what its maintainer called an "apocalypse" and started sunsetting. Even CPython just shipped fresh guidelines on AI-assisted contributions this week. <br/> <br/> So what does all of this actually look like from the receiving end of the pull request? <br/> <br/> On this episode, Paolo Melchiorre joins us to tell that story from inside the maintainer's chair. Paolo is a director of the Django Software Foundation, an organizer of PyCon Italy, a Django Girls coach, and he has spent the past year carefully collecting examples of how AI is reshaping open source contributions. The good, the bad, and the extra fingers. <br/> <br/> We dig into his PyCon US talk on AI-assisted contributions and maintainer load, why AI is best understood as an amplifier rather than a new kind of contributor, the wildly different policies across 86 open source foundations, whether projects banning AI today are reacting to last year's models.<br/> <br/> <strong>Episode sponsors</strong><br/> <br/> <a href='https://talkpython.fm/agentfield-page'>AgentField AI</a><br> <a href='https://talkpython.fm/training'>Talk Python Courses</a><br/> <br/> <h2 class="links-heading mb-4">Links from the show</h2> <div><strong>Guest</strong><br/> <strong>Paolo Melchiorre</strong>: <a href="https://github.com/pauloxnet?featured_on=talkpython" target="_blank" >github.com</a><br/> <br/> <strong>DSF</strong>: <a href="https://www.djangoproject.com/foundation/?featured_on=talkpython" target="_blank" >www.djangoproject.com</a><br/> <strong>djangonaut-space</strong>: <a href="https://djangonaut.space/?featured_on=talkpython" target="_blank" >djangonaut.space</a><br/> <strong>PyCon Italia</strong>: <a href="https://2026.pycon.it/en?featured_on=talkpython" target="_blank" >2026.pycon.it</a><br/> <strong>uDjango</strong>: <a href="https://github.com/pauloxnet/uDjango?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>My PyCon US 2026 post</strong>: <a href="https://www.paulox.net/2026/05/21/my-pycon-us-2026/?featured_on=talkpython" target="_blank" >www.paulox.net</a><br/> <strong>AI-Assisted Contributions and Maintainer Load</strong>: <a href="https://www.paulox.net/2026/05/15/pycon-us-2026/?featured_on=talkpython" target="_blank" >www.paulox.net</a><br/> <strong>Senior Engineer Tries Vibe Coding</strong>: <a href="https://www.youtube.com/watch?v=_2C2CNmK7dQ" target="_blank" >www.youtube.com</a><br/> <strong>Code Rabbit AI PR Reviews</strong>: <a href="https://www.coderabbit.ai?featured_on=talkpython" target="_blank" >www.coderabbit.ai</a><br/> <strong>GitHub Usage Graphs</strong>: <a href="https://github.blog/news-insights/company-news/an-update-on-github-availability/?featured_on=talkpython" target="_blank" >github.blog</a><br/> <strong>Update on CPython's AI Policies</strong>: <a href="https://fosstodon.org/@mariatta/116610508567734365" target="_blank" >fosstodon.org</a><br/> <strong>High-Quality Chaos from Curl</strong>: <a href="https://daniel.haxx.se/blog/2026/04/22/high-quality-chaos/?featured_on=talkpython" target="_blank" >daniel.haxx.se</a><br/> <strong>The Generative AI Policy Landscape in Open Source</strong>: <a href="https://redmonk.com/kholterhoff/2026/02/26/generative-ai-policy-landscape-in-open-source/?featured_on=pythonbytes" target="_blank" >redmonk.com</a><br/> <br/> <strong>Watch this episode on YouTube</strong>: <a href="https://www.youtube.com/watch?v=1RJ1kkpTdow" target="_blank" >youtube.com</a><br/> <strong>Episode #550 deep-dive</strong>: <a href="https://talkpython.fm/episodes/show/550/ai-contributions-and-maintainer-load-in-open-source#takeaways-anchor" target="_blank" >talkpython.fm/550</a><br/> <strong>Episode transcripts</strong>: <a href="https://talkpython.fm/episodes/transcript/550/ai-contributions-and-maintainer-load-in-open-source" 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>
Bob Belderbos
The control layer is the product, not the model
Gary Bernhardt posted something this week that names a phenomenon we're teaching in our agentic AI cohort:
Everyone seems fixated on the models, but I think there's so much low-hanging fruit in the control layer above the model. "Agent" and "harness" sell that layer short. There's so much more that we can do beyond "read input, send to model, run commands it returns."
He's right. The model is a brain in a jar. Useful, fast, occasionally wrong, stateless. Everything that turns it into a product lives in the code that wraps it: the routing, the validation, the state, the audit trail. Gary calls that the control layer. I'm stealing the term.
One of the replies under the tweet nailed the design goal in a single question: do you actually know what the agent is going to do?
That's what a control layer buys you. Not magic, not autonomy, predictability. A workflow where, by the time the model is called, the next move is already constrained to something safe.
Why "agent" and "harness" sell it short
When a developer says "I'm building an agent", they usually mean a while True loop that pings an LLM, parses a tool call, runs it, feeds the result back, and repeats. That pattern works for demos. It rarely survives contact with a real workflow.
The word "harness" makes the wrapping code sound passive, a strap that holds the model in place. It's actually the control layer where the engineering happens. The model is a function call inside it. Once you flip that mental model, you stop asking "which LLM should I use" and start asking "what guarantees does my control layer make?" and "how can I make the inherently unpredictable model fit into a predictable workflow?"
These are the questions production teams have to answer.
Pattern 1: deterministic state machines, not unconstrained agents
An agent without constraints decides what to do next from inside the model. A state machine decides outside the model and gives the model one bounded job at each step. The pipeline runs categorize â validate â confirm â persist, and the LLM only ever gets called inside one of those buckets.
This shifts control flow back to your code, where you can test it, log it, and reason about it. The expense agent we build in our cohort, which I broke down in How an AI expense agent is actually structured, follows exactly this pattern: Protocol-defined LLM boundary, Pydantic-validated outputs, service layer holds the state, human-in-the-loop (HITL) confirms before anything writes. Four layers, no free-roaming agent, constraints at every step.
Pattern 2: the model behind a typed boundary
The model should be one swappable function call inside your control layer, not a dependency threaded through every layer. In our cohort the LLM lives behind a Python Protocol: a small interface the service layer depends on, so nothing downstream knows or cares whether the call goes to OpenAI or Anthropic.
Once the boundary is a Protocol, the decisions people reach for "routing" to solve become wiring instead of rewrites. Picking a cheap fast model for a 12-way classification and saving the expensive one for hard reasoning is a one-line change. Falling back to a second provider when the first is rate-limited is a small factory, not a refactor. Swapping OpenAI for Anthropic, two SDKs that disagree on almost every detail, touches one file because the boundary absorbs the difference.
And it makes the whole pipeline testable. Tests pass a mock that satisfies the Protocol, so you exercise every path without an API call incurring latency or cost.
Pattern 3: evaluators and guardrails
The model's output is not the user's output. Between the two sits validation: schema checks, business rules, PII filters, sometimes a second model grading the first one's work.
This is the generator-evaluator split and it's an important pattern (apart from HITL) I've found for AI code that has to be right. The generator proposes. The evaluator approves or rejects. When the evaluator rejects, control loops back with feedback, not a stack trace.
It's also the layer that catches the worst failure mode of multi-step agents. What production AI agents actually require goes deeper on the four questions the control layer answers before any action runs: state, idempotency, audit, rollback.
Pattern 4: structured generation
A raw string from the model is the start of your problems. You can't store it, validate it, or test it well. The fix is to constrain output at the boundary: the model is allowed to speak, but only in shapes your code understands.
Where the typed boundary in Pattern 2 decides where the model sits in your code, structured generation decides what shape it's allowed to emit.
Pydantic plus your model's structured outputs gives you typed data instead of strings, which means the next layer of your control flow becomes ordinary Python.
I covered this in Build the data layer before you touch the LLM, explaining why we teach students to build the schema before they make a single API call.
The frontier models make the headlines. The control layer ships the product. Gary's tweet names a gap that has been there the whole time, between the people optimizing benchmarks and the people building products. The control layer is the product, not the model. If you want to build AI products, that's where you need to spend your time.
If you want a working walkthrough of the patterns above, the 10 small agentic AI exercises Juanjo and I shipped, run in the browser and cover the arc from a 3-line model call to a complete loop with HITL. They're the conceptual map.
The cohort is the same map, end to end. Six weeks, no frameworks, the control layer built explicitly, with code review at every step. By the end you can answer that one question: you know what your agent is going to do.
May 29, 2026
Real Python
The Real Python Podcast â Episode #297: Improving Python Through PEPs and Protocols
Have you ever been confused by the naming of modules you're importing from a package? Is there a standard way to organize and name your Python virtual environments? This week on the show, Brett Cannon returns to discuss the Python Enhancement Proposals (PEPs) he's been working on recently.
[ Improve Your Python With đ Python Tricks đ â Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Quiz: Python's assert: Debug and Test Your Code Like a Pro
In this quiz, you’ll test your understanding of Python’s assert: Debug and Test Your Code Like a Pro.
By working through this quiz, you’ll revisit how assertions help you debug, test, and document your code, when to disable them in production, and which common pitfalls to avoid.
[ Improve Your Python With đ Python Tricks đ â Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Ned Batchelder
Snake way for ducklings
This is the mascot for Boston Python. It’s called Snake Way for Ducklings:

My son Ben drew it, which makes me very happy. He also drew Sleepy Snake. Wearing this image on a shirt around PyCon, I had to explain it a number of times. People in Boston understand it almost immediately, but others need more background.
In 1941, Robert McCloskey wrote a children’s book called Make Way for Ducklings. It’s a classic, selling millions of copies and never going out of print. We read it to our own children growing up many times.
The book is the story of Mrs. Mallard making her way through Boston guiding her eight ducklings (Jack, Kack, Lack, Mack, Nack, Oack, Pack, and Quack) to a pond in Boston’s Public Garden. It has charming pencil illustrations:
The book led to a sculpture in the Public Garden near the actual pond:
The sculpture is sized and placed for kids to play on, and is widely known and beloved in Boston. The ducks are dressed in costumes for all kinds of occasions: holidays, sports events, even Star Wars day. On Mother’s Day, there’s a duckling parade: families bring their children dressed as ducklings. In Boston, the ducklings are a big deal.
And it’s not just fiction.
So it seemed natural to Ben to riff on the ducklings for Boston Python. One observer thought a snake eating the ducklings seemed kind of dark, but you can see the ducklings are still quacking, so they are fine!

BTW, Boston also has Duck Boat tours, but that’s completely different.
PyCon Ireland
Call for Proposals Now Open
We’re excited to announce that the Call for Proposals (CFP) for PyCon Ireland 2026 is now open!
We Want to Hear From You
Whether you’re a first-time speaker or an experienced presenter, we’d love to hear your Python story. We welcome proposals on a wide range of topics, including:
- Web Development â Django, Flask, FastAPI, and beyond
- Data Science & ML â pandas, scikit-learn, PyTorch, and more
- DevOps & Infrastructure â CI/CD, containers, cloud-native Python
- Security â Best practices for secure Python applications
- Community & Education â Teaching Python, diversity initiatives, community building
- Core Python â Language features, CPython internals, typing
Talk Formats
- Full talks (30 minutes) â Deep dives into a topic
- Lightning talks (5 minutes) â Quick, focused presentations
How to Submit
Visit our proposal submission page to submit your talk. The deadline is 30 August 2026.
Don’t hesitate to reach out at contact@python.ie if you have any questions about your proposal.
Seth Michael Larson
How much âSuper Marioâ per year?
It's impossible to objectively quantify art, but we try anyway. For example: Is âSuper Marioâ a good video-game franchise?
Looking at review scores, Super Mario includes some of the most universally-acclaimed games ever published: Galaxy, Galaxy 2, and Odyssey are respectively the #4, #5, and #13 highest ranking video-games of all time on Metacritic, all with 97 overall. Chances seem good?
What if we tried quantifying art in a different and slightly more reductive way? This blog post introduces and calculates a new unit: âSuper Mario per yearâ. If you enjoy this franchise like I do then this unit is of particular importance to you.
Calculating âSuper Mario per yearâ
There have been ~19 titles (and two add-ons) published to what I consider the "main-line" Super Mario games, both 2D and 3D. Below is a table with every title, the year it was published, and the approximate duration to play. This last column is the most subjective, because thereâs speed-runners, casual players, completionists. If you think any value is way off, send me an email.
| Game | 2D/3D | Platform | Year | Time to Beat |
|---|---|---|---|---|
| Super Mario Bros. | 2D | NES | 1985 | 5 hours |
| Super Mario Bros. Lost Levels | 2D | NES | 1986 | 10 hours |
| Super Mario Bros. 2 | 2D | NES | 1988 | 5 hours |
| Super Mario Bros. 3 | 2D | NES | 1988 | 5 hours |
| Super Mario Land | 2D | GB | 1989 | 5 hours |
| Super Mario World | 2D | SNES | 1990 | 10 hours |
| Super Mario Land 2 | 2D | GB | 1992 | 10 hours |
| Super Mario 64 | 3D | N64 | 1996 | 15 hours |
| Super Mario Sunshine | 3D | GC | 2002 | 20 hours |
| New Super Mario Bros. | 2D | DS | 2006 | 10 hours |
| Super Mario Galaxy | 3D | Wii | 2007 | 15 hours |
| New Super Mario Bros. Wii | 2D | Wii | 2009 | 5 hours |
| Super Mario Galaxy 2 | 3D | Wii | 2010 | 15 hours |
| Super Mario 3D Land | 3D | 3DS | 2011 | 15 hours |
| New Super Mario Bros. 2 | 2D | 3DS | 2012 | 10 hours |
| New Super Mario Bros. U | 2D | Wii U | 2012 | 15 hours |
| Super Mario 3D World | 3D | Wii U | 2013 | 20 hours |
| Super Mario Odyssey | 3D | Switch | 2017 | 25 hours |
| Bowser's Fury (Super Mario 3D World) | 3D | Switch | 2021 | 5 hours |
| Super Mario Bros. Wonder | 2D | Switch | 2023 | 15 hours |
| Meetup at Bellabel Park (Super Mario Bros. Wonder) | 2D | Switch | 2026 | 5 hours |
Using the table above we can calculate approximately how much new Super Mario gameplay is published on average per year.
| Year | All-Time Avg | 10-Year Avg (10YA) | 2D (10YA) | 3D (10YA) |
|---|---|---|---|---|
| 1985 | 5.0 | 5.0 | 5.0 | 0.0 |
| 1986 | 7.5 | 7.5 | 7.5 | 0.0 |
| 1987 | 5.0 | 5.0 | 5.0 | 0.0 |
| 1988 | 6.2 | 6.2 | 6.2 | 0.0 |
| 1989 | 6.0 | 6.0 | 6.0 | 0.0 |
| 1990 | 6.7 | 6.7 | 6.7 | 0.0 |
| 1991 | 5.7 | 5.7 | 5.7 | 0.0 |
| 1992 | 6.2 | 6.2 | 6.2 | 0.0 |
| 1993 | 5.6 | 5.6 | 5.6 | 0.0 |
| 1994 | 5.0 | 5.0 | 5.0 | 0.0 |
| 1995 | 4.5 | 5.0 | 5.0 | 0.0 |
| 1996 | 5.4 | 6.0 | 4.5 | 1.5 |
| 1997 | 5.0 | 5.0 | 3.5 | 1.5 |
| 1998 | 4.6 | 5.0 | 3.5 | 1.5 |
| 1999 | 4.3 | 4.0 | 2.5 | 1.5 |
| 2000 | 4.1 | 3.5 | 2.0 | 1.5 |
| 2001 | 3.8 | 2.5 | 1.0 | 1.5 |
| 2002 | 4.7 | 4.5 | 1.0 | 3.5 |
| 2003 | 4.5 | 3.5 | 0.0 | 3.5 |
| 2004 | 4.2 | 3.5 | 0.0 | 3.5 |
| 2005 | 4.0 | 3.5 | 0.0 | 3.5 |
| 2006 | 4.3 | 4.5 | 1.0 | 3.5 |
| 2007 | 4.8 | 4.5 | 1.0 | 3.5 |
| 2008 | 4.6 | 4.5 | 1.0 | 3.5 |
| 2009 | 4.6 | 5.0 | 1.5 | 3.5 |
| 2010 | 5.0 | 6.5 | 1.5 | 5.0 |
| 2011 | 5.4 | 8.0 | 1.5 | 6.5 |
| 2012 | 6.1 | 10.5 | 4.0 | 6.5 |
| 2013 | 6.6 | 10.5 | 4.0 | 6.5 |
| 2014 | 6.3 | 10.5 | 4.0 | 6.5 |
| 2015 | 6.1 | 10.5 | 4.0 | 6.5 |
| 2016 | 5.9 | 10.5 | 4.0 | 6.5 |
| 2017 | 6.5 | 12.0 | 3.0 | 9.0 |
| 2018 | 6.3 | 10.5 | 3.0 | 7.5 |
| 2019 | 6.1 | 10.5 | 3.0 | 7.5 |
| 2020 | 6.0 | 10.0 | 2.5 | 7.5 |
| 2021 | 5.9 | 9.0 | 2.5 | 6.5 |
| 2022 | 5.8 | 7.5 | 2.5 | 5.0 |
| 2023 | 6.0 | 6.5 | 1.5 | 5.0 |
| 2024 | 5.9 | 4.5 | 1.5 | 3.0 |
| 2025 | 5.7 | 4.5 | 1.5 | 3.0 |
| 2026 | 5.7 | 5.0 | 2.0 | 3.0 |
This table will help you calculate approximately how much Super Mario is coming in the next decade. The current 10-year window pace shows 5 hours of Super Mario per year.
Looking at the trends, it looks like we may have already passed peak 2D and 3D Mario individually. This table also shows how overdue we are for a new big 3D Super Mario title, the last entry being Super Mario Odyssey almost a decade ago in 2017.
If I were to somewhat morbidly apply these numbers I can estimate how much more new âSuper Marioâ gameplay Iâm likely to experience. Letâs be optimistic and apply the âAll-Time Averageâ instead of the â10-Year Averageâ: the resulting number is 256 hours. Around 10 games of similar size to âSuper Mario Odysseyâ... seems good to me!
Super Mario Blogroll
If you want to read more Super Mario writing here are a few personal selections from my blogroll:
- âMario 101: For Super Playersâ by Drew Mackie
- âSuper Mario Bros was designed on graph paperâ by NicolĂĄs Valencia
- âThe most Mario colorsâ by Louie Mantia
Happy gaming!
Thanks for keeping RSS alive! â„ What to do next? Share your thoughts with me on Mastodon, Bluesky, or email. I try to reply to everyone!Browse the blog archive. Check out my blogroll. Or maybe go outside (best option)?
May 28, 2026
Real Python
Quiz: BNF Notation: Dive Deeper Into Python's Grammar
In this quiz, you’ll test your understanding of BNF Notation: Dive Deeper Into Python’s Grammar.
By working through this quiz, you’ll revisit how to read Python’s grammar rules, recognize terminals and nonterminals, and interpret the BNF fragments that appear throughout the official documentation.
[ Improve Your Python With đ Python Tricks đ â Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Bob Belderbos
Two Python Scoping Bugs: A Lesson in Object Lifetimes
Your app works fine with one user. You open a second browser tab and the data is wrong. Your tests pass individually but fail when run together. The culprit: a global object created at module scope.
How it starts
I see this a lot in Python web projects:
# database.py
from sqlmodel import create_engine, Session
engine = create_engine("sqlite:///database.db")
def get_session():
with Session(engine) as session:
yield session
This 'innocent' engine is created the moment database.py is first imported. Every module that imports from database shares the same engine, the same connection pool, the same database file. For a simple script, this is fine. For a multi-module app, it creates hidden coupling and shared state.
The test isolation problem
I hit this recently in a FastAPI app:
# test_app.py
from myapp.database import engine, create_db_and_tables, clear_db_and_tables
@pytest.fixture(autouse=True)
def setup_database():
clear_db_and_tables()
create_db_and_tables()
def test_create_race(race_events):
championship = create_races(2026, race_events)
assert championship.id == 1 # Passes alone, fails in suite
That assert championship.id == 1 works when the test runs first. Run it after another test that inserts data, and the auto-increment ID comes back as 2. The fixture does its job, but state still leaks between tests in subtle ways: connection pool state, cached metadata, and SQLite's own bookkeeping on the shared file can carry over even with drop/recreate cycles.
The root cause is upstream: every test reaches for the same module-level engine pointed at the same on-disk database. If you want true isolation, the engine itself has to be per-test, not the cleanup ritual around it.
The fix is creating an engine per test session:
@pytest.fixture
def engine():
engine = create_engine("sqlite://", echo=False)
SQLModel.metadata.create_all(engine)
yield engine
engine.dispose()
@pytest.fixture
def session(engine):
with Session(engine) as session:
yield session
Now each test gets a fresh database (no scope defined on the fixture decorator means function scope = per test). No cleanup needed. No shared state.
Alternatively, keep the module-level engine and wrap each test in a transaction you roll back at teardown (sqlmodel's Session supports this).
If you omit engine.dispose() in the code above, you may see a ResourceWarning: unclosed database, but only when running pytest --cov. Coverage's sys.settrace() hook keeps frame locals alive longer, delaying GC of the engine.
The shared simulator problem
The database engine bug is about too much sharing. Here is the inverse: not enough sharing, which breaks in a different way.
Consider a race simulation dashboard. FakeDataSource wraps a RaceSimulator that holds the full mutable race state, driver positions, lap counter, cumulative changes, and advances it on each call:
class FakeDataSource(RaceDataSource):
def __init__(self, data_file: Path, delay_ms: int = 100):
drivers = self._load_drivers(data_file)
self.simulator = RaceSimulator(drivers=drivers) # mutable state lives here
async def get_positions(self, fixture_id: str) -> list[Position]:
self.simulator.tick() # randomly swaps adjacent positions, advances lap
return self.simulator.get_current_positions()
The FastAPI dependency looks like this:
def get_data_source() -> RaceDataSource:
source_type = config("DATA_SOURCE", default="fake")
if source_type == "fake":
return FakeDataSource(data_file=..., delay_ms=...) # new instance every call
...
def get_race_data_source() -> RaceDataSource:
return get_data_source()
FastAPI calls get_race_data_source() once per request. Each browser tab that opens the SSE stream gets a brand new FakeDataSource with a brand new RaceSimulator starting at lap 1 with drivers in their initial order.
The random swaps then diverge independently: Tab A shows Verstappen in P1 at lap 12, Tab B shows Hamilton in P1 at lap 3. Neither reflects a shared reality, because there is no shared state at all.
Two fixes, from quick to idiomatic
1. cache: one line, works immediately
from functools import lru_cache
@lru_cache(maxsize=1)
def get_data_source() -> RaceDataSource:
source_type = config("DATA_SOURCE", default="fake")
if source_type == "fake":
return FakeDataSource(...)
return SportmonksDataSource()
One instance for the lifetime of the process. Simple, but hard to override in tests.
2. FastAPI app.state: idiomatic and testable
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.data_source = get_data_source()
yield
app = FastAPI(lifespan=lifespan)
def get_race_data_source(request: Request) -> RaceDataSource:
return request.app.state.data_source
The data source is created once at startup, shared across all requests, and easy to replace in tests via app.dependency_overrides[get_race_data_source] = lambda: test_source.
Key takeaways
- Module-level objects are created at import time and shared everywhere: global mutable state, and a common source of subtle bugs.
- Tests that share a database engine aren't isolated, even with setup/teardown fixtures.
- Web apps that create per-request instances lose shared state; apps that share module-level instances lose testability.
- Use
app.stateorcachefor shared runtime state; override the FastAPI dependency in tests for isolation.
The rule of thumb: if an object holds mutable state, pick its scope deliberately. Too broad (module scope) and tests leak into each other. Too narrow (per-request) and there's no shared reality. Match the scope to the object's intended lifetime.
Or put more sharply: any module-level object that holds mutable state or owns a resource (DB engines, HTTP clients, caches, queues, connection pools) should be encapsulated. Move it into a fixture, a Depends(), or app.state. Constants and pure values at module scope are fine; resources are not.
The cost of "just import it" is paid later, in test isolation, debugging, and concurrency. Under real concurrency the GIL hides this class of bug until it doesn't, see a race condition Rust wouldn't have let me write, where the same module-global pattern leaked one user's data into another user's response.



