skip to navigation
skip to content

Planet Python

Last update: March 20, 2026 09:44 PM UTC

March 20, 2026


Real Python

The Real Python Podcast – Episode #288: Automate Exploratory Data Analysis & Invent Python Comprehensions

How do you quickly get an understanding of what's inside a new set of data? How can you share an exploratory data analysis with your team? Christopher Trudeau is back on the show this week with another batch of PyCoder's Weekly articles and projects.

March 20, 2026 12:00 PM UTC

Quiz: Python Decorators 101

Work through this quiz to review first-class functions, inner functions, and decorators, and learn how to use them to extend behavior cleanly in Python.

March 20, 2026 12:00 PM UTC

March 19, 2026


"Michael Kennedy's Thoughts on Technology"

Use Chameleon templates in the Robyn web framework

TL;DR; Chameleon-robyn is a new Python package I created that brings Chameleon template support to the Robyn web framework. If you prefer Chameleon’s structured, HTML-first approach over Jinja and want to try Robyn’s Rust-powered performance, this package bridges the two.


People who have known me for a while know that I’m very much not a fan of the Jinja templating language. Neither am I a fan of the Django templating language, since it’s very similar. I dislike the fact that you’re mostly programming with interlaced HTML rather than having mostly HTML that is very restricted in what it allows in terms of coding. While nowhere near perfect, I prefer Chameleon because it requires you to write well-structured code. Sadly, I think Jinja won exactly because it allows you to write whatever Python code in your HTML you want. For most frameworks, Jinja is the only templating language they support.

Why migrate Chameleon templates to a new framework?

I’d love to try out some new frameworks, but I have so much existing Chameleon code that any sort of migration will never include converting to Jinja, if I have a say in it. Not because of my dislike for it, but because it’s incredibly error prone, and it would mean changing my entire web design, not just my code.

Here’s the code breakdown for just Talk Python Training.

That design category is 14,650 lines of HTML and 11,104 lines of CSS! If I can get Chameleon running on a framework, it will 100% reuse every line of that to perfection. If I cannot, I’m rewriting them all. No thanks.

How does Robyn use Rust to speed up Python web apps?

I’ve been thinking a lot about what if our web frameworks actually ran in Rust? Right now I’m running Quart (async Flask) on top of Granian. So Rust is the base of my web server and processing. But there is a lot of infrastructure provided by Flask and Werkzueg leading up to my code actually running that is all based on Python.

Would it be a lot faster? Maybe. My exploring this idea was inspired by TurboAPI. TurboAPI did exactly this as I was thinking about, but with Zig and for FastAPI. While I am not recommending people leave FastAPI, their headline “FastAPI-compatible. Zig HTTP core. 22x faster,” does catch one’s attention.

Eventually I found my way to Robyn. Robyn merges Python’s async capabilities with a Rust runtime for reliable, scalable web solutions. Here are a few key highlights:

There’s this quite interesting performance graph from Robyn’s benchmarks. Of course, take it with all the caveats that benchmarks come with.

Benchmark comparing Robyn, FastAPI, Flask, and Django on request throughput

How to use Chameleon templates with Robyn

I want to try this framework on real projects that I’m running in production to see how they size up. However, given all of my web UI is written in Chameleon, there’s absolutely no way I’m converting to Jinja. I can hear everyone now, “So just use it for something simple and new, Michael.” For me that defeats the point. Thus, my obsession with getting Chameleon to work.

I created the integration for Chameleon for Flask with my chameleon-flask package. Could I do the same thing for Robyn?

It turns out that I can! Without further ado, introducing chameleon-robyn:

It’s super early days and I’m just starting to use this package for my prototype. I’m sure as I put it into production in a real app, I’ll see if it’s feature complete or not.

For now, it’s out there on GitHub and on PyPI. If Chameleon + Robyn sounds like an interesting combo to you as well, give this a try. PRs are welcome.

March 19, 2026 11:18 PM UTC


Talk Python to Me

#541: Monty - Python in Rust for AI

When LLMs write code to accomplish a task, that code has to actually run somewhere. And right now, the options aren't great. Spin up a sandboxed container and you're paying a full second of cold start overhead plus the complexity of another service. Let the LLM loose on your actual machine and... well, you'd better be watching. On this episode, I sit down with Samuel Colvin, creator of Pydantic, now at 10 billion downloads, to explore Monty, a Python interpreter written from scratch in Rust, purpose-built to run LLM-generated code. It starts in microseconds, is completely sandboxed by design, and can even serialize its entire state to a database and resume later. We dig into why this deliberately limited interpreter might be exactly what the AI agent era needs.

