Planet Python
Last update: March 30, 2026 04:46 PM UTC
March 30, 2026
"Michael Kennedy's Thoughts on Technology"
Raw+DC Database Pattern: A Retrospective
TL;DR; After migrating three production Python web apps from MongoEngine to the Raw+DC database pattern, I measured nearly 2x the requests per second, 18% less memory, and gained native async support. Raw+DC delivered real-world performance gains, not just synthetic benchmarks.
About a month ago, I wrote about a new design pattern I’m seeing gain traction in the software space: Raw+DC: The ORM pattern of 2026. This article generated a lot of interest and a lot of debate. The short version: instead of using an ORM or ODM, you write raw database queries paired with Python dataclasses for type safety. This gives AI coding assistants a much larger training base to work from, reduces dependency risk, and delivers comparable or better performance.
Putting Raw+DC into practice
Now that some time has passed and I’ve thought about it more, I’ve had a chance to migrate three of my most important web apps to Raw+DC: Talk Python the podcast, Talk Python Courses, and Python Bytes.
So how did it go? From a pure functionality perspective, it went great. There were maybe one to three problems per web app. This might not sound great, and I didn’t love it, but given this is thousands and thousands of lines of code per app, that’s a small percentage of issues, given how many things went right.
More importantly, I was able to remove a dependency on two faltering database libraries. Mongoengine, the one that I’m going to pull numbers from for Talk Python Training below, has not had a meaningful release in years. It was one of the two core blockers that prevented me from using async programming patterns on the website entirely.
How much faster is Raw+DC than MongoEngine?
I said I imagined that we would save in memory and CPU costs, but did it actually pan out in a practical application? After all, we saw that Robyn, the web framework, is 25 times faster than Flask. However, in practice, it was almost a dead even heat.
I’m thrilled to report that yes, the web app is much faster using Raw+DC.
Below is an apples-to-apples comparison for Talk Python Training using MongoEngine and the Raw+DC pattern.
| Metric | MongoEngine (ODM) | Raw+DC | Improvement |
|---|---|---|---|
| Requests/sec | baseline | ~1.75x | 1.75x faster |
| Response time | baseline | ~50% less | ~50% faster |
| Memory usage | baseline | 200 MB less | 18% less |

The memory story is really great as well. After letting the web app run for over 24 hours for each mode, we saw a 200 MB memory usage decrease using Raw+DC.