March 19, 2026 07:38 PM UTC


Real Python

Quiz: How to Add Python to PATH

Test your knowledge of the PATH variable and learn how to add Python on Windows, Linux, and macOS for easy terminal access.

March 19, 2026 12:00 PM UTC


Tryton News

Release 0.1.0 of hatch-tryton

We are proud to announce the first release of the version 0.1.0 of hatch-tryton.

hatch-tryton is a hatchling plugin that manages Tryton dependencies.

We will rely on this tool to upgrade Tryton’s packages to pyproject.toml for future releases.

hatch-tryton is available on PyPI as hatch-tryton 0.1.0

1 post - 1 participant

Read full topic

March 19, 2026 10:18 AM UTC


Nicola Iarocci

Eve 2.3.0

Eve v2.3 was just released on PyPI. It adds optimize_pagination_for_speed, a resource-level setting that allows granular control, overriding the equivalent global option that goes by the same name. Many thanks to Emanuele Di Giacomo for contributing to the project.

March 19, 2026 09:38 AM UTC


"Michael Kennedy's Thoughts on Technology"

Fire and forget (or never) with Python’s asyncio

Python asyncio fire and forget task pattern

TL;DR; Python’s asyncio.create_task() can silently garbage collect your fire-and-forget tasks starting in Python 3.12 - they may never run. The fix: store task references in a set and register a done_callback to clean them up.


Do you use Python’s async/await in programming? Often you have some async task that needs to run, but you don’t care to monitor it, know when it’s done, or even if it errors.

Let’s imagine you have an async function that logs to a remote service. You want its execution out of the main-line execution. Maybe it looks like this:

async def log_account_created(username: str): ...

Someone new to Python’s odd version of async would think they could write code like this (hint, they cannot):

async def register_user():
    data = get_form_data()
    user = user_service.create_user(data)
    
    # Log in the background (actually no, but we try)
    log_account_created(user.name) # BUG!
    
    return redirect('/account/welcome')

You cannot just run an async function, you fool! Why? I don’t know. It’s a massively needless complication of modern Python. You either have to await it (which would block the main execution foiling our fire and forget intention) or you have to start it as a task separately. Here’s the working version (at least in Python 3.11 it works, Python 3.12+? Sometimes):

async def register_user():
    data = get_form_data()
    user = user_service.create_user(data)
    
    # Log in the background?
    # Runs on the asyncio loop, fixed, maybe
    asyncio.create_task(log_account_created(user.name)) 
    
    return redirect('/account/welcome')

Why asyncio.create_task loses tasks in Python 3.12+

Actually that fixed version has a tremendously subtle race condition that was introduced in Python 3.12 (seriously). In Python 3.11 or before, the async loop holds the new task and will just run it at some point soon.

Here is the first line in the docs for create_task:

Wrap the coro coroutine into a Task and schedule its execution. Return the Task object.

It schedules its execution. But in Python 3.12, it might forget about it!

I call functions like log_account_created fire and forget async functions. You don’t care to wait for it or even check its outcome. What are you going to do if logging fails anyway? Log it more? But check this out, straight from Python 3.14’s documentation:

Important: Save a reference to the result of [ asyncio.create_task ], to avoid a task disappearing mid-execution. The event loop only keeps weak references to tasks. A task that isn’t referenced elsewhere may get garbage collected at any time, even before it’s done. For reliable “fire-and-forget” background tasks, gather them in a collection:

background_tasks = set()

for i in range(10):
    task = asyncio.create_task(some_coro(param=i))

    # Add task to the set. This creates a strong reference.
    background_tasks.add(task)

    # To prevent keeping references to finished tasks forever,
    # make each task remove its own reference from the set after
    # completion:
    task.add_done_callback(background_tasks.discard)

Wait, what? If I start a Python async task it might be GC’ed before it even starts? Wow, just wow.

The fix? Well, you just create a set to track them (keep a strong reference in GC-parlance). In our example, it looks like this:

background_tasks = set()

async def register_user():
    data = get_form_data()
    user = user_service.create_user(data)
    
    # Log in the background
    task = asyncio.create_task(log_account_created(user.name)) 
    background_tasks.add(task)                       # prevent GC
    task.add_done_callback(background_tasks.discard) # cleanup when done
    
    return redirect('/account/welcome')

One obvious way to do it

This set hack is entirely non-obvious that this is required. After all, the Zen of Python states:

There should be one – and preferably only one – obvious way to do it.

But Zen doesn’t always apply, does it? I’m sure there is a reason for this change, though I’m not sure it was worth it.

If it were up to me, Python would come with one omnipresent event loop running on a background thread. Just calling an async function would schedule and run it there. The await keyword would be a control flow only construct, not the thing that actually does the execution.

But it doesn’t work that way and so we get oddities here and there I guess.

March 19, 2026 03:22 AM UTC


Seth Michael Larson

Getting started with the GameSir “Pocket Taco” with iPhone and Delta emulator

March 19, 2026 12:00 AM UTC

March 18, 2026


Real Python

Build Your Weekly Python Study Schedule: 7 Days to Consistent Progress

Create a weekly Python study schedule you can stick to. Build a realistic 7-day plan, stay consistent, and turn learning Python into a sustainable habit.

March 18, 2026 02:00 PM UTC

Quiz: Exploring Basic Data Types in Python

Test your understanding of Python data types and built-in functions to write clearer, more reliable code.

March 18, 2026 12:00 PM UTC


Seth Michael Larson

Python library “Requests” needs you to test type hints

March 18, 2026 12:00 AM UTC

March 17, 2026


PyCoder’s Weekly

Issue #726: Lazy Imports, Pydantic AI, Classes, and More (March 17, 2026)

March 17, 2026 07:30 PM UTC


Real Python

Downloading Files From URLs With Python

Learn to download files from URLs with Python using urllib and requests, including data streaming for large files.

March 17, 2026 02:00 PM UTC

Quiz: Linked Lists in Python: An Introduction

Learn Python linked lists, deques, and circular & doubly linked structures with practical examples and efficient operations.

March 17, 2026 12:00 PM UTC

March 16, 2026


Mike Driscoll

Textual – Creating a Custom Checkbox

Textual is a great Python user interface package. Textual lets you create a GUI-like interface in your terminal. You can use many different widgets in Textual. However, the widget you will be focusing on in this tutorial is the humble checkbox. Checkboxes are used for Boolean choices. They return a True if checked and a […]

The post Textual – Creating a Custom Checkbox appeared first on Mouse Vs Python.

March 16, 2026 07:42 PM UTC


Ari Lamstein

acs-nativity: A Python Package for Analyzing Changes in the Foreign-Born Population

President Trump has made reducing illegal immigration and increasing deportations central goals of his second administration (1, 2). This is causing many people to ask: how are these policies changing the country’s population? To help answer that, I built a new open-source Python package called acs-nativity. It provides a simple interface for accessing and visualizing […]

March 16, 2026 04:00 PM UTC


PyCon

Attend PyCon US for a day of Trailblazing Python Security!

March 16, 2026 03:58 PM UTC


Real Python

Spyder: Your IDE for Data Science Development in Python

Learn how to use the Spyder IDE, a Python code editor built for scientists, engineers, and data analysts working with data-heavy workflows.

March 16, 2026 02:00 PM UTC

Quiz: Speed Up Python With Concurrency

Test your Python concurrency knowledge: CPU vs I/O-bound tasks, GIL, asyncio, race conditions, and multiprocessing.

March 16, 2026 12:00 PM UTC


Python Bytes

#473 A clean room rewrite?

Topics include , refined-github, , and Agentic Engineering Patterns.

March 16, 2026 08:00 AM UTC

March 15, 2026


The Python Coding Stack

The Weird and Wonderful World of Descriptors in Python • The Price is Right

The Weird and Wonderful World of Descriptors in Python • Let’s demystify one of the trickiest topics around

March 15, 2026 09:27 PM UTC

March 14, 2026


Real Python

Quiz: Splitting, Concatenating, and Joining Python Strings

Brush up on splitting, concatenating, and joining strings in Python. Test your understanding of methods, immutability, and common pitfalls.

March 14, 2026 12:00 PM UTC


Marcos Dione

Block sizes from OSM data

My city has big blocks. One close to mine is around 6km of circumference. I wanted to make a map that show the sizes of the blocks. What I have is a rendering DB for my area. Let's see what I can do.