That amount of memory might still look high to you. This Raw+DC transformation actually facilitates future work that will cut it in half again, down to about 500 MB for the full app, up and running in production at equilibrium.
Is Raw+DC worth migrating to?
To me, this seems 100% worth it. I’ve gained four important things with Raw+DC.
- 1.75x the requests per second on the exact same hardware and codebase (sans data layer swap)
- 18% less memory usage with much more savings on the horizon
- New data layer natively supports async/await
- Removal of problematic, core data access library
All of these benefits and none of that even touches on whether or not this new programming model is better for AI (it is).
Real Python
How to Use Ollama to Run Large Language Models Locally
Running Ollama in your terminal allows you to start chatting with a local large language model (LLM) quickly. You won’t need API keys, cloud services, or ongoing costs. Ollama is a free, open-source tool that lets you download and run models directly on your machine. By following this guide, you’ll install Ollama, chat with local models from your terminal, and use them to power agentic coding tools:
Example of Using Ollama to Run an LLM LocallyLarge language models traditionally require expensive API subscriptions and a constant internet connection. Ollama eliminates both requirements by running models directly on your hardware. Because everything runs locally, your prompts stay on your machine, and no per-token fees apply.
Get Your Cheat Sheet: Click here to download your free Ollama cheat sheet and keep the essential steps and commands for running LLMs locally at your fingertips.
Take the Quiz: Test your knowledge with our interactive “How to Use Ollama to Run Large Language Models Locally” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
How to Use Ollama to Run Large Language Models LocallyTest your knowledge of running LLMs locally with Ollama. Install it, pull models, chat, and connect coding tools from your terminal.
Prerequisites
To follow this guide, you’ll need the following software and hardware:
- macOS 14 Sonoma or newer, Windows 10 or newer, or a recent Linux distribution
- At least 8 GB of RAM, or 16 GB or more for larger models
- 5–16 GB of free disk space to store models
- Basic skills with the command line or terminal, including opening a terminal and running commands
No Python installation is required for this guide, and no prior experience with LLMs or AI is needed. If you want to integrate Ollama with Python after finishing here, check out How to Integrate Local LLMs With Ollama and Python.
Step 1: Install Ollama and Pull Your First Model
To quickly install Ollama on your operating system, run the following command based on your platform:
Once this command finishes, Ollama will be installed on your system.
Note: In some Linux distributions, you may need to install curl to download the installer and the zstd library for extraction. On Debian/Ubuntu, you can install them with the following command:
$ sudo apt update && sudo apt install curl zstd
Alternatively, you can download a dedicated installer for Windows and macOS. Visit Ollama’s download page to get the installer for those operating systems.
Note: Ollama has a GUI application for macOS and Windows users. This quick guide focuses solely on the command-line (CLI) tool. See Ollama’s app announcement if you want to explore that option.
After installation, you can verify that the CLI is available with the following command:
$ ollama -v
ollama version is 0.17.7
The Ollama service should be running in the background. Normally, you don’t need to start it manually. It runs on port 11434 by default. If you get a warning after running the command above, then you may need to run the background server manually:
$ ollama serve
Read the full article at https://realpython.com/ollama/ »
[ 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 ]
Mike Driscoll
Vibe Coding Pong with Python and pygame
Pong is one of the first computer games ever created, way back in 1972. If you have never heard of Pong, you can think of it as a kind of “tennis” game. There are two paddles, on each side of the screen. They move up and down. The goal is to bounce a ball between the two paddles. If you miss the ball, your opponent scores a point.
Atari created Pong and put it in arcades. The name of the game is derived from the game of ping-pong. Pong has been remade many times, with different features and gameplay elements added or removed.
Want to know more? Get the Book!
You will use Anthropic’s Claude AI to create this game in Python using the pygame package.
Let’s get started!
Creating the Game
Prompt: Please create the pong videogame using Python and pygame. Give the user a way to start a new game and keep score.
I’ll create a classic Pong game for you using Python and pygame. This will include two paddles, a ball, scoring, and the ability to start new games.
import pygame
import sys
import random
# Initialize Pygame
pygame.init()
# Constants
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
PADDLE_WIDTH = 15
PADDLE_HEIGHT = 90
BALL_SIZE = 15
PADDLE_SPEED = 7
BALL_SPEED_X = 6
BALL_SPEED_Y = 6
# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (128, 128, 128)
class Paddle:
def __init__(self, x, y):
self.rect = pygame.Rect(
x, y, PADDLE_WIDTH, PADDLE_HEIGHT)
self.speed = PADDLE_SPEED
def move_up(self):
if self.rect.top > 0:
self.rect.y -= self.speed
def move_down(self):
if self.rect.bottom < SCREEN_HEIGHT:
self.rect.y += self.speed
def draw(self, screen):
pygame.draw.rect(screen, WHITE, self.rect)
class Ball:
def __init__(self):
self.rect = pygame.Rect(
SCREEN_WIDTH // 2,
SCREEN_HEIGHT // 2,
BALL_SIZE,
BALL_SIZE)
self.reset_ball()
def reset_ball(self):
self.rect.center = (SCREEN_WIDTH // 2,
SCREEN_HEIGHT // 2)
self.speed_x = BALL_SPEED_X * random.choice([-1, 1])
self.speed_y = BALL_SPEED_Y * random.choice([-1, 1])
def move(self):
self.rect.x += self.speed_x
self.rect.y += self.speed_y
# Bounce off top and bottom walls
if (self.rect.top <= 0
or self.rect.bottom >= SCREEN_HEIGHT):
self.speed_y = -self.speed_y
def draw(self, screen):
pygame.draw.rect(screen, WHITE, self.rect)
class PongGame:
def __init__(self):
self.screen = pygame.display.set_mode(
(SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Pong Game")
self.clock = pygame.time.Clock()
# Create game objects
self.left_paddle = Paddle(
30, SCREEN_HEIGHT // 2 - PADDLE_HEIGHT // 2)
self.right_paddle = Paddle(
SCREEN_WIDTH - 30 - PADDLE_WIDTH,
SCREEN_HEIGHT // 2 - PADDLE_HEIGHT // 2)
self.ball = Ball()
# Scores
self.left_score = 0
self.right_score = 0
self.font = pygame.font.Font(None, 74)
self.small_font = pygame.font.Font(None, 36)
# Game state
self.game_state = "menu" # "menu", "playing", "paused"
def handle_collision(self):
# Ball collision with paddles
if self.ball.rect.colliderect(self.left_paddle.rect):
# Only bounce if moving toward paddle
if self.ball.speed_x < 0:
self.ball.speed_x = -self.ball.speed_x
# Add some variation based on where ball hits paddle
hit_pos = (self.ball.rect.centery - self.left_paddle.rect.centery
) / (PADDLE_HEIGHT / 2)
self.ball.speed_y += hit_pos * 2
if self.ball.rect.colliderect(self.right_paddle.rect):
# Only bounce if moving toward paddle
if self.ball.speed_x > 0:
self.ball.speed_x = -self.ball.speed_x
# Add some variation based on where ball hits paddle
hit_pos = (self.ball.rect.centery - self.right_paddle.rect.centery
) / (PADDLE_HEIGHT / 2)
self.ball.speed_y += hit_pos * 2
# Limit ball speed
max_speed = 12
if abs(self.ball.speed_y) > max_speed:
self.ball.speed_y = max_speed if self.ball.speed_y > 0 else -max_speed
def check_scoring(self):
# Check if ball went off screen
if self.ball.rect.left <= 0:
self.right_score += 1
self.ball.reset_ball()
elif self.ball.rect.right >= SCREEN_WIDTH:
self.left_score += 1
self.ball.reset_ball()
def handle_input(self):
keys = pygame.key.get_pressed()
if self.game_state == "playing":
# Left paddle controls (W/S)
if keys[pygame.K_w]:
self.left_paddle.move_up()
if keys[pygame.K_s]:
self.left_paddle.move_down()
# Right paddle controls (UP/DOWN arrows)
if keys[pygame.K_UP]:
self.right_paddle.move_up()
if keys[pygame.K_DOWN]:
self.right_paddle.move_down()
def draw_menu(self):
self.screen.fill(BLACK)
title_text = self.font.render("PONG", True, WHITE)
title_rect = title_text.get_rect(
center=(SCREEN_WIDTH // 2, 150))
self.screen.blit(title_text, title_rect)
start_text = self.small_font.render(
"Press SPACE to Start", True, WHITE)
start_rect = start_text.get_rect(
center=(SCREEN_WIDTH // 2, 250))
self.screen.blit(start_text, start_rect)
controls_text = [
"Controls:",
"Left Player: W (Up) / S (Down)",
"Right Player: Arrow Keys",
"Press R to restart during game",
"Press ESC to return to menu"
]
for i, text in enumerate(controls_text):
rendered_text = self.small_font.render(
text, True, GRAY if i == 0 else WHITE)
text_rect = rendered_text.get_rect(
center=(SCREEN_WIDTH // 2, 320 + i * 40))
self.screen.blit(rendered_text, text_rect)
def draw_game(self):
self.screen.fill(BLACK)
# Draw center line
for i in range(0, SCREEN_HEIGHT, 20):
if i % 40 == 0:
pygame.draw.rect(
self.screen,
WHITE,
(SCREEN_WIDTH // 2 - 2, i, 4, 10))
# Draw paddles and ball
self.left_paddle.draw(self.screen)
self.right_paddle.draw(self.screen)
self.ball.draw(self.screen)
# Draw scores
left_score_text = self.font.render(
str(self.left_score), True, WHITE)
right_score_text = self.font.render(
str(self.right_score), True, WHITE)
self.screen.blit(left_score_text, (SCREEN_WIDTH // 4, 50))
self.screen.blit(
right_score_text,
(3 * SCREEN_WIDTH // 4 - right_score_text.get_width(),
50))
# Draw instructions
instruction_text = self.small_font.render(
"Press ESC for menu, R to restart", True, GRAY)
instruction_rect = instruction_text.get_rect(
center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT - 30))
self.screen.blit(instruction_text, instruction_rect)
def reset_game(self):
self.left_score = 0
self.right_score = 0
self.ball.reset_ball()
self.left_paddle.rect.y = (SCREEN_HEIGHT // 2 -
PADDLE_HEIGHT // 2)
self.right_paddle.rect.y = (SCREEN_HEIGHT // 2 -
PADDLE_HEIGHT // 2)
def run(self):
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if (event.key == pygame.K_SPACE
and self.game_state == "menu"):
self.game_state = "playing"
self.reset_game()
elif event.key == pygame.K_ESCAPE:
self.game_state = "menu"
elif (event.key == pygame.K_r
and self.game_state == "playing"):
self.reset_game()
if self.game_state == "menu":
self.draw_menu()
elif self.game_state == "playing":
self.handle_input()
self.ball.move()
self.handle_collision()
self.check_scoring()
self.draw_game()
pygame.display.flip()
self.clock.tick(60)
pygame.quit()
sys.exit()
if __name__ == "__main__":
game = PongGame()
game.run()
I’ve created a complete Pong game in Python using pygame! Here are the key features:
Game Features:
- Classic Pong gameplay with two paddles and a bouncing ball
- Score tracking for both players
- Menu system with game start option
- Ability to restart games and return to menu
Controls:
- Left Player: W (up) / S (down)
- Right Player: Arrow keys (up/down)
- SPACE: Start new game from menu
- R: Restart current game
- ESC: Return to main menu
Game Mechanics:
- Ball bounces off top/bottom walls and paddles
- Ball speed varies slightly based on where it hits the paddle
- Scores increase when ball goes off either side
- Ball resets to center after each score
To run the game:
- Make sure you have pygame installed:
pip install pygame - Save the code to a
.pyfile (e.g.,pong.py) - Run it with:
python pong.py
The game includes a clean menu system, smooth 60 FPS gameplay, and responsive controls. The ball physics include some randomization to keep gameplay interesting, and the paddle collisions add a bit of strategy by allowing players to influence the ball’s trajectory based on where they hit it.
Prompt: Can you explain how the code works?
I’ll break down how the Pong game code works, explaining each major component and how they work together.
Code Structure Overview
The code is organized using object-oriented programming with three main classes:
Paddle– represents the player paddlesBall– handles the bouncing ballPongGame– manages the overall game logic and flow
1. Initialization and Constants
pygame.init() # Constants define game dimensions and speeds SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 PADDLE_SPEED = 7
The game starts by initializing pygame and defining constants for screen size, object dimensions, and movement speeds. Using constants makes the code easier to modify and maintain.
2. The Paddle Class
class Paddle:
def __init__(self, x, y):
self.rect = pygame.Rect(x, y, PADDLE_WIDTH, PADDLE_HEIGHT)
Each paddle is represented as a pygame.Rect object, which provides built-in collision detection and positioning. The paddle has methods to:
move_up()andmove_down()– handle movement with boundary checkingdraw()– renders the paddle as a white rectangle
The boundary checking ensures paddles can’t move off-screen:
if self.rect.top > 0: # Don't go above screen if self.rect.bottom < SCREEN_HEIGHT: # Don't go below screen
3. The Ball Class
class Ball:
def reset_ball(self):
self.speed_x = BALL_SPEED_X * random.choice([-1, 1])
self.speed_y = BALL_SPEED_Y * random.choice([-1, 1])
The ball maintains its position and velocity. Key features:
reset_ball()– centers the ball and gives it a random directionmove()– updates position and handles wall bouncing- Wall collision detection flips the Y velocity:
self.speed_y = -self.speed_y
4. Main Game Class Structure
The PongGame class manages everything:
Game States
self.game_state = "menu" # "menu", "playing", "paused"
The game uses states to control what’s displayed and what input is accepted.
Game Loop
def run(self):
while running:
# Handle events (keyboard, quit)
# Update game logic based on current state
# Draw everything
# Control frame rate (60 FPS)
5. Collision Detection
The most complex part handles ball-paddle collisions:
def handle_collision(self):
if self.ball.rect.colliderect(self.left_paddle.rect):
# Only bounce if moving toward paddle
if self.ball.speed_x < 0:
self.ball.speed_x = -self.ball.speed_x
Key points:
- Uses pygame’s built-in
colliderect()for collision detection - Checks ball direction to prevent multiple bounces
- Adds spin based on where the ball hits the paddle:
hit_pos = (self.ball.rect.centery -
self.left_paddle.rect.centery
) / (PADDLE_HEIGHT / 2)
self.ball.speed_y += hit_pos * 2
6. Input Handling
def handle_input(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_w]:
self.left_paddle.move_up()
The game checks which keys are currently pressed each frame, allowing for smooth continuous movement. Different keys are handled based on the game state.
7. Scoring System
def check_scoring(self):
if self.ball.rect.left <= 0: # Ball went off left side
self.right_score += 1
self.ball.reset_ball()
Scoring is checked every frame by seeing if the ball has moved past the screen edges.
8. Rendering System
The game uses different drawing functions for different states:
draw_menu()– shows title and instructionsdraw_game()– renders paddles, ball, scores, and game elements
The center dashed line is drawn using a loop:
for i in range(0, SCREEN_HEIGHT, 20):
if i % 40 == 0: # Only draw every other dash
pygame.draw.rect(
self.screen,
WHITE,
(SCREEN_WIDTH // 2 - 2, i, 4, 10))
9. Game Flow
- Initialization: Create paddles, ball, set up pygame
- Main Loop:
- Process input events
- Update game logic (if playing)
- Draw current state
- Maintain 60 FPS with clock.tick(60)
- State Management: Switch between menu and gameplay based on user input
Key Programming Concepts Used
- Object-Oriented Design: Separate classes for different game elements
- Event-Driven Programming: Responding to keyboard and quit events
- Game Loop Pattern: Continuous update-draw cycle
- State Management: Different behaviors based on current game state
- Collision Detection: Using pygame’s rectangle collision methods
- Vector Math: Ball movement with X/Y velocity components
The code is structured to be easily extensible – you could add features like AI opponents, power-ups, or different game modes by building on this foundation.
Running Pong
Now that you have the code and understand how to use it, you are ready to run the code. You can run your code in your Python IDE or on the command line like this:
python pong.py
When you first launch your game, you will see the following welcome screen:

If you press the SPACE key, you will see the actual game, which will look something like this:

Grab a friend or play against yourself. The game works pretty well, although playing solo can be rather challenging.
Pong is a fun little game. Much like ping-pong or table tennis, the game is designed to be played with someone else. Technically, you could also create a computer opponent, but you usually play Pong between two people instead.
You should play the game with a friend or family member. You can also study the code to discover how it works. Then, when you are ready, you can start improving the game.
Get the Book
This article is from my latest Python book, Vibe Coding Video Games with Python.
You can get Vibe Coding Video Games with Python at the following websites:
The post Vibe Coding Pong with Python and pygame appeared first on Mouse Vs Python.
Real Python
Quiz: Using Jupyter Notebooks
In this quiz, you’ll test your understanding of Using Jupyter Notebooks.
By working through this quiz, you’ll review Jupyter Notebook basics: running code cells, switching modes, using shortcuts, formatting Markdown, managing the server, and exporting notebooks.
Use it to check your workflow knowledge and spot areas to review.
[ 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 ]
March 29, 2026
"Michael Kennedy's Thoughts on Technology"
Fire and Forget at Textual
If you read my Fire and Forget (or Never) about Python and asynchronous programming, you could think it’s a super odd edge case. But a reader/listener, Richard, pointed me at Will McGugan’s article The Heisenbug lurking in your async code. This is basically the same article, but in Will-style.
Will does say “This behavior is well documented, as you can see from this excerpt.” True, but the documentation got this emphasis and warning in Python 3.12 whereas the feature create_task was added in Python 3.6/3.5 timeframe. So it’s not just a matter of did we read the docs carefully. It’s a matter of did we reread the docs carefully, years later?
Luckily Will added some nice concrete numbers I didn’t have:
https://github.com/search?q=%22asyncio.create_task%28%22&type=code
This appears in over 0.5M separate code files on GitHub. To be clear, not every search result for create_task uses the fire-and-forget pattern, but just on the first page of results there are 5 instances.
If the design pattern to fix this is to:
- Create a global set
- When a task is added to the event loop, add it to the set
- Remove it from the set when it’s done
Wouldn’t it have been better for the Python team to add this to the event loop internally once and solve this problem for everyone globally across the entire Python ecosystem?
It doesn’t look like that’s going to happen. So make sure you double check your code for create_task. And don’t let the Heisenbugs bite.
And yes, I know about task groups. Several people told me that we could use task groups to hang on to the task. Yes, that’s true. But task groups are incongruent with the fire-and-forget design pattern. Why? Because you create the group in a context manager and then you wait for all the tasks in the group to be finished. That doesn’t allow you to fire off a task and then continue working. So task groups may or may not have fixed Will’s problem, but they don’t solve the one I was originally talking about.
March 28, 2026
EuroPython
Humans of EuroPython: Jodie Burchell
What does it take to run Europe’s largest Python conference? 🐍 Not budgets or venues—it’s people.
EuroPython isn’t powered by code alone, but by a vibrant network of volunteers who shape every session and welcome every attendee. From ensuring talks run seamlessly to curating world-class content these are the unsung heroes building community, one contribution at a time.
We’re shining a spotlight on the people behind the magic. Read our full conversation with Jodie Burchell, co-lead of the EuroPython 2025 Programme Team and discover what drives those who give their time to grow our community.
Jodie Burchell, Co-Lead of the Programme Team at EuroPython 2025EP: What first inspired you to volunteer for EuroPython?
I first attended EuroPython in 2023, and was asked by my friends Cheuk and Lais to help run the Humble Data workshop. I had so much fun, and really liked all the people I met, so I decided to help out with comms and other things in 2024, and ended up working on the Programme Team and helping run the Beginners’ Day in 2025.
EP: What was your primary role as a volunteer, and what did a typical day look like for you?
I was one of two co-team leads of the Programme Team in 2025. This team tends to touch a lot of the conference, although the tasks vary from week-to-week. We actually started work all the way back in December, and worked up until the end of the conference! My team’s role included running the CfP, selecting talks, and assembling the schedule, finding keynote speakers, organising special events, coordinating the open spaces, and finding last minute speakers when people cannot make it. It involved a lot of logistics, following up with other teams at EuroPython, and communicating with speakers.
I think one of my favourite things I organised at the conference was the international snack exchange. Seeing people sharing snacks from their home countries was so much fun, and really made us feel like a big international family.
EP: What&aposs your favorite memory from volunteering at EuroPython?
I actually can’t pick just one!
One of my favourites was seeing the programme team in person after so many months of working together, and sharing some international snacks together to celebrate.
Watching the excerpt of “Python: the Documentary” that CultRepo created for us, and seeing the reaction of the audience to the film and the panel was very moving.
And of course, running Humble Data at the Beginner’s Day during the sprints. As someone with a non-traditional path into tech myself, I am really passionate about helping beginners and making them feel welcome, and having beginners starting to learn Python, and then speaking with core developers of well-established projects was really special.
EP: Did you make any lasting friendships or professional connections through volunteering?
Many! The Python community is incredible, and I am lucky to have found some of my closest friends through the EuroPython, Humble Data and wider Python community. I look forward to EuroPython every year (in whatever capacity I attend) so I can see all of these amazing, special friends.
EP: What&aposs one misconception about conference volunteering you&aposd like to clear up?
I think one of the biggest misconceptions that people have about community conferences like EuroPython is that they’re run by professionals. While the EuroPython Society does have one (very talented) paid employee, most of the work you see at these conferences is done by members of the community, just like you and me. So if you feel inspired to contribute to EuroPython or another Python conference, reach out and find out how you can help! Although it can be a lot of work, it’s also very meaningful to know you’ve shaped an event that means a lot to the Python community.
EP: Thank you for your work, Jodie!
March 27, 2026
Real Python
The Real Python Podcast – Episode #289: Limitations in Human and Automated Code Review
With the mountains of Python code that it's possible to generate now, how's your code review going? What are the limitations of human review, and where does machine review excel? Christopher Trudeau is back on the show this week with another batch of PyCoder's Weekly articles and projects.
[ 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: Interacting With REST APIs and Python
In this quiz, you’ll test your understanding of Interacting with REST APIs in Python.
This quiz reviews REST principles, HTTP methods, status codes, and Python tools like requests, Flask, FastAPI, and Django REST Framework.
Test your understanding of consuming and designing REST APIs, Pydantic validation, and endpoint design. For more practice, revisit the course page for guided lessons and examples.
[ 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 ]
Holger Krekel
Hello world!
Welcome to WordPress. This is your first post. Edit or delete it, then start writing!
March 26, 2026
Real Python
Quiz: Getting Started With Django: Building a Portfolio App
In this quiz, you’ll test your understanding of Getting Started With Django: Building a Portfolio App.
By working through this quiz, you’ll review core Django concepts like projects, apps, URL routing, views, templates, models, and migrations.
You’ll reinforce how Django’s framework pieces fit together to handle requests and interact with the database. This quiz helps solidify the foundations needed to build and structure real-world Django applications.
[ 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 ]
EuroPython
March Newsletter: Sponsorship Early Bird Ending, Programme Due Soon
Hey there! 👋
Hope you&aposre all having a fantastic March. We sure have been busy and we’ve got some exciting updates for you as we gear up for EuroPython 2026. This year the conference will take place in Kraków, the city of castles and dragons 🐉 Let’s dive right in!
🗃️ Programme Updates Coming Shortly
Our Programme Team is hard at work putting the final touches on an amazing lineup. We received 635 fantastic proposals this year, a whooping record high at EuroPython. We are incredibly grateful to everyone who took the time to write a proposal — the conference wouldn’t be possible without you.
We closed the Community Voting phase with 20,799 individual reviews! Being able to use the views of the community to inform our selection process is super important to us, and such a great turnout really helps us create the best conference possible. Anyone who participated in EuroPython in the past 3 years or submitted a proposal this year was eligible to vote, and almost 400 people took part in the review process.
We are currently finalising the selection and will start sending emails out to all submitters in the coming days. Hang in there!
🤖 Beyond Python
EuroPython is not just a conference focused on Python internals, tooling, and libraries - although we’ll have plenty of those sessions of course! For a few years now, the conference has included two other major topics related to our favourite programming language:
➡️ Data Science
Tutorials and talks in this area have been covering data preparation and visualisation, data engineering, MLOps, as well as Jupyter and scientific Python.
➡️ Machine Learning & AI
From machine learning research and application, through to natural language processing, computer vision, ethics, and beyond — we’re covering it all at EuroPython.
Do you know someone who is interested in these topics? Let them know and we’ll see you all in Kraków!
💰 Sponsorship: Early Bird Ends on March 31st
If you&aposre passionate about supporting EuroPython and helping us make the event accessible to all, consider becoming a sponsor or asking your employer to join us in this effort.
By sponsoring EuroPython, you’re not just backing an event – you&aposre gaining highly targeted visibility that will present your company or personal brand to one of the largest and most diverse Python communities in the world!
But don’t take our world for it, hear what sponsors from 2025 have to say about EuroPython:
EuroPython 2025 Sponsor Highlights
We offer a range of sponsorship tiers, some with limited slots available. Along with our main packages, there are optional add-ons and extras to craft your brand messaging in exactly the way that you need.
Don’t wait too long if you’d like to sponsor the conference. We’ve already sold out 80% of the Gold packages!
🐦 We have an Early Bird 10% discount for companies that sign up by March 31st.
👉 More information at: https://ep2026.europython.eu/sponsorship/sponsor/
👉 Contact us at sponsoring@europython.eu
💰 Financial Aid
Our Financial Aid Program is here to make sure that your financial situation never stands between you and EuroPython.
We want to support those who make the Python community a positive, inclusive, and welcoming space. We especially encourage applications from speakers, students, volunteers, community organizers, and open-source contributors who need financial support to attend.
👉 For more information about our Financial Aid programme and our selection criteria, please visit https://ep2026.europython.eu/finaid/
🤝 Community Partners
EuroPython organisers are dedicated to supporting and growing the European Python community — even beyond the conference. We are proud that our Community Partners, who all share our mission, include some of the most established meetups and organisations across Europe. Meet some of the local communities that will be joining us in Kraków this year:
🚣 Python Łódź
Python Łódź is a local community of Python enthusiasts organizing regular meetups in Łódź, Poland. They bring together developers of all experience levels to share knowledge and connect.
Their events take place on the 13th floor with a beautiful view of the city, are streamed live on YouTube, and - for a bit of fun and nostalgia - even recorded on cassette tapes.
Python Łódź is currently looking for additional sponsors to support their events, as well as people interested in helping with organisation and community building. Any support - financial, logistical, or promotional - would help them grow and keep the meetups accessible and high-quality.
👉 Reach out to Python Łódź directly https://www.linkedin.com/company/python-lodz/
🐴 Pykonik
Python user group based in Kraków organizing regular meetups for enthusiasts. The main goal of the initiative is to exchange experiences face-to-face and channel the energy of enthusiasts of this dynamic programming language into concrete actions for the good of humanity.
At their latest meetup Pykonik members discussed what should there be more of at EuroPython 2026
Fun fact: the name “Pykonik” is a play on words Python and Lajkonik, an unofficial symbol of Kraków.
🌐 You can find more information about EuroPython 2026 Community Partners at https://ep2026.europython.eu/community-partners/
📣 Community Outreach
The EuroPython Society isn’t just present at other Python events—we actively support them too! As a community partner, we love helping local PyCons grow and thrive. We love giving back to the community and strengthening Python events across Europe! 🐍💙
🍕Python Pizza Ostrava
In February we joined our friends in Ostrava to support Python Pizza. Organized entirely by volunteers, the event brought the Python Community together for some amazing original Pizza™ and Python Talks. It was hosted by Impact Hub Ostrava and each talk was 10 minutes long.
Moisés Guimarães, as usual, was channeling his inner Cthulu. This time at Python Pizza Ostrava.📽️ Watch the stream from the micro conference Ostrava Python Pizza 2026
🌐 Have a look at their website: https://ostrava.python.pizza/
🔓FOSDEM 2026
FOSDEM is Europe&aposs largest free and open source software conference, bringing together thousands of developers every year in Brussels. Members of the EuroPython Society have been attending for a long time, and this year marked the first time in many years that the Python community had a stand at the event.
We were so pleased to share a spot in the exhibition space with our friends at the Django Software Foundation and the Python Software Foundation. Thanks to everyone who came to say hello!
ABP! Always Be Promoting EuroPython CfP :) Angel Ramboi was the ultimate recruiter of speakers.🌐 You can find our full report at https://europython-society.org/europython-society-at-fosdem-2026/
💝 Humans of EuroPython
EuroPython – the oldest Python conference – is led by volunteers. In 2025, EuroPython was a gathering of over 1,500 attendees and 160+ speakers across six tracks. We hosted tutorials, summits, workshops, and a full day of events for beginners.
Organising all of this took a lot of time and care: building the programme, ordering catering, answering help desk questions, MCing each stage, and much more. None of this would have been possible without the (huge!) group of committed volunteers who dedicated their time to making EuroPython happen.
Here’s what two of them said about their experience volunteering last year:
“I’ve built lasting friendships, met people I had only known from online talks and tutorials, and had the chance to become a familiar face in the community myself. Every EuroPython, and every local conference, strengthens those connections and leaves you with renewed energy and inspiration to keep contributing.”
Jakub Červinka, Operations Team
“Seeing how an event like this is created from the inside is incredible, plus the team is lovely, and you&aposll learn a lot because you’ll be surrounded by top people from the community.”
Marina Moro López, Programme Team
We wanted to shine a light on some of the members of the EuroPython 2025 organising team in our interview series Humans of EuroPython.
📰 Read what Jakub, Marina, and others gained whilst contributing to the conference https://blog.europython.eu/tag/humans-of-europython/
👋 Stay Connected
Follow us on social media for all the updates:
- LinkedIn: https://www.linkedin.com/company/europython/
- X/Twitter: https://x.com/europython
- Mastodon: https://fosstodon.org/@europython
- Bluesky: https://bsky.app/profile/europython.eu
- Instagram: https://www.instagram.com/europython/
- YouTube: https://www.youtube.com/@EuroPythonConference
That&aposs all for now! Keep an eye on your inbox, our website, and social media for more news and announcements. We&aposre counting down the days until we can come together in Kraków to celebrate our shared love for Python. 🐍❤️
Cheers,
The EuroPython Team
Sign up for EuroPython Blog
The official blog of everything & anything EuroPython! EuroPython 2026 13-19 July, Kraków
No spam. Unsubscribe anytime.
Brett Cannon
Why pylock.toml includes digital attestations
A Python project got hacked where malicious releases were directly uploaded to PyPI. I said on Mastodon that had the project used trusted publishing with digital attestations, then people using a pylock.toml file would have noticed something odd was going on thanks to the lock file including attestation data. That led to someone asking for a link to something to explain what I meant. I didn&apost have a link handy since it&aposs buried in 4 years and over 1,800 comments of discussion, so I figured I would write a blog post. 😁
Since trusted publishing is a prerequisite for digital attestations, I&aposll cover that quickly. Basically you can set a project up on PyPI such that a continuous deployment (CD) system can upload a release to PyPI on your behalf. Since PyPI has to trust the CD system to do security right as it lets other sites upload to PyPI on your behalf, not every CD system out there is supported, but the big ones are and others get added as appropriate. Since this helps automate releases without exposing any keys to PyPI that someone might try to steal, it&aposs a great thing to make your life as a maintainer easier while doing something safer; win-win!
Digital attestations are a way for a CD system to attest that a file came from that CD system. That&aposs handy as once you know where a file should come from you can verify that fact to make sure nothing nefarious is going on. And since this is just a thing to flip on, it&aposs extremely simple to do. If you use the official PyPA publish action for GitHub Actions, you get it automatically. For other CD systems it should be a copy-and-paste thing into your CD configuration.
Now, the thing that pylock.toml records is who the publisher is for a file. Taking packaging as an example, you can look at the provenance for packaging-26.0-py3-none-any.whl that comes from the digital attestation and you will notice it tells you the file came from GitHub via the pypa/packaging repo, using the publish.yml workflow run in the "pypi" environment (which you can also see via the file&aposs details on PyPI):
"publisher": {
"environment": "pypi",
"kind": "GitHub",
"repository": "pypa/packaging",
"workflow": "publish.yml"
}So what can you do with this information once it&aposs recorded in your pylock.toml? Well, the publisher details are stored for each package in the lock file. That lets code check that any files listed in the lock file for that package version were published from the same publisher that PyPI or whatever index you&aposre using says the file came from. So if the lock file and index differ on where they say a file came from, something bad may have happened.
What can you do as a person if you don&apost have code to check that things line up (which isn&apost a lot of code; the lock file should have the index server for the package, so you follow the index server API to get the digital attestation for each file and compare)? There are two things you can do manually. One, if you know that a project uses trusted publishing then that digital attestation details should be in the lock file (you can manually check by looking at the file details on PyPI); if it&aposs missing or changed to something suspicious then something bad may have happened. Two, when looking at a PR to update your lock file (and pylock.toml was designed to be human-readable), if digital attestation details suddenly disappear then something bad probably happened.
So to summarize:
- Use trusted publishing if you&aposre a maintainer
- Upload digital attestations if you&aposre a maintainer
- Use lock files where appropriate (and I&aposm partial to
pylock.toml😁) - If you&aposre using
pylock.tomlhave code check the recorded attestations are consistent - When reviewing lock file diffs (which you should do!), make sure the digital attestations don&apost look weird or were suddenly deleted
A special thanks to William Woodruff, Facundo Tuesca, Dustin Ingram, and Donald Stufft for helping to make trusted publishers and digital attestations happen.
March 25, 2026
Talk Python to Me
#542: Zensical - a modern static site generator
If you've built documentation in the Python ecosystem, chances are you've used Martin Donath's work. His Material for MKDocs powers docs for FastAPI, uv, AWS, OpenAI, and tens of thousands of other projects. But when MKDocs 2.0 took a direction that would break Material and 300 ecosystem plugins, Martin went back to the drawing board. The result is Zensical: A new static site generator with a Rust core, differential builds in milliseconds instead of minutes, and a migration path designed to bring the whole community along.<br/> <br/> <strong>Episode sponsors</strong><br/> <br/> <a href='https://talkpython.fm/sentry'>Sentry Error Monitoring, Code talkpython26</a><br> <a href='https://talkpython.fm/training'>Talk Python Courses</a><br/> <br/> <h2 class="links-heading mb-4">Links from the show</h2> <div><strong>Guest</strong><br/> <strong>Martin Donath</strong>: <a href="https://github.com/squidfunk?featured_on=talkpython" target="_blank" >github.com</a><br/> <br/> <strong>Zensical</strong>: <a href="https://zensical.org?featured_on=talkpython" target="_blank" >zensical.org</a><br/> <strong>Material for MkDocs</strong>: <a href="https://squidfunk.github.io/mkdocs-material/?featured_on=talkpython" target="_blank" >squidfunk.github.io</a><br/> <strong>Getting Started</strong>: <a href="https://zensical.org/docs/get-started/?featured_on=talkpython" target="_blank" >zensical.org</a><br/> <strong>Github pages</strong>: <a href="https://docs.github.com/en/pages?featured_on=talkpython" target="_blank" >docs.github.com</a><br/> <strong>Cloudflare pages</strong>: <a href="https://pages.cloudflare.com?featured_on=talkpython" target="_blank" >pages.cloudflare.com</a><br/> <strong>Michaels Example</strong>: <a href="https://gist.github.com/mikeckennedy/f03686c4c4ce7ce88b41c6b91c3226cf?featured_on=talkpython" target="_blank" >gist.github.com</a><br/> <strong>Material for MkDocs</strong>: <a href="https://zensical.org/docs/setup/basics/#transition-from-mkdocs" target="_blank" >zensical.org</a><br/> <strong>gohugo.io/content-management/shortcodes</strong>: <a href="https://gohugo.io/content-management/shortcodes/?featured_on=talkpython" target="_blank" >gohugo.io</a><br/> <strong>a sense of size of the project</strong>: <a href="https://blobs.talkpython.fm/zensical-size.webp?cache_id=fe7bda" target="_blank" >blobs.talkpython.fm</a><br/> <strong>Zensical Spark</strong>: <a href="https://zensical.org/spark/?featured_on=talkpython" target="_blank" >zensical.org</a><br/> <br/> <strong>Watch this episode on YouTube</strong>: <a href="https://www.youtube.com/watch?v=V1BvvIPUzes" target="_blank" >youtube.com</a><br/> <strong>Episode #542 deep-dive</strong>: <a href="https://talkpython.fm/episodes/show/542/zensical-a-modern-static-site-generator#takeaways-anchor" target="_blank" >talkpython.fm/542</a><br/> <strong>Episode transcripts</strong>: <a href="https://talkpython.fm/episodes/transcript/542/zensical-a-modern-static-site-generator" 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>
PyCharm
Expanding Our Core Web Development Support in PyCharm 2026.1
With PyCharm 2026.1, our core IDE experience continues to evolve as we’re bringing a broader set of professional-grade web tools to all users for free. Everyone, from beginners to backend-first developers, is getting access to a substantial set of JavaScript, TypeScript, and CSS features that were previously only available with a Pro subscription.
React, JavaScript, TypeScript, and CSS support
Leverage a comprehensive set of editing and formatting tools for modern web languages within PyCharm, including:
- Basic React support with code completion, component and attribute navigation, and React component and prop rename refactorings.
- Advanced import management:
- Enjoy automatic JavaScript and TypeScript imports as you work.
- Merge or remove unnecessary references via the Optimize imports feature.
- Get required imports automatically when you paste code into the editor.
- Enhanced styling: Access CSS-tailored code completion, inspections, and quick-fixes, and view any changes in real time via the built-in web preview.
- Smart editor behavior: Utilize smart keys, code vision inlay hints, and postfix code completions designed for web development.
Navigation and code intelligence
Finding your way around web projects is now even more efficient with tools that allow for:
- Pro-grade navigation: Use dedicated gutter icons for Jump to… actions, recursive calls, and TypeScript source mapping.
- Core web refactorings: Perform essential code changes with reliable Rename refactorings and actions (Introduce variable, Change signature, Move members, and more).
- Quality control: Maintain high code standards with professional-grade inspections, intentions, and quick-fixes.
- Code cleanup: Identify redundant code blocks through JavaScript and TypeScript duplicate detection.
Frameworks and integrated tools
With the added essential support for some of the most popular frontend frameworks and tools, you will have access to:
- Project initialization: Create new web projects quickly using the built-in Vite generator.
- Standard tooling: Standardize code quality with integrated support for Prettier, ESLint, TSLint, and StyleLint.
- Script management: Discover and execute NPM scripts directly from your
package.json. - Security: Check project dependencies for security vulnerabilities.
We’re excited to bring these tried and true features to the core PyCharm experience for free! We’re certain these tools will help beginners, students, and hobbyists tackle real-world tasks within a single, powerful IDE. Best of all, core PyCharm can be used for both commercial and non-commercial projects, so it will grow with you as you move from learning to professional development.
Real Python
How to Use Git: A Beginner's Guide
This tutorial shows you how to use Git to track changes in a project using just a few core commands and save clean snapshots of your work. If you’ve ever changed a file, broken something, and wished you could undo it, version control makes that possible. Git keeps a running history of your files so you can see what changed and when.
In this guide, you’ll set up Git locally and use the core workflow from the terminal to track and record changes in a Python project. By the end, you’ll have a working Git repository with a recorded commit history you can inspect and manage:
Commit History Displayed With git log
In the next sections, you’ll create your own repository and begin building that history from scratch. Before you begin, you can download a Git cheat sheet to keep the core commands handy:
Get Your Cheat Sheet: Click here to download your free Git cheat sheet and keep the core Git workflow, essential commands, and commit tips at your fingertips.
Take the Quiz: Test your knowledge with our interactive “How to Use Git: A Beginner's Guide” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
How to Use Git: A Beginner's GuideTest your knowledge of Git basics: initializing repos, staging files, committing snapshots, and managing your project history.
How to Use Git: Prerequisites
Before you start tracking your code with Git, make sure you have the right tools in place. This tutorial assumes that you’re comfortable working with the command line and have some basic Python knowledge.
Here’s what you’ll need to get started:
- A terminal or command prompt
- Python 3.10 or higher installed on your system
Note: Git and GitHub are often confused, but they’re not the same thing:
- Git is version control software that runs on your computer. It tracks changes to your files and manages your project’s history locally.
- GitHub is an online platform for hosting Git repositories. It provides collaboration tools that make sharing code, working with teams, and backing up your projects easier.
You don’t need a GitHub account to use Git or follow this tutorial. Later, if you want to share your code with others or back it up online, you can optionally push your Git repository to platforms like GitHub, GitLab, or Bitbucket.
To learn more about the differences between Git and GitHub, check out Introduction to Git and GitHub for Python Developers.
With these prerequisites in place, you’re ready to begin setting up Git and tracking changes in your project. In the next step, you’ll install Git, prepare your existing Python files, and initialize your first repository.
Step 1: Install Git and Prepare Your Project
To start, you’ll check whether Git is installed on your system, prepare a simple project, and initialize a Git repository so you can begin tracking changes right away.
Check Whether Git Is Already Installed
Before you can start using Git, you need to make sure it’s installed on your machine. Chances are that Git is already present on your system. To check whether Git is installed, run this command:
$ git --version
If this command displays a Git version, you’re good to go and can create a project directory. Otherwise, you need to install Git on your system before continuing.
Install Git on Your System
Luckily, Git provides installers for Windows, macOS, and Linux on its official website, offering a straightforward way to install Git on your machine. Because installation steps vary across operating systems, this guide links to the official documentation rather than reproducing those steps here.
If you prefer a graphical interface, you can install a Git client such as GitHub Desktop, Sourcetree, or GitKraken. These tools install Git automatically during setup.
Once installed, open your terminal and confirm that Git is available:
$ git --version
git version 2.24.0.windows.2
Your Git version may appear slightly different from this example, depending on your operating system and when you installed Git. That’s perfectly fine. As long as Git is installed and the command runs successfully, you’ll be able to follow along with the rest of this tutorial without any issues.
Create a Project Directory
Read the full article at https://realpython.com/how-to-use-git/ »
[ 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: Using Data Classes in Python
Revisit how Python data classes work with Python Data Classes.
You’ll review how to define data classes, add default values to fields, customize ordering and comparisons, and work with immutable data classes.
This quiz is based on the video course and reinforces the practical syntax and patterns you need to create concise classes and reduce boilerplate.
[ 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 ]
Kevin Renskers
Building modern Django apps with Alpine AJAX, revisited
About nine months ago I wrote an article about my quest to simplify my web development stack. How I went from SvelteKit on the frontend and Django on the backend, to an all-Django stack for a new project, using Alpine AJAX to enable partial page updates.
I’ve now been using this new stack for a while, and my approach -as well as my opinion- has changed significantly. Let’s get into what works, what doesn’t, and where I ended up.
A quick recap
Alpine AJAX is a lightweight alternative to htmx, which you can use to enhance server-side rendered HTML with a few attributes, turning <a> and <form> tags into AJAX-powered versions. No more full page refreshes when you submit a form.
The key mechanic: when a form has x-target="comments", Alpine AJAX submits the form via AJAX, finds the element with that ID in the response, and swaps it into the page. The server returns HTML, not JSON.
In the original article I used django-template-partials (since merged into Django itself) to mark sections of a template as named partials using {% partialdef %}. Combined with a custom AlpineTemplateResponse the view could automatically return just the targeted partial when the request came from Alpine AJAX.
Where I began: template partials
Let’s say you have an article page with the article body parsed from Markdown, a like button, and a comment section. The template looks something like this:
article.html{% extends "base.html" %} {% block body %} <article> <h1>{{ article.title }}</h1> {{ article_html|safe }} {% partialdef like_form inline %} <form method="post" id="like_form" x-target="like_form"> {% csrf_token %} <button type="submit" name="toggle-like"> {% if article.is_liked %}Unlike{% else %}Like{% endif %} </button> </form> {% endpartialdef %} {% partialdef comments inline %} <div id="comments"> {% for comment in article.comments.all %} <div>{{ comment.user }}: {{ comment.text }}</div> {% endfor %} <form method="post" x-target="comments"> {% csrf_token %} {{ comment_form }} <button type="submit" name="add-comment">Submit</button> </form> </div> {% endpartialdef %} </article> {% endblock %} Every form POSTs to the same article view, which handles all the actions in one big post method:
views.pyclass ArticleView(View): def get_context(self, request, pk): article = get_object_or_404( Article.objects.prefetch_related("comments") .annotate_is_liked(request.user), pk=pk, ) return { "article": article, "article_html": markdown(article.body), "comment_form": CommentForm(), } def post(self, request, pk): context = self.get_context(request, pk) article = context["article"] if "toggle-like" in request.POST: if article.is_liked: article.unlike(request.user) article.is_liked = False else: article.like(request.user) article.is_liked = True return AlpineTemplateResponse(request, "article.html", context) if "add-comment" in request.POST: form = CommentForm(request.POST) if form.is_valid(): Comment.objects.create(article=article, user=request.user, ...) return AlpineTemplateResponse(request, "article.html", context) return redirect(article) def get(self, request, pk): context = self.get_context(request, pk) return AlpineTemplateResponse(request, "article.html", context) (Please note that I’m not saying this is the correct way to do things, simply that this is how I used to do it in this particular project.)
The AlpineTemplateResponse from the original article takes care of automatically returning just the targeted partial when the request comes from Alpine AJAX. This works fine.
I thought I was being smart to prevent template duplication this way, but there are two problems:
-
The view does too much work. Every POST action calls
get_context, which fetches everything: the article, the parsed Markdown body, the comments, the like state, the comment form. When the user clicks “Like”, we do all this work that we’ll never use in the partial template. The template partial means the response is small, but the server-side work is exactly the same as rendering the full page. -
The template is a mess. Those
{% partialdef %}blocks scattered throughout the template make it noisy and hard to read. In a small example it’s fine, but in a real template with 200+ lines, it gets ugly fast.
When doubt set in: switching to Jinja2
To be honest though, the real killer of my motivation while working on this project has been the Django Template Language. I’m sorry, but I just hate it. I have since 2009, and I still do. The syntax is bad enough, but then you have to constantly fight its limitations. The fact I can’t simply call a function is so incredibly annoying, and is causing way more boilerplate with tons of custom template tags and filters.
So, switch to Jinja2, right? Except that template partials aren’t supported in combination with Jinja2. No more {% partialdef %}. Which means returning full page responses for AJAX requests, which isn’t exactly ideal.
I did it anyway. I ripped out all the {% partialdef %} tags, migrated my templates to Jinja2, and my views just returned the full template for AJAX requests. Alpine AJAX is smart enough to extract the elements it needs by their IDs, and throws away the rest.
This was simpler and I was much happier writing Jinja2 templates, but the wastefulness got worse. Before, the server at least returned a small response. Now it rendered the entire page and sent all of it over the wire, just for the browser to use a tiny piece of it. Of course it’s still better than an old-fashioned MPA, where every response is a full page refresh, but not by a lot.
It was at this moment that I seriously thought about throwing the entire frontend away and rebuilding it in SvelteKit, with Django REST Framework returning JSON responses. But that seemed like a pretty big waste of effort, so instead I took a deep breath and thought about what I wanted:
- Jinja2 templates. Non-negotiable.
- Small, fast AJAX responses. No rendering the full page for a like toggle.
- No template duplication between the full page and the AJAX response.
- Simple views that only do the work they need to do.
Template partials gave me #2 and #3, but not #1 or #4. Switching to Jinja2 and returning the full template for AJAX requests gave me #1 and #3, but not #2 or #4. I needed a different approach.
Where I ended up: separate views with template includes
The answer turned out to be straightforward, and the one I initially discarded as “too much boilerplate”: instead of one monolithic view handling all POST actions, split each action into its own view with its own URL. And instead of {% partialdef %}, use plain {% include %} tags to extract reusable template fragments.
Let me show you. Here’s the simplified article template:
article.html{% extends "base.html" %} {% block body %} <article> <h1>{{ article.title }}</h1> {{ article.body }} {% include "articles/_like_form.html" %} {% include "articles/_comments.html" %} </article> {% endblock %} Clean and readable. Each include is a self-contained fragment. And here’s the like form:
_like_form.html<form method="post" action="{{ url('toggle-like', args=[article.id]) }}" id="like_form" x-target="like_form"> {{ csrf_input }} {% if article.is_liked %} <button type="submit">Unlike</button> {% else %} <button type="submit">Like</button> {% endif %} </form> And finally, the view:
views.pyclass ToggleLikeView(LoginRequiredMixin, View): def post(self, request, pk): article = get_object_or_404( Article.objects.annotate_is_liked(request.user), pk=pk, ) if article.is_liked: article.unlike(request.user) article.is_liked = False article.like_count -= 1 else: article.like(request.user) article.is_liked = True article.like_count += 1 if is_alpine(request): return TemplateResponse( request, "articles/_like_form.html", {"article": article}, ) # For non-Alpine requests, we just redirect back return redirect(article) No comment queries. No form building. No Markdown parsing. Just the like state.
The is_alpine check provides a redirect fallback for non-JavaScript POST requests, keeping things progressive. And the ArticleView itself becomes GET-only. No more branching on POST keys. No get_context method that fetches everything for every action. Each view does one thing.
The trade-offs
There are a few downsides to this approach that are worth mentioning.
More templates. For the article page, I went from one template to several: the include fragments (_like_form.html, _comments.html) that are shared between the full page and the AJAX responses. When an action needs to update multiple elements on the page, you also end up with small response templates that combine the right includes. For example, if submitting a comment should update both the comment list and a comment count elsewhere on the page:
_add_comment_response.html{% include "articles/_comments.html" %} {% include "articles/_engagement_counts.html" %} Trivial, but still a file you have to create and name.
It’s also harder to make sure that the template fragment has access to the context it needs when included into the big template via {% include %}, compared to {% partialdef %} and one single view always rendering it.
More views and URL routes. Each action gets its own view class and its own path() entry. For a page with likes, comments, and subscriptions, that’s three or four extra views.
But here’s what I got in return:
Actual performance improvement. Not just smaller responses, but less work on the server. Each view only queries what it needs.
Jinja2. I’m using Jinja2 instead of the Django Template Language. I can call functions, I have proper expressions, and I don’t need custom template tags for basic things. This alone was worth the switch.
Readable templates. The main article.html is short and shows the page structure at a glance. Each fragment is self-contained. No {% partialdef %} blocks scattered everywhere.
Simple views. Each view does exactly one thing. Easy to understand, easy to test, easy to optimize.
Conclusion
I went through three stages: template partials with Django Template Language, full-page responses with Jinja2, and finally separated views with template includes. Each step solved a real problem with the previous approach.
The pattern I’ve ended up with requires more files and views than I’d like, but each is simple and does one thing. It’s become easier to understand the flow of every action.
My overall feelings on Django + Alpine AJAX have also changed. I still believe there are benefits to using a simplified tech stack and using hypermedia as the engine of state. Just return HTML instead of returning JSON to a JavaScript framework which then has to turn it into HTML. Conceptually it still makes sense to me.
But the dream was to build a plain old Django application using simple views and simple templates, using old-fashioned MPA server-rendered pages. Sprinkle in a few Alpine AJAX attributes and magically your site gets SPA-like usability. And it simply hasn’t played out that way for me. Yes, you could do that, if you’re fine with the wastefulness of returning full pages as a response to AJAX requests. But when you want to do it better than that, you end up with more boilerplate to make it possible to return small bits of HTML.
And this isn’t really about Alpine AJAX specifically; htmx would lead to the exact same place. The fundamental tension is in the HTML-over-the-wire approach itself: the server has to know which fragments of HTML to return, and that means structuring your views and templates around it. You trade the complexity of a JavaScript frontend for a different kind of complexity on the server.
Progressive enhancement adds to that complexity. Every form handling view needs an is_alpine check with a redirect fallback, every form needs to work both as a regular POST and as an AJAX submit. If I dropped progressive enhancement and just required JavaScript, those redirect fallbacks and the branching that comes with them would disappear. The views would be simpler. But I think progressive enhancement is important enough to keep in place.
Would I use Alpine AJAX (or htmx) again? Honestly: probably not. I have a lot more fun when building frontends with SvelteKit, and for me Django shines when I limit its role to an API, ORM, and admin interface - not so much HTML templates and form handling.
Building composable and reusable UI components is so much more natural in SvelteKit, and the performance is simply better (once the initial JS bundle has been downloaded and parsed). But am I going to throw away my current project’s code and redo it all? No, I am not. Django with Alpine AJAX is a nice change of scenery, it’s a nice playground I don’t usually get to play in. I think I ended up with a good compromise, and hey: I still don’t have to build and maintain a separate API, API docs, and frontend.
Antonio Cuni
Inside SPy, part 2: Language semantics
Inside SPy 🥸, part 2: Language semantics
This is the second post of the Inside SPy series. The firstpost was mostly about motivations andgoals of SPy. This post will cover in more detail thesemantics of SPy, including the parts which make it different from CPython.
We will talk about phases of execution, colors, redshifting, the very peculiar waySPy implements static typing, and we will start to dive into metaprogramming.
!!! Success "" Before diving in, I want to express my gratitude to my employer, Anaconda, for giving me the opportunity to dedicate 100% of my time to this open-source project.
March 24, 2026
PyCoder’s Weekly
Issue #727: Sunsetting Jazzband, Spyder, A/B Testing, and More (March 24, 2026)
#727 – MARCH 24, 2026
View in Browser »
Sunsetting Jazzband
Jazzband is (was) an Open Source cooperative for creating and maintaining projects. It maintained over 70 projects many of which were for the Django ecosystem. They included django-redis, django-nose, django-taggit, and loads more. Jazzband’s model has become untenable from the mass of AI submissions, and so it is winding down.
JAZZBAND.CO
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.
REAL PYTHON
How Nordstrom Built Self-Healing Docs with AI Agents
What if your docs updated themselves from Slack conversations? Join our webinar to learn how Nordstrom uses Temporal and AI agents to detect knowledge gaps, extract insights from chat history, and automatically generate pull requests →
TEMPORAL sponsor
Comparing Python Packages for A/B Test Analysis
A practical comparison of tea-tasting, Pingouin, statsmodels, and SciPy for A/B test analysis, with code examples.
EVGENY IVANOV
Python Jobs
Python + AI Content Specialist (Anywhere)
Articles & Tutorials
Guido Interviews Brett Cannon
After last year’s release of the Python documentary, Guido decided to explore those contributors who weren’t mentioned. He now has an going series of written interviews with a variety of contributors from Python’s first 25 years. This interview is with Brett Cannon.
GUIDO VAN ROSSUM
“Requests” Needs You to Test Type Hints
Requests is a popular HTTP client library and is frequently in the top 10 PyPI downloads. There is an on-going effort to add type hinting to the library and to make sure the next release causes few issues, they need help testing.
SETH LARSON
Depot CI: Built for the Agent era
Depot CI: A new CI engine. Fast by design. Your GitHub Actions workflows, running on a fundamentally faster engine — instant job startup, parallel steps, full debuggability, per-second billing. One command to migrate →
DEPOT sponsor
Fire and Forget (Or Never) With Python’s Asyncio
Python’s asyncio.create_task() can silently garbage collect fire-and-forget tasks in 3.12+, meaning they might never run. This article shows you how to use the background tasks set pattern to fix it.
MICHAEL KENNEDY
Thoughts on OpenAI Acquiring Astral
Astral is the organization behind popular Python tools such as uv, ruff, and ty. Recently it was announced that OpenAI would be acquiring Astral. This opinion piece discusses the possible impact.
SIMON WILLISON
Standard Error
Standard error is one of the two writable file streams that is used for printing errors, warning messages, or any outputs that shouldn’t be mixed with the main program.
TREY HUNNER
🎓 Master Python’s Core Principles (Live Course)
Transform your Python skills in just eight weeks, with live expert guidance. No more second-guessing if your code is “Pythonic enough.” Master Python’s object model, advanced iteration, decorators, and clean system design through live instruction and hands-on practice in a small group setting:
REAL PYTHON sponsor
Textual: Creating a Custom Checkbox
The Textual TUI framework allows for a lot of customization and control over its widgets. This article shows you how to change a checkbox widget to give it a new look.
MIKE DRISCOLL
A Practical Guide to Python Supply Chain Security
A comprehensive guide to securing your Python dependencies from ingestion to deployment, covering linting, pinning, vulnerability scanning, SBOMs, and attestations
BERNÁT GÁBOR
Python 3.15’s JIT Is Now Back on Track
Python 3.15’s JIT is now back on track, meeting the performance targets the team set for itself. Progress was a bit bumpy and this post talks about what happened.
KEN JIN
From Properties to Descriptors
This article is about the weird and wonderful world of descriptors in Python. Learn what they’re for and how to use one of the trickier Python concepts.
STEPHEN GRUPPETTA
Modern Python Monorepo With uv and prek
Talk Python interviews Amogh Desai and Jarek Potiuk and they talk about how to use a monorepo with uv and prek.
TALK PYTHON podcast
Downloading Files From URLs With Python
Learn to download files from URLs with Python using urllib and requests, including data streaming for large files.
REAL PYTHON course
Building a Django Chat App With WebSockets
This article covers the best ways to build a chat app in Django using Websockets and ASGI.
HONEYBADGER.IO • Shared by Addison Curtis
Projects & Code
pristan: The Simplest Way to Create a Plugin System
GITHUB.COM/MUTATING • Shared by pomponchik
MaskOps: PII Masking as a Native Polars Plugin
GITHUB.COM/FCARVAJALBROWN • Shared by Felipe Carvajal Brown
Events
Weekly Real Python Office Hours Q&A (Virtual)
March 25, 2026
REALPYTHON.COM
Django Girls Colombia 2026
March 28 to March 29, 2026
DJANGOGIRLS.ORG
Python Sheffield
March 31, 2026
GOOGLE.COM
Python Southwest Florida (PySWFL)
April 1, 2026
MEETUP.COM
STL Python
April 2, 2026
MEETUP.COM
Happy Pythoning!
This was PyCoder’s Weekly Issue #727.
View in Browser »
[ Subscribe to 🐍 PyCoder’s Weekly 💌 – Get the best Python news, articles, and tutorials delivered to your inbox once a week >> Click here to learn more ]
Mike Driscoll
The Python Show – Python Illustrated
In this episode, we hear from two sisters who put together a beginner’s book about Python. The unique hook for their book is that one sister wrote the text while the other did the illustrations. Listen in as we learn about these incredible sisters and how they got into software programming, writing, and technical education.
You can check out their book, Python Illustrated, on Packt or Amazon.
Maaike is an Udemy instructor, and she also has courses on Pluralsight.
This episode was originally published on The Python Show!
The post The Python Show – Python Illustrated appeared first on Mouse Vs Python.
Ari Lamstein
Upcoming O’Reilly Workshop: Building Data Apps with Streamlit and Copilot
On April 14, I’ll be teaching a new 4‑hour live workshop for O’Reilly: Building Data Apps with Streamlit and Copilot.
If you work in Python and want to turn your analyses into interactive, shareable tools, this workshop is designed for you. We’ll start from a Jupyter notebook and build a complete Streamlit app that lets users explore a dataset through interactive controls, charts, and maps. Along the way, we’ll use Copilot to speed up development and discover Streamlit features more efficiently.
What we’ll cover
- Structuring a Streamlit app
- Working with user input (select boxes, filters, etc.)
- Creating interactive graphics with Plotly
- Organizing the UI with columns and tabs
- Deploying your app to Streamlit Cloud
The workshop is hands‑on: you’ll build the app step‑by‑step, and by the end you’ll have a working project you can adapt to your own data.
What You’ll Build
Here’s a screenshot from the app we’ll build together:
The app lets users choose a state and demographic statistic, explore how it changes over time, and view the data as a chart, map, or table.
And while the example uses demographic data, the skills you’ll learn—structuring an app, building interactive controls, and creating dynamic visualizations—apply to any Streamlit project you want to build.
Who is this for?
- Data scientists and analysts who want to make their work more interactive
- Python users who want to build dashboards without learning web development
- Anyone curious about Streamlit or Copilot
If you’re interested, I’d love to have you join. Registration is open now.
The Python Coding Stack
3 • 7600 • 33 • 121 • When Python Stacks Up
When I was a child, I used to pace up and down the corridor at home pretending to teach an imaginary group of people. It was my way of learning.
It still is.
I started writing about Python as a learning tool—to help me sort things out in my head, weave a thread through all the disparate bits of information, clarify my thoughts, make sure any knowledge gaps are filled.
I started The Python Coding Stack three years ago. That’s the first of the mystery numbers in the post’s title revealed! I had written elsewhere before, but at the time of starting The Stack, I felt I had found my own “Python voice”. I had been teaching Python for nearly a decade. I had written plenty of articles, but setting up The Python Coding Stack was a deliberate choice to step up. I was still writing articles primarily for my own benefit, but now I was also writing for others, hoping they would want to learn the way I do.
And 7,600 subscribers apparently do. Thank you for joining this journey, whether you were there three years ago or you joined a few days ago. If you just joined, there’s an archive of 121 articles, most of them long-form tutorials or step-by-step guides.
A special thank you to the 33 subscribers who chose to upgrade to premium and join The Club. It may only amount to 3 coffees per month for you, but it makes a difference to me. Thank you! I hope you’ve been enjoying the exclusive content for The Club members.
And perhaps, if a few more decide to join you in The Club (you can surely cut three coffees out of your monthly intake!), then this publication may even become self-sustainable. Your support can make a real difference—if you value these articles and want to see them continue, please consider joining now. At the moment, I give up a lot of my time for free to think about my articles, plan them, draft them, review them technically, review them linguistically, get them ready for publication, and then publish.
I mentioned my live teaching earlier. My written articles and my live teaching have a lot in common. One of the hardest things about teaching (or communication in general) is to place yourself in the learner’s mindset. I know, it’s obvious. But it’s hard.
A string of words can make perfect sense to someone who already understands the concept, but it’s hard to understand for someone learning it for the first time.
Going from A to B can be a smooth reasoning step for an expert, but requires a few more intermediate steps for a novice.
A trait that helps me in my teaching is my ability to recall the pain points I had when learning a topic. Everything is easy once you know it, but hard when you don’t. Remembering that what comes easily today was once hard is essential for teaching, whatever the format.
I often use my writing to help me with my live teaching. And, just as often, I discover a new angle or insight during live teaching that I then put down in writing. It’s a two-way street. Both forms of communication—live teaching and writing—complement each other.
All this to say that I enjoy writing these articles. They’re useful for me personally, and for my work teaching Python. And I hope they’re useful for you.
121 articles. The cliché would have me say that choosing favourites is like choosing a favourite child. But that’s not the case. There are articles I like less than others. So, I tried to put together a highlights reel of the past three years. Here we go…
The Centre of the Python Universe • Objects
A Stroll Across Python • Fancy and Not So Fancy Tools
The Curious Little Shop at The End of My Street • Python’s f-strings
If You Find if..else in List Comprehensions Confusing, Read This, Else…
Where Do I Store This? • Data Types and Structures
Clearing The Deque—Tidying My Daughter’s Soft Toys • A Python Picture Story
Hermione’s Undetectable Extension Charm: Revealed (Using Python)
bytes: The Lesser-Known Python Built-In Sequence • And Understanding UTF-8 EncodingWhere’s William? How Quickly Can You Find Him? • What’s a Python Hashable Object?
And here are the posts in The Club section of this publication, exclusive for premium subscribers: The Club | The Python Coding Stack
Happy 3rd Birthday to The Python Coding Stack. From just under a hundred people in the first week to 7,600+ today, this community has grown thanks to your enthusiasm.
Let’s keep up the momentum—consider joining The Club today! Your membership can help ensure The Python Coding Stack continues on its path, stronger than ever.
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
Also, are you interested in technical writing? You’d like to make your own writing more narrative, more engaging, more memorable? Have a look at Breaking the Rules.
And you can find out more about me at stephengruppetta.com
Real Python
Understanding CRUD Operations in SQL
CRUD operations are at the heart of nearly every application you interact with. As a developer, you usually want to create data, read or retrieve data, update data, and delete data. Whether you access a database or interact with a REST API, only when all four operations are present are you able to make a complete data roundtrip in your app.
Creating, reading, updating, and deleting are so vital in software development that these methods are widely referred to as CRUD. Understanding CRUD will give you an actionable blueprint when you build applications and help you understand how the applications you use work behind the scenes. So, what exactly does CRUD mean?
[ 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 ]
Rodrigo Girão Serrão
Ask the LLM to write code for it
This article covers a useful LLM pattern where you ask the LLM to write code to solve a problem instead of asking it to solve the problem directly.
The problem of merging two transcripts
I had two files that contained two halves of the transcript of an audio recording and I wanted to use an LLM to merge the two halves. There were three reasons that stopped me from simply copying part 2 and pasting it after part 1:
- the two transcripts overlapped (the end of part 1 was after the start of part 2);
- the timestamps for part 2 started from 0, so they were missing an offset; and
- speaker identification was not consistent.
I uploaded the two halves into ChatGPT and asked it to merge the two transcripts, fix the timestamps and the speaker identification, but to not change the text.
The result I got back was a ridiculous attempt at providing the full transcript, with two sections that supposedly represented parts of either transcript I could just copy and paste confidently, and a couple of other ridiculous blunders.
Instead of fighting ChatGPT, I decided to use a very useful pattern I learned about last year.
Ask the LLM to write code for it
Instead of asking ChatGPT to merge the transcripts, I could ask it to analyse them, find the solutions to the three problems listed above, and then write code that would merge the transcripts.
Since I was confident that ChatGPT could
- identify the overlap between the two files;
- use the overlap information to compute the timestamp offset required for part 2; and
- figure out you had to swap the two speakers in part 2,
I knew ChatGPT would be able to write a Python script that could read from both files and apply a couple of string operations to the second part.
This yielded much better results in two ways. ChatGPT was able to find the solutions for the three problems above and write a script that fixed them automatically. That was the goal.
On top of that, since ChatGPT had a very clear implicit goal — get the final merged transcript — and since running Python code is something that ChatGPT can do, ChatGPT even ran the script for me and produced two artifacts at the end:
- the full Python script I could run against the two halves if I wanted; and
- the final, fixed transcript.
This is an example application of a really useful LLM pattern:
Don't ask the LLM to solve a problem. Instead, ask it to write code that solves the problem.
As another visual example, it's much easier to ask an LLM to write a Python script that draws a path that solves a maze (that's just a couple hundred of lines of code) than it is to upload an image and ask the LLM to draw a valid path on the picture of a maze. Try it yourself!
Real Python
Quiz: Python Modules and Packages: An Introduction
In this quiz, you’ll test your understanding of Python Modules and Packages.
By working through this quiz, you’ll revisit how to write and import modules and packages, how to structure code for modular development, and how to combine modules to create larger applications.
This quiz will help you practice organizing projects so they stay easier to maintain 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 ]