An obvious solution would be to use any routing database, which would convert squiggly segments into edges of a graph. I feebly tried that, but no routing system I know is packaged for Debian, and one of them asked me to compile it. I could compile it, but I was lazy that way.

So the plan is this: take the whole street network1 and recursively remove the dead end streets. The recursion part is because there are areas where there are whole branches of dead end streets, so if in one pass a street A might look not dead because it connects to another B, but B is removed, the next pass should remove A too. So, for each street, take each end, and see how many other segments it's connected to; if 0, it's a dead end and we should remove it. Keep going until no new streets were removed.

One problem is how the data is represented. In a rendering database, streets do not exist, just segments with a consistent tagging, so streets can be split if, for instance, max speed changes, or there's a bridge. But one thing they're not split on is where another street joins them (which would be reflected in a routing db, lazy me! :)

This does not matter when searching for connected streets to a point, but a segment might have parts that are a dead end, and parts that are not, because they're connected to other streets(segments) that are connected too. So now, for each end, take the point, and if it's not connected, removed the point and try again. The full algo:

Looks like an infinite problem! The city I'm interested in has 16k segments, and this looks worse than O(N²)! But we can do some shortcuts. One of the expensive operations is "the end is connected" or "find all the other segments that include this point". Luckily, a rendering DB has a geographic index, and we can do trics like looking for segments only around the area of the segment you're looking at, so reducing the amount of comparisons by a lot: less that 10 instead of 16k! And of course, you should not consider segments you already removed.

To be honest, I thought this would take way more effort and code. Yes, I changed the algo thrice, but all in all it took me like 6h to get it as it is. Now, it is not perfect. It does not detect some artifacts (mostly cycles connected to a single segment), but is good enough for me for now. Not bad for <200 lines of Python and SQL :)

#! /usr/bin/env python3

import psycopg2
from shapely import from_wkb, set_srid, LineString, Point


def main():
    print('Fetching all segments, takes a while.')
    conn = psycopg2.connect(dbname='europe')
    cur = conn.cursor()

    cur.execute(f"""
        WITH marseille AS (
            SELECT way
            FROM planet_osm_polygon
            WHERE osm_id = -76469
        )
        SELECT line.osm_id, line.way
        FROM
            planet_osm_line AS line,
            marseille
        WHERE
            line.way && marseille.way AND
            line.highway IN (
                'primary',
                'primary_link',
                'secondary',
                'secondary_link',
                'tertiary',
                'tertiary_link',
                'residential',
                'unclassified',
                'living_street',
                'road'
            ) AND
            (line.access IS NULL OR line.access = 'yes')
            { f" LIMIT {sys.argv[1]}" if len(sys.argv) > 1 else '' };
    """)

    segments = {}
    segments_removed = set([666])  # fake osm_id so line.osm_id NOT IN %s AND works (empty sets are syntax errors)

    # first pass: collect all segments
    for data in cur.fetchall():
        osm_id = data[0]
        segment = from_wkb(data[1])

        segments[osm_id] = segment

    print(f"Found {len(segments)} segments.")

    # second pass: eliminate segments for which we can find an unconnected end
    pass_number = 1

    while len(segments) > 0:
        print(f"PASS {pass_number:2d}")

        total_count = 0
        total_changed = 0
        to_remove = set()

        for osm_id, segment in segments.items():
            changed = False
            removed = False
            connections = 0

            for direction in -1, 0:
                while len(segment.coords) > 0:
                    point = Point(segment.coords[direction])

                    cur.execute("""
                        WITH segment AS (
                            SELECT way
                            FROM planet_osm_line
                            WHERE osm_id = %s
                        )
                        SELECT
                            line.osm_id,
                            line.name
                        FROM planet_osm_line AS line, segment
                        WHERE
                            line.way && ST_Buffer(ST_Envelope(segment.way), 10) AND
                            line.osm_id != %s AND
                            line.osm_id NOT IN %s AND
                            line.highway IN (
                                'trunk',
                                'trunk_link',
                                'primary',
                                'primary_link',
                                'secondary',
                                'secondary_link',
                                'tertiary',
                                'tertiary_link',
                                'residential',
                                'unclassified',
                                'living_street',
                                'road'
                            ) AND
                            (access IS NULL OR access = 'yes') AND
                            ST_Intersects(line.way, ST_GeomFromWKB(%s, 3857));
                    """, (osm_id, osm_id, tuple(segments_removed), point.wkb))

                    data = cur.fetchall()

                    if len(data) == 0:
                        if len(segment.coords) == 2:
                            # removing one point removes the segment
                            removed = True
                            to_remove.add(osm_id)
                            break

                        # remove the point
                        if direction == 0:
                            segment = LineString(segment.coords[1:])
                        else:
                            segment = LineString(segment.coords[:-1])

                        changed = True
                    else:
                        connections += len(data)
                        break

            if changed:
                segments[osm_id] = segment
                total_changed += 1

            total_count += 1
            match total_count % 10, changed, removed:
                case _, True, _:
                    print('*', end='', flush=True)
                case _, False, True:
                    print('_', end='', flush=True)
                case 0, _, _:
                    print('|', end='', flush=True)
                case 5, _, _:
                    print(',', end='', flush=True)
                case _:
                    print('.', end='', flush=True)

            if total_count % 100 == 0:
                print()

        segments_removed.update(to_remove)

        print()
        print(', '.join([ str(id) for id in to_remove ]))

        for osm_id in to_remove:
            del segments[osm_id]

        print(f"{total_changed} changed, {len(to_remove)} removed, {len(segments)} left.")
        print()

        # if only some segments were changed, the topology does not change, so we save one pass
        if len(to_remove) == 0:
            break

        pass_number += 1

    print("Saving segments...")
    for segment in segments.values():
        cur.execute("""
            INSERT INTO streets (way)
            VALUES (ST_GeomFromWKB(%s, 3857))
        """, (segment.wkb, ))

    conn.commit()

    conn.commit()
    print()

    print("Cutting...")
    for index, segment in enumerate(segments.values()):
        buffered = segment.buffer(1)

        marseille = difference(marseille, buffered)

        match (index + 1) % 10:
            case 0:
                print('|', end='', flush=True)
            case 5:
                print(',', end='', flush=True)
            case _:
                print('.', end='', flush=True)

        if (index + 1) % 100 == 0:
            print()

    print()

    out = open('blocks.geojson', 'w+')
    # GeoJSONstream, one GeoJSON object per line
    # GDAL/QGIS won't accept 
    for polygon in marseille.geoms:
        out.write(f"{json.dumps(mapping(polygon))}\n")


main()

Runtime takes a while because someone correctly micromapped a 350m dead end road due to diffrences in road side parking, 14 segments/passes in total; otherwise 8 would have suffised. I could have added a heuristic where, when a pass removed just a few segments, the next pass would only search among the segments close to those. Technically I could do this from the first pass.

One thing that surprised me whas this: QGIS can read GeoJSON files, but if they're going to be a colleciton of things, better be a GeoJSONseq, that is a file with a GeoJSON object per line. But since these files do not include info about the EPSG, you have to set it by hand on QGIS. now, this would be fine if QGIS would ask about it when loading the layer, but instead two things happen: it confuses the EPSG, but would still correctly zoom to the layer assuming that the layer has the same projection as the project. Thanks to uglyhack#qgis@libera.chat.

Several caveats:

I didn't include motorways or tunks in the block computation, because they create a complication in the definition of block, specially with bridges over or tunnels under other roads, of which there are a lot here. I know at least 3 bridges that still break the graph; there are few enough that I can ignore them.

The city has big blocks of industrial areas. Service roads are not included, and in any case most are dead ends.

The biggest areas are actually not urban, including a big chunk of a National Park in the two big red areas to the South2 and the ports and the sea nearby3. Extracting a real urban area is possible, but harder.

On top I also drew three things, roads (grey, white, blue), rivers (thick blue) and train tracks (black), to give a better idea of the complexity of the city. This time it includes motorways, trunks, service and private roads. The latter also give an idea of how much is in private hands (white). The thin blue lines are the streets thad define the blocks, and the grey are public but dead ends.


  1. I'm focused on a traffic issue: the city is not navigable by car, leading to lots of traffic in the few streets available, leading to lots of noise from impatient drivers. 

  2. The map is rotated 108° so I could zoom in as much as possible, so South is rougly to the left. 

  3. See where all the ferry lines go to; that's the Vieux Port (Old Port), and the new, industrial one is in the same area to the North/right. 

March 14, 2026 11:16 AM UTC


Seth Michael Larson

I’ve added human.json to my website

March 14, 2026 12:00 AM UTC