skip to navigation
skip to content

Planet Python

Last update: January 29, 2026 04:44 PM UTC

January 29, 2026


PyBites

7 Software Engineering Fixes To Advance As A Developer

It’s January! If you look back at yourself from exactly one year ago, January 2025, how different are you as a developer from then to now?

Did you ship the app you were thinking about? Did you finally learn how to configure a proper CI/CD pipeline? Did you land the Senior role you were after?

Or did you just watch a lot more YouTube videos and buy a few more Udemy courses that you haven’t finished yet?

If the answer stings a little bit, you aren’t alone.

Over the last six years of coaching hundreds of developers in PDM, Bob and I have noticed a pattern. We see the same specific bottlenecks that keep smart, capable people stuck in Tutorial Hell for years.

They know the syntax and can solve code challenges, but they aren’t shipping.

In this week’s episode of the Pybites Podcast, we get straight to the fix. We aren’t talking about the latest Python library or a cool new feature in Django. We’re talking about the 7 Engineering Shifts you need to make to stop going in circles and actually become a professional software engineer this year.

We dive deep into the hard truths, including:

We are sharing the exact tips we give our PDM coaching clients to get them unstuck.

If you are tired of feeling productive but having nothing to show for it, this episode is for you.

Want the cheat sheet?

We condensed these 7 shifts into a brand new, high-impact guide: Escape Tutorial Hell. It breaks down every single point we discuss in the episode with actionable steps you can take today.

Download the free guide here.

Listen and Subscribe Here

January 29, 2026 11:18 AM UTC

January 28, 2026


Real Python

How Long Does It Take to Learn Python?

Have you read blog posts that claim you can learn Python in days and quickly secure a high-paying developer job? That’s an unlikely scenario and doesn’t help you prepare for a steady learning marathon. So, how long does it really take to learn Python, and is it worth your time investment?

By the end of this guide, you’ll understand that:

  • Most beginners can learn core Python fundamentals in about 2 to 6 months with consistent practice.
  • You can write a tiny script in days or weeks, but real confidence comes from projects and feedback.
  • Becoming job-ready often takes 6 to 12 months, depending on your background and target role.
  • Mastery takes years because the ecosystem and specializations keep growing.

The short answer for how long it takes to learn Python depends on your goals, time budget, and the level you’re aiming for.

Get the PDF Guide: Click here to download a free PDF guide that breaks down how long it takes to learn Python and what factors affect your timeline.

Take the Quiz: Test your knowledge with our interactive “Python Skill Test” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Python Skill Test

Test your Python knowledge in a skills quiz with basic to advanced questions. Are you a Novice, Intermediate, Proficient, or Expert?

How Long Does It Take to Learn Python Basics?

Python is beginner-friendly, and you can start writing simple programs in just a few days. But reaching the basics stage still takes consistent practice because you’re learning both the language itself and how to think like a programmer.

The following timeline shows how long it typically takes to learn Python basics based on how much time you can practice each week:

Weekly practice time Typical timeline for basics What that feels like
2–3 hours/week 8–12 months Slow but steady progress
5–10 hours/week 3–6 months Realistic pace for busy adults
15–20 hours/week ~2 months Consistent focus and fast feedback
40+ hours/week ~1 month Full-time immersion

These ranges assume about five study days per week. If you add a sixth day, you’ll likely land toward the faster end of each range.

You’ll get better results if you use this table as a planning guide. Don’t think of it as rigid deadlines—your learning pace depends on many factors. For example, if you already know another programming language, then you can usually move faster. If you’re brand-new to coding, then expect to be at the slower end of each range.

As a general guideline, many beginners reach the basics in about 2 to 6 months with steady practice.

Note: If you’re ready to fast-track your learning with an expert-guided small cohort course that gives you live guidance and accountability, then check out Real Python’s live courses!

With a focused schedule of around four hours per day, five days per week, you can often reach the basics stage in roughly 6 to 10 weeks, assuming you’re writing and debugging code most sessions. By then, you should be able to finish several small projects on your own.

When you read online that someone learned Python quickly, they’re probably talking about this basics stage. And indeed, with the right mix of dedication, circumstances, and practice, learning Python basics can happen pretty fast!

Before you go ahead and lock in a timeline, take a moment to clarify for yourself why you want to learn Python. Understanding your motivation for learning Python will help along the way.

Learning Python means more than just learning the Python programming language. You need to know more than just the specifics of a single programming language to do something useful with your programming skills. At the same time, you don’t need to understand every single aspect of Python to be productive.

Learning Python is about learning how to accomplish practical tasks with Python programming. It’s about having a skill set that you can use to build projects for yourself or an employer.

As your next step, write down your personal goal for learning Python. Always keep that goal in mind throughout your learning journey. Your goal shapes what you need to learn and how quickly you’ll progress.

What’s a Practical 30-Day Learning Plan for Complete Beginners?

When you’re clear about your why, you can start drafting your personal Python learning roadmap.

If you’re starting from zero and can spend about 5 to 10 hours per week, the following plan keeps you moving without becoming overwhelming:

Aim to finish at least one small project by the end of the month. The project matters more than completing every tutorial or task on your checklist.

Read the full article at https://realpython.com/how-long-does-it-take-to-learn-python/ »


[ 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 ]

January 28, 2026 02:00 PM UTC


PyCharm

PyCharm is designed to support the full range of modern Python workflows, from web development to data and ML/AI work, in a single IDE. An essential part of these workflows is Jupyter notebooks, which are widely used for experimentation, data exploration, and prototyping across many roles.

PyCharm provides first-class support for Jupyter notebooks, both locally and when connecting to external Jupyter servers, with IDE features such as refactoring and navigation available directly in notebooks. Meanwhile, Google Colab has become a key tool for running notebook-based experiments in the cloud, especially when local resources are insufficient.

With PyCharm 2025.3.2, we’re bringing local IDE workflows and Colab-hosted notebooks together. Google Colab support is now available for free in PyCharm as a core feature, along with basic Jupyter notebook support. If you already use Google Colab, you can now bring your notebooks into PyCharm and work with them using IDE features designed for larger projects and longer development sessions.

Getting started with Google Colab in PyCharm

Connecting PyCharm to Colab is quick and straightforward:

  1. Open a Jupyter notebook in PyCharm.
  2. Select Google Colab (Beta) from the Jupyter server menu in the top-right corner.
  3. Sign in to your Google account.
  4. Create and use a Colab-backed server for the notebook.

Once connected, your notebook behaves as usual, with navigation, inline outputs, tables, and visualizations rendered directly in the editor.

Working with data and files 

When your Jupyter notebook depends on files that are not yet available on the Colab machine, PyCharm helps you handle this without interrupting your workflow. If a file is missing, you can upload it directly from your local environment. The remote file structure is also visible in the Project tool window, so you can browse directories and inspect files as you work.

Whether you’re experimenting with data, prototyping models, or working with notebooks that outgrow local resources, this integration makes it easier to move between local work, remote execution, and cloud resources without changing how you work in PyCharm.

If you’d like to try it out:

January 28, 2026 01:40 PM UTC


EuroPython

January Newsletter: We Want Your Proposals for Kraków!

Happy New Year! We&aposre kicking off 2026 with exciting news: EuroPython is moving to a brand new location! After three wonderful years in Prague, we&aposre heading to Kraków, Poland for our 25th anniversary edition. Mark your calendars for July 13-19, 2026. 🎉

🏰 Welcome to Kraków!

EuroPython 2026 will take place at the ICE Kraków Congress Centre, bringing together 1,500+ Python enthusiasts for a week of learning, networking, and collaboration. 

Check out all the details: ep2026.europython.eu/krakow

alt

📣 Call for Proposals is OPEN!

The CfP is now live, and we want to hear from YOU! Whether you&aposre a seasoned speaker or considering your first talk, tutorial or poster, we&aposre looking for proposals on all topics and experience levels.

Deadline: February 15th, 2026 at 23:55 UTC+1 (no extension, so don’t leave it for the last minute!)

We&aposre seeking:

No matter your level of Python or public speaking experience, EuroPython is here to help you bring yourself to our community. Represent your work, your interests, and your unique perspective!

Want to get some extra help? The first 100 proposals will get direct feedback from the Programme team, so hurry with your submissions!

👉 Submit your proposal by February 15th: programme.europython.eu

alt

🎤 Speaker Mentorship is Open

First time speaking? Feeling nervous? The Speaker Mentorship Programme is back! We match mentees with experienced speakers who&aposll help you craft strong proposals and, if accepted, prepare your talk. This programme especially welcomes folks from underrepresented backgrounds in tech.

Applications are open now for Mentees and Mentors. Don&apost let uncertainty hold you back – apply and join our supportive community of speakers. 

Deadline: 10th February 2026, 23:59 UTC

👉 More info: ep2026.europython.eu/mentorship

🎙️ Conversations with First-Time Speakers

Want to hear from people who&aposve been in your shoes? Check out our interviews with first-time speakers who took the leap. They share their experience of what it&aposs really like to speak at EuroPython.

👉 With Jenny Vega: https://youtu.be/0lLrQkPtOy8

👉 With Kayode Oladapo: https://youtu.be/qy7BZUJCYD4 

🎥 Video Recap from Prague

Prague was incredible! ✨ Relive the best moments from EuroPython 2025 in our video recap.

📢 Help Us Spread the Word!

Big thanks to our speaker and community organiser Honza Král for giving a lightning talk about EuroPython at Prague Pyvo. If you&aposre a speaker or community organizer, we&aposd love your help spreading the word about the CfP!

alt

💰 Sponsorship & Financial Aid

Sponsorship packages will be announced soon! Interested in supporting EuroPython 2026? Reach out to us at sponsoring@europython.eu.

Financial Aid applications will open in the coming weeks. We&aposre committed to making EuroPython accessible to everyone, regardless of financial situation. Stay tuned!

🤝  Where can you meet us this month?  

We&aposll be at FOSDEM this weekend (February 1-2) with a booth alongside the Python Software Foundation and Django Software Foundation. If you&aposre in Brussels, come say hi, grab some stickers, and get the latest EuroPython news!

We&aposre also heading to Ostrava Python Pizza! Join us for tasty pizza and good conversation about all things Python on 21st February. 

👋 Stay Connected

Follow us on social media and subscribe to our newsletter for all the updates:

January 28, 2026 10:56 AM UTC


Hugo van Kemenade

Speeding up Pillow's open and save

Tachyon #

I tried out Tachyon, the new “high-frequency statistical sampling profiler” coming in Python 3.15, to see if we can speed up the Pillow imaging library. I started with a simple script to open an image:

import sys
from PIL import Image

im = Image.open(f"Tests/images/hopper.{sys.argv[1]}")

Then ran:

$ python3.15 -m profiling.sampling run --flamegraph /tmp/1.py png
Captured 35 samples in 0.04 seconds
Sample rate: 1,000.00 samples/sec
Error rate: 25.71
Flamegraph data: 1 root functions, total samples: 26, 169 unique strings
Flamegraph saved to: flamegraph_97927.html

Which generates this flame graph:

Flame graph for opening a PNG with Pillow

The whole thing took 40 milliseconds, with half in Image.py’s open(). If you visit the interactive HTML page we can see open() calls preinit(), which in turn imports GifImagePlugin, BmpImagePlugin, PngImagePlugin and JpegImagePlugin (hover over the <module> boxes to see them).

Do we really need to import all those plugins when we’re only interested in PNG?

Okay, let’s try another kind of image:

$ python3.15 -m profiling.sampling run --flamegraph /tmp/1.py webp
Captured 59 samples in 0.06 seconds
Sample rate: 1,000.00 samples/sec
Error rate: 22.03
Flamegraph data: 1 root functions, total samples: 46, 256 unique strings
Flamegraph saved to: flamegraph_98028.html

Flame graph for opening a WebP with Pillow

Hmm, 60 milliseconds with 80% in open() and most of that in init(). The HTML page shows it imports AvifImagePlugin, PdfImagePlugin, WebpImagePlugin, DcxImagePlugin, DdsImagePlugin and PalmImagePlugin. We also have preinit importing GifImagePlugin, BmpImagePlugin and PngImagePlugin.

Again, why import even more plugins when we only care about WebP?

Loading all the plugins? #

That’s enough profiling, let’s look at the code.

When open()ing or save()ing an image, if Pillow isn’t yet initialised, we call a preinit() function. This loads five drivers for five formats by importing their plugins: BMP, GIF, JPEG, PPM and PNG.

During import, each plugin registers its file extensions, MIME types and some methods used for opening and saving.

Then we check each of these plugins in turn to see if one will accept the image. Most of Pillow’s plugins detect an image by opening the file and checking if the first few bytes match a magic prefix. For example:

If none of these five match, we call init(), which imports the remaining 42 plugins. We then check each of these for a match.

This has been the case since at least PIL 1.1.1 released in 2000 (this is the oldest version I have to check). There were 33 builtin plugins then and 47 now.

Lazy loading #

This is all a bit wasteful if we only need one or two image formats during a program’s lifetime, especially for things like CLIs. Longer running programs may need a few more, but unlikely all 47.

A benefit of the plugin system is third parties can create their own plugins, but we can be more efficient with our builtins.

I opened a PR to add a mapping of file extensions to plugins. Before calling preinit() or init(), we can instead do a cheap lookup, which may save us importing, registering, and checking all those plugins.

Of course, we may have an image without an extension, or with the “wrong” extension, but that’s fine; I expect it’s rare and anyway we’ll fall back to the original preinit() -> init() flow.

After merging the PR, here’s a new flame graph for opening PNG (HTML page):

Much less compressed flame graph showing less work

And for WebP (HTML page):

Much less compressed for WebP

The flame graphs are scaled to the same width, but there’s far fewer boxes meaning there’s much less work now. We’re down from 40 and 60 milliseconds to 20 and 20 milliseconds.

The PR has a bunch of benchmarks which show opening a PNG (that previously loaded five plugins) is now 2.6 times faster. Opening a WebP (that previously loaded all 47 plugins), is now 14 times faster. Similarly, Saving PNG is improved by 2.2 times and WebP by 7.9 times. Success! This will be in Pillow 12.2.0.

See also #

January 28, 2026 10:29 AM UTC


EuroPython

Humans of EuroPython: Rodrigo Girão Serrão

EuroPython depends entirely on the dedication of volunteers who invest tremendous effort into bringing it to life. From managing sponsor relationships and designing the event schedule to handling registration systems and organizing social events, countless hours of passionate work go into ensuring each year surpasses the last.

Discover our recent conversation with Rodrigo Girão Serrão, who served on the EuroPython 2025 Programme Team.

We&aposre grateful for your work on the conference programme, Rodrigo!

altRodrigo Girão Serrão, member of the Programme Team at EuroPython 2025

EP: Had you attended EuroPython before volunteering, or was volunteering your first experience with it?

When I attended my first EuroPython in person I was not officially a volunteer but ended up helping a bit. Over the years, my involvement with EuroPython as a volunteer and organiser has been increasing exponentially!

EP: Are there any new skills you learned while volunteering at EuroPython? If so, which ones?

Volunteering definitely pushed me to develop many skills. As an example, hosting the sprints developed my social skills since I had to welcome all the participants and ensure they had everything they needed. It also improved my management skills, from supporting the project sprint organisers to coordinating with venue staff.

EP: Did you have any unexpected or funny experiences during EuroPython?

In a recent EuroPython someone came up to me after my tutorial and said something like “I doubted your tutorial was going to be good, but in the end it was good”. Why on Earth would that person doubt me in the first place and then come to me and admit it? 🤣

EP: Did you make any lasting friendships or professional connections through volunteering?

Yes to both! Many of these relationships grew over time through repeated interactions across multiple EuroPython editions and also other conferences. Volunteering created a sense of continuity and made it much easier to connect with the same people year after year.

EP: If you were to invite someone else, what do you think are the top 3 reasons to join the EuroPython organizing team?

Nothing beats the smiles and thank you’s you get when the conference is over. Plus, it is an amazing feeling to be part of something bigger than yourself.

EP: Would you volunteer again, and why?

Hell yeah! See above :)

EP: Thanks, Rodrigo!

January 28, 2026 10:07 AM UTC


PyCharm

Google Colab Support Is Now Available in PyCharm 2025.3.2

January 28, 2026 09:33 AM UTC


Python Morsels

All iteration is the same in Python

In Python, for loops, list comprehensions, tuple unpacking, and * unpacking all use the same iteration mechanism.

Table of contents

  1. Looping over dictionaries gives keys
  2. Looping over strings provides characters
  3. Looping is looping
  4. The ups and downs of duck typing

Looping over dictionaries gives keys

When you loop over a dictionary, you'll get the keys in that dictionary:

>>> my_dict = {'red': 2, 'blue': 3, 'green': 4}
>>> for thing in my_dict:
...     print(thing)
...
red
blue
green

If you loop over a dictionary in a list comprehensions, you'll also get keys:

>>> names = [x.upper() for x in my_dict]
>>> names
['RED', 'BLUE', 'GREEN']

Iterable unpacking with * also relies on iteration. So if we use this to iterate over a dictionary, we again get the keys:

>>> print(*my_dict)
red blue green

The same thing happens if we use * to unpack a dictionary into a list:

>>> colors = ["purple", *my_dict]
>>> colors
['purple', 'red', 'blue', 'green']

And even tuple unpacking relies on iteration. Anything you can loop over can be unpacked. Since we know there are three items in our dictionary, we could unpack it:

>>> a, b, c = my_dict

And of course, as strange as it may seem, we get the keys in our dictionary when we unpack it:

>>> a
'red'
>>> b
'blue'

So what would happen if we turned our dictionary into a list by passing it to the list constructor?

>>> list(my_dict)

Well, list will loop over whatever iterable was given to it and make a new list out of it. And when we loop over a dictionary, what do we get?

The keys:

>>> list(my_dict)
['red', 'blue', 'green']

And of course, if we ask whether something is in a dictionary, we are asking about the keys:

>>> 'blue' in my_dict
True

Iterating over a dictionary object in Python will give you keys, no matter what Python feature you're using to do that iteration. All forms of iteration do the same thing in Python.

Aside: of course if you want key-value pairs you can get them using the dictionary items method.

Looping over strings provides characters

Strings are also iterables.

Read the full article: https://www.pythonmorsels.com/all-iteration-is-the-same/

January 28, 2026 12:30 AM UTC

January 27, 2026


Giampaolo Rodola

From Python 3.3 to today: ending 15 years of subprocess polling

One of the less fun aspects of process management on POSIX systems is waiting for a process to terminate. The standard library's subprocess module has relied on a busy-loop polling approach since the timeout parameter was added to Popen.wait() in Python 3.3, around 15 years ago (see source). And psutil's Process.wait() method uses exactly the same technique (see source).

The logic is straightforward: check whether the process has exited using non-blocking waitpid(WNOHANG), sleep briefly, check again, sleep a bit longer, and so on.

import os, time

def wait_busy(pid, timeout):
    end = time.monotonic() + timeout
    interval = 0.0001
    while time.monotonic() < end:
        pid_done, _ = os.waitpid(pid, os.WNOHANG)
        if pid_done:
            return
        time.sleep(interval)
        interval = min(interval * 2, 0.04)
    raise TimeoutExpired

In this blog post I'll show how I finally addressed this long-standing inefficiency, first in psutil, and most excitingly, directly in CPython's standard library subprocess module.

The problem with busy-polling

Event-driven waiting

All POSIX systems provide at least one mechanism to be notified when a file descriptor becomes ready. These are select(), poll(), epoll() (Linux) and kqueue() (BSD / macOS) system calls. Until recently, I believed they could only be used with file descriptors referencing sockets, pipes, etc., but it turns out they can also be used to wait for events on process PIDs!

Linux

In 2019, Linux 5.3 introduced a new syscall, pidfd_open(), which was added to the os module in Python 3.9. It returns a file descriptor referencing a process PID. The interesting thing is that pidfd_open() can be used in conjunction with select(), poll() or epoll() to effectively wait until the process exits. E.g. by using poll():

import os, select

def wait_pidfd(pid, timeout):
    pidfd = os.pidfd_open(pid)
    poller = select.poll()
    poller.register(pidfd, select.POLLIN)
    # block until process exits or timeout occurs
    events = poller.poll(timeout * 1000)
    if events:
        return
    raise TimeoutError

This approach has zero busy-looping. The kernel wakes us up exactly when the process terminates or when the timeout expires if the PID is still alive.

I chose poll() over select() because select() has a historical file descriptor limit (FD_SETSIZE), which typically caps it at 1024 file descriptors per-process (reminded me of BPO-1685000).

I chose poll() over epoll() because it does not require creating an additional file descriptor. It also needs only a single syscall, which should make it a bit more efficient when monitoring a single FD rather than many.

macOS and BSD

BSD-derived systems (including macOS) provide the kqueue() syscall. It's conceptually similar to select(), poll() and epoll(), but more powerful (e.g. it can also handle regular files). kqueue() can be passed a PID directly, and it will return once the PID disappears or the timeout expires:

import select

def wait_kqueue(pid, timeout):
    kq = select.kqueue()
    kev = select.kevent(
        pid,
        filter=select.KQ_FILTER_PROC,
        flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT,
        fflags=select.KQ_NOTE_EXIT,
    )
    # block until process exits or timeout occurs
    events = kq.control([kev], 1, timeout)
    if events:
        return
    raise TimeoutError

Windows

Windows does not busy-loop, both in psutil and subprocess module, thanks to WaitForSingleObject. This means Windows has effectively had event-driven process waiting from the start. So nothing to do on that front.

Graceful fallbacks

Both pidfd_open() and kqueue() can fail for different reasons. For example, with EMFILE if the process runs out of file descriptors (usually 1024), or with EACCES / EPERM if the syscall was explicitly blocked at the system level by the sysadmin (e.g. via SECCOMP). In all cases, psutil silently falls back to the traditional busy-loop polling approach rather than raising an exception.

This fast-path-with-fallback approach is similar in spirit to BPO-33671, where I sped up shutil.copyfile() by using zero-copy system calls back in 2018. In there, more efficient os.sendfile() is attempted first, and if it fails (e.g. on network filesystems) we fall back to the traditional read() / write() approach to copy regular files.

Measurement

As a simple experiment, here's a simple program which waits on itself for 10 seconds without terminating:

# test.py
import psutil, os
try:
    psutil.Process(os.getpid()).wait(timeout=10)
except psutil.TimeoutExpired:
    pass

We can measure the CPU context switching using /usr/bin/time -v. Before the patch (the busy-loop):

$ /usr/bin/time -v python3 test.py 2>&1 | grep context
    Voluntary context switches: 258
    Involuntary context switches: 4

After the patch (the event-driven approach):

$ /usr/bin/time -v python3 test.py 2>&1 | grep context
    Voluntary context switches: 2
    Involuntary context switches: 1

This shows that instead of spinning in userspace, the process blocks in poll() / kqueue(), and is woken up only when the kernel notifies it, resulting in just a few CPU context switches.

Sleeping state

It's also interesting to note that waiting via poll() (or kqueue()) puts the process into the exact same sleeping state as a plain time.sleep() call. From the kernel's perspective, both are interruptible sleeps: the process is de-scheduled, consumes zero CPU, and sits quietly in kernel space.

The "S+" state shown below by ps means that the process "sleeps in foreground".

$ (python3 -c 'import time; time.sleep(10)' & pid=$!; sleep 0.3; ps -o pid,stat,comm -p $pid) && fg &>/dev/null
    PID STAT COMMAND
 491573 S+   python3
$ (python3 -c 'import os,select; fd = os.pidfd_open(os.getpid(),0); p = select.poll(); p.register(fd,select.POLLIN); p.poll(10_000)' & pid=$!; sleep 0.3; ps -o pid,stat,comm -p $pid) && fg &>/dev/null
    PID STAT COMMAND
 491748 S+   python3

CPython contribution

After landing the psutil implementation (psutil/PR-2706), I took the extra step and submitted a matching pull request for CPython subprocess module: cpython/PR-144047.

I'm especially proud of this one: this is the second time in psutil's 17+ year history that a feature developed in psutil made its way upstream into the Python standard library. The first was back in 2011, when psutil.disk_usage() inspired shutil.disk_usage() (see python-ideas ML proposal).

Funny thing: 15 years ago, Python 3.3 added the timeout parameter to subprocess.Popen.wait() (see commit). That's probably where I took inspiration when I first added the timeout parameter to psutil's Process.wait() around the same time (see commit). Now, 15 years later, I'm contributing back a similar improvement for that very same timeout parameter. The circle is complete.

Links

Topics related to this:

Discussion

January 27, 2026 11:00 PM UTC


PyCoder’s Weekly

Issue #719: Django Tasks, Dictionaries, Ollama, and More (Jan. 27, 2026)

#719 – JANUARY 27, 2026
View in Browser »

The PyCoder’s Weekly Logo


Migrating From Celery to Django Tasks

Django 6 introduced the new tasks framework, a general interface for asynchronous tasks. This article shows you how to go from Celery specific code to the new general purpose mechanism.
PAUL TRAYLOR

The Hidden Cost of Python Dictionaries

Learn why Python dicts cause silent bugs and how NamedTuple, dataclass, and Pydantic catch errors earlier with better error messages.
CODECUT.AI • Shared by Khuyen Tran

Python Errors? Fix ‘em Fast for FREE with Honeybadger

alt

If you support web apps in production, you need intelligent logging with error alerts and de-duping. Honeybadger filters out the noise and transforms Python logs into contextual issues so you can find and fix errors fast. Get your FREE account →
HONEYBADGER sponsor

How to Integrate Local LLMs With Ollama and Python

Learn how to integrate your Python projects with local models (LLMs) using Ollama for enhanced privacy and cost efficiency.
REAL PYTHON

Quiz: How to Integrate Local LLMs With Ollama and Python

REAL PYTHON

Djangonaut Space: Session 6 Accepting Applications

DJANGO SOFTWARE FOUNDATION

Announcing PSF Fellow Members for Q4 2025

PYTHON SOFTWARE FOUNDATION

PEP 686: Make UTF-8 Mode Default (Final)

PYTHON.ORG

pandas 3.0.0 Released

GITHUB.COM/PANDAS-DEV

Articles & Tutorials

Nothing to Declare: From NaN to None via null

Explore the key differences between NaN, null, and None in numerical data handling using Python. While all signal “no meaningful value,” they behave differently. Learn about the difference and how to correctly handle the data using Pydantic models and JSON serialization.
FMULARCZYK.PL • Shared by Filip Mularczyk

Continuing to Improve the Learning Experience at Real Python

If you haven’t visited the Real Python website lately, then it’s time to check out a great batch of updates on realpython.com! Dan Bader returns to the show this week to discuss improvements to the site and more ways to learn Python.
REAL PYTHON podcast

The Ultimate Guide to Docker Build Cache

alt

Docker builds feel slow because cache invalidation is working against you. Depot explains how BuildKit’s layer caching works, when to use bind mounts vs cache mounts, and how to optimize your Dockerfile so Gradle dependencies don’t rebuild on every code change →
DEPOT sponsor

The State of WebAssembly: 2025 and 2026

A comprehensive look at WebAssembly in 2025 and 2026, covering browser support, Safari updates, WebAssembly 3.0, WASI, .NET, Kotlin, debugging improvements, and growing adoption across edge computing and embedded devices.
GERARD GALLANT

Asyncio Is Neither Fast Nor Slow

There are many misconceptions on asyncio, as such there are many misleading benchmarks out there. This article looks at how to analyse a benchmark result and to come up with more relevant conclusions.
CHANGS.CO.UK • Shared by Jamie Chang

Expertise Is the Art of Ignoring

Kevin says that trying to “master” a programming language is a trap. Real expertise comes from learning what you need, when you need it, and ignoring the rest on purpose.
KEVIN RENSKERS

uv vs pip: Python Packaging and Dependency Management

Choosing between uv vs pip? This video course compares speed, reproducible environments, compatibility, and dependency management to help you pick the right tool.
REAL PYTHON course

Ee Durbin Departing the Python Software Foundation

Ee Durbin is a long time contributor to Python and was heavily involved in the community even before becoming a staff member at the PSF. Ee is moving on though.
PYTHON SOFTWARE FOUNDATION

Self-Concatenation

Strings and other sequences can be multiplied by numbers to self-concatenate them. You need to be careful with mutable sequences though.
TREY HUNNER

Python, Is It Being Killed by Incremental Improvements?

This opinion piece asks whether Python’s recent focus on concurrency is a misstep and whether efforts should be focused elsewhere.
STEFAN-MARR.DE

How to Parametrize Exception Testing in PyTest?

A quick TIL-style article on how to provide different input data and test different exceptions being raised in pytest.
BORUTZKI

Projects & Code

redress: A Retry Library That Classifies Errors

GITHUB.COM/APONYSUS • Shared by Joshua Sorrell

django-nis2-shield: NIS2 Compliance Middleware

GITHUB.COM/NIS2SHIELD • Shared by Fabrizio Di Priamo

pfst: AST Manipulation That Preserves Formatting

GITHUB.COM/TOM-PYTEL • Shared by Tomasz Pytel

jupyterlab-git: A Git Extension for JupyterLab

GITHUB.COM/JUPYTERLAB

steady-queue: A DB-backed Task Backend for Django

GITHUB.COM/KNIFECAKE

Events

Weekly Real Python Office Hours Q&A (Virtual)

January 28, 2026
REALPYTHON.COM

Python Devroom @ FOSDEM 2026

January 31 to February 1, 2026
FOSDEM.ORG

Melbourne Python Users Group, Australia

February 2, 2026
J.MP

PyBodensee Monthly Meetup

February 2, 2026
PYBODENSEE.COM

STL Python

February 5, 2026
MEETUP.COM


Happy Pythoning!
This was PyCoder’s Weekly Issue #719.
View in Browser »

alt

[ 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 ]

January 27, 2026 07:30 PM UTC


death and gravity

DynamoDB crash course: part 1 – philosophy

This is part one of a series covering core DynamoDB concepts and patterns, from the data model and features all the way up to single-table design.

The goal is to get you to understand what idiomatic usage looks like and what the trade-offs are in under an hour, providing entry points to detailed documentation.

(Don't get me wrong, the AWS documentation is comprehensive, but can be quite complex, and DynamoDB being a relatively low level product with lots of features added over the years doesn't really help with that.)

Today, we're looking at what DynamoDB is and why it is the way it is.

What is DynamoDB? #

Quoting Wikipedia:

Amazon DynamoDB is a managed NoSQL database service provided by AWS. It supports key-value and document data structures and is designed to handle a wide range of applications requiring scalability and performance.

See also

This definition should suffice for now; for a more detailed refresher, see:

The DynamoDB data model can be summarized as follows:

A table is a collection of items, and an item is a collection of attributes. Items are uniquely identified by two attributes, the partition key and the sort key. The partition key determines where (i.e. on what computer) an item is stored. The sort key is used to get ordered ranges of items from a specific partition.

That's is, that's the whole data model. Sure, there's indexes and transactions and other features, but at its core, this is it. Put another way:

A DynamoDB table is a hash table of B-trees1 – partition keys are hash table keys, and sort keys are B-tree keys. Because of this, any access not based on partition and sort key is expensive, since you end up doing a full table scan.

If you were to implement this model in Python, it'd look something like this:

from collections import defaultdict
from sortedcontainers import SortedDict

class Table:

    def __init__(self, pk_name, sk_name):
        self._pk_name = pk_name
        self._sk_name = sk_name
        self._partitions = defaultdict(SortedDict)

    def put_item(self, item):
        pk, sk = item[self._pk_name], item[self._sk_name]
        old_item = self._partitions[pk].setdefault(sk, {})
        old_item.clear()
        old_item.update(item)

    def get_item(self, pk, sk):
        return dict(self._partitions[pk][sk])

    def query(self, pk, minimum=None, maximum=None, inclusive=(True, True), reverse=False):
        # in the real DynamoDB, this operation is paginated
        partition = self._partitions[pk]
        for sk in partition.irange(minimum, maximum, inclusive, reverse):
            yield dict(partition[sk])

    def scan(self):
        # in the real DynamoDB, this operation is paginated
        for partition in self._partitions.values():
            for item in partition.values():
                yield dict(item)

    def update_item(self, item):
        pk, sk = item[self._pk_name], item[self._sk_name]
        old_item = self._partitions[pk].setdefault(sk, {})
        old_item.update(item)

    def delete_item(self, pk, sk):
        del self._partitions[pk][sk]
>>> table = Table('Artist', 'Song')
>>>
>>> table.put_item({'Artist': '1000mods', 'Song': 'Vidage', 'Year': 2011})
>>> table.put_item({'Artist': '1000mods', 'Song': 'Claws', 'Album': 'Vultures'})
>>> table.put_item({'Artist': 'Kyuss', 'Song': 'Space Cadet'})
>>>
>>> table.get_item('1000mods', 'Claws')
{'Artist': '1000mods', 'Song': 'Claws', 'Album': 'Vultures'}
>>> [i['Song'] for i in table.query('1000mods')]
['Claws', 'Vidage']
>>> [i['Song'] for i in table.query('1000mods', minimum='Loose')]
['Vidage']

Philosophy #

One can't help but feel this kind of simplicity would be severely limiting.

A consequence of DynamoDB being this low level is that, unlike with most relational databases, query planning and sometimes index management happen at the application level, i.e. you have to do them yourself in code. In turn, this means you need to have a clear, upfront understanding of your application's access patterns, and accept that changes in access patterns will require changes to the application.

In return, you get a fully managed, highly-available database that scales infinitely:2 there are no servers to take care of, there's almost no downtime, and there are no limits on table size or the number of items in a table; where limits do exist, they are clearly documented, allowing for predictable performance.

This highlights an intentional design decision that is essentially DynamoDB's main proposition to you as its user: data modeling complexity is always preferable to complexity coming from infrastructure maintenance, availability, and scalability (what AWS marketing calls "undifferentiated heavy lifting").

To help manage this complexity, a number of design patterns have arisen, covered extensively by the official documentation, and which we'll discuss in a future article. Even so, the toll can be heavy – by AWS's own admission, the prime disadvantage of single table design, the fundamental design pattern, is that:

[the] learning curve can be steep due to paradoxical design compared to relational databases

As this walkthrough puts it:

a well-optimized single-table DynamoDB layout looks more like machine code than a simple spreadsheet

...which, admittedly, sounds pretty cool, but also why would I want that? After all, most useful programming most people do is one or two abstraction levels above assembly, itself one over machine code.

See also

A bit of history #

Perhaps it's worth having a look at where DynamoDB comes from.

Amazon.com used Oracle databases for a long time. To cope with the increasing scale, they first adopted a database-per-service model, and then sharding, with all the architectural and operational overhead you would expect. At its 2017 peak (five years after DynamoDB was released in AWS, and over ten years after some version of it was available internally), they still had 75 PB of data in nearly 7500 Oracle databases, owned by 100+ teams, with thousands of applications, for OLTP workloads alone. That sounds pretty traumatic – it was definitely bad enough to allegedly ban OLTP relational databases internally, and require that teams get VP approval to use one.

Yeah, coming from that, it's hard to argue DynamoDB adds complexity.

That is not to say relational databases cannot be as scalable as DynamoDB, just that Amazon doesn't belive in them – distributed SQL databases like Google's Spanner and CockroachDB have existed for a while now, and even AWS seems to be warming up to the idea.

This might also explain why the design patterns are so slow to make their way into SDKs, or even better, into DynamoDB itself; when you have so many applications and so many experienced teams, the cost of yet another bit of code to do partition key sharding just isn't that great.

See also


Anyway, that's it for now.

In the next article, we'll have a closer look at the DynamoDB data model and features.

Learned something new today? Share it with others, it really helps!

Want to know when new articles come out? Subscribe here to get new stuff straight to your inbox!

  1. Or any other sorted data structure that allows fast searches, sequential access, insertions, and deletions. [return]

  2. As the saying goes, the cloud is just someone else's computers. Here, "infinitely" means it scales horizontally, and you'll run out of money before AWS runs out of computers. [return]

January 27, 2026 05:00 PM UTC


Real Python

Create Callable Instances With Python's .__call__()

In Python, a callable is any object that you can call using a pair of parentheses and, optionally, a series of arguments. Functions, classes, and methods are all common examples of callables in Python. Besides these, you can also create custom classes that produce callable instances. To do this, you can add the .__call__() special method to your class.

Instances of a class with a .__call__() method behave like functions, providing a flexible and handy way to add functionality to your objects. Understanding how to create and use callable instances is a valuable skill for any Python developer.

In this video course, you’ll:


[ 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 ]

January 27, 2026 02:00 PM UTC


PyBites

The missing 66% of your skillset

Bob and I have spent many years as Python devs, and 6 years coaching with Pybites and we can safely say that being a Senior Developer is only about 1/3 Python knowledge.

The other 60% is the ecosystem. It’s the tooling. It’s all of the tech around Python that makes you stand out from the rest.

This is the biggest blind spot keeping developers stuck in Tutorial Hell. You spend hours memorising obscure library features, but you crumble when asked to configure a CI/CD pipeline. (That’s not just made up by the way – many of you in dev roles will have seen this with colleagues at some point or another!)

These are the elements of the Python ecosystem you should absolutely be building experience with if you want to move from being a scripter to an engineer:

It looks like a lot. It is a lot. But this is the difference between a hobbyist and a professional.

Does this make you feel overwhelmed? Or does it give you a roadmap of what to do this year?

I’m curious! Feel free to hit me up in the Community with your thoughts.

And yes, these are all things we coach people on in PDM. Use the link below to have a chat.

Julian

This note was originally sent to our email list. Join here: https://pybit.es/newsletter

January 27, 2026 11:50 AM UTC


HoloViz

A Major Step Toward Structured, Auditable AI-Driven Data Apps: Lumen AI 1.0

January 27, 2026 12:00 AM UTC


Seth Michael Larson

Use “\A...\z”, not “^...$” with Python regular expressions

Two years ago I discovered a potential foot-gun with the Python standard library “re” module. I blogged about this behavior, and turns out that I wasn't only one who didn't know this: The article was #1 on HackerNews and the most-read article on my blog in 2024. In short the unexpected behavior is that the pattern “^Hello$” matches both “Hello” and “Hello\n”, and sometimes you don't intend to match a trailing newline.

This article serves as a follow-up! Back in 2024 I created a table showing that \z was a partially viable alternative to $ for matching end-of-string without matching a trailing newline... for every regular expression implementation EXCEPT Python and EMCAScript.

But that is no longer true, Python 3.14 now supports \z! This means \z is one step closer to being the universal recommendation to match the end of string without matching a newline. Obviously no one is upgrading their Python version just for this new feature, but it's good to know that the gap is being closed. Thanks to David Wheeler for doing deeper research in the OpenSSF Best Practices WG and publishing this report.

Until Python 3.13 is deprecated and long gone: using \Z (as an alias for \z) works fine for Python regular expressions. Just note that this behavior isn't the same across regular expression implementations, for example EMCAScript, Golang, and Rust don't support \Z and for PHP, Java, and .NET \Z actually matches trailing newlines!



Thanks for keeping RSS alive! ♥

January 27, 2026 12:00 AM UTC


Armin Ronacher

Colin and Earendil

Regular readers of this blog will know that I started a new company. We have put out just a tiny bit of information today, and some keen folks have discovered and reached out by email with many thoughtful responses. It has been delightful.

Colin and I met here, in Vienna. We started sharing coffees, ideas, and lunches, and soon found shared values despite coming from different backgrounds and different parts of the world. We are excited about the future, but we’re equally vigilant of it. After traveling together a bit, we decided to plunge into the cold water and start a company together. We want to be successful, but we want to do it the right way and we want to be able to demonstrate that to our kids.

Vienna is a city of great history, two million inhabitants and a fascinating vibe that is nothing like San Francisco. In fact, Vienna is in many ways the polar opposite to the Silicon Valley, both in mindset, in opportunity and approach to life. Colin comes from San Francisco, and though I’m Austrian, my career has been shaped by years working with California companies and people from there who used my Open Source software. Vienna is now our shared home. Despite Austria being so far away from California, it is a place of tinkerers and troublemakers. It’s always good to remind oneself that society consists of more than just your little bubble. It also creates the necessary counter balance to think in these times.

The world that is emerging in front of our eyes is one of change. We incorporated as a PBC with a founding charter to craft software and open protocols, strengthen human agency, bridge division and ignorance and to cultivate lasting joy and understanding. Things we believe in deeply.

I have dedicated 20 years of my life in one way or another creating Open Source software. In the same way as artificial intelligence calls into question the very nature of my profession and the way we build software, the present day circumstances are testing society. We’re not immune to these changes and we’re navigating them like everyone else, with a mixture of excitement and worry. But we share a belief that right now is the time to stand true to one’s values and principles. We want to take an earnest shot at leaving the world a better place than we found it. Rather than reject the changes that are happening, we look to nudge them towards the right direction.

If you want to follow along you can subscribe to our newsletter, written by humans not machines.

January 27, 2026 12:00 AM UTC

January 26, 2026


Real Python

GeoPandas Basics: Maps, Projections, and Spatial Joins

GeoPandas extends pandas to make working with geospatial data in Python intuitive and powerful. If you’re looking to do geospatial tasks in Python and want a library with a pandas-like API, then GeoPandas is an excellent choice. This tutorial shows you how to accomplish four common geospatial tasks: reading in data, mapping it, applying a projection, and doing a spatial join.

By the end of this tutorial, you’ll understand that:

  • GeoPandas extends pandas with support for spatial data. This data typically lives in a geometry column and allows spatial operations such as projections and spatial joins, while Folium focuses on richer interactive web maps after data preparation.
  • You inspect CRS with .crs and reproject data using .to_crs() with an authority code like EPSG:4326 or ESRI:54009.
  • A geographic CRS stores longitude and latitude in degrees, while a projected CRS uses linear units like meters or feet for area and distance calculations.
  • Spatial joins use .sjoin() with predicates like "within" or "intersects", and both inputs must share the same CRS or the relationships will be computed incorrectly.

Here’s how GeoPandas compares with alternative libraries:

Use Case Pick pandas Pick Folium Pick GeoPandas
Tabular data analysis -
Mapping -
Projections, spatial joins - -

GeoPandas builds on pandas by adding support for geospatial data and operations like projections and spatial joins. It also includes tools for creating maps. Folium complements this by focusing on interactive, web-based maps that you can customize more deeply.

Get Your Code: Click here to download the free sample code for learning how to work with GeoPandas maps, projections, and spatial joins.

Take the Quiz: Test your knowledge with our interactive “GeoPandas Basics: Maps, Projections, and Spatial Joins” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

GeoPandas Basics: Maps, Projections, and Spatial Joins

Test GeoPandas basics for reading, mapping, projecting, and spatial joins to handle geospatial data confidently.

Getting Started With GeoPandas

You’ll first prepare your environment and load a small dataset that you’ll use throughout the tutorial. In the next two subsections, you’ll install the necessary packages and read in a sample dataset of New York City borough boundaries. This gives you a concrete GeoDataFrame to explore as you learn the core concepts.

Installing GeoPandas

This tutorial uses two packages: geopandas for working with geographic data and geodatasets for loading sample data. It’s a good idea to install these packages inside a virtual environment so your project stays isolated from the rest of your system and you can manage its dependencies cleanly.

Once your virtual environment is active, you can install both packages with pip:

Shell
$ python -m pip install "geopandas[all]" geodatasets

Using the [all] option ensures you have everything needed for reading data, transforming coordinate systems, and creating plots. For most readers, this will work out of the box.

If you do run into installation issues, the project’s maintainers provide alternative installation options on the official installation page.

Reading in Data

Most geospatial datasets come in GeoJSON or shapefile format. The read_file() function can read both, and it accepts either a local file path or a URL.

In the example below, you’ll use read_file() to load the New York City Borough Boundaries (NYBB) dataset. The geodatasets package provides a convenient path to this dataset, so you don’t need to download anything manually. You’ll also drop unnecessary columns:

Python
>>> import geopandas as gpd
>>> import matplotlib.pyplot as plt
>>> from geodatasets import get_path
>>> path_to_data = get_path("nybb")
>>> nybb = gpd.read_file(path_to_data)
>>> nybb = nybb[["BoroName", "Shape_Area", "geometry"]]
>>> nybb
    BoroName        Shape_Area      geometry
0   Staten Island   1.623820e+09    MULTIPOLYGON (((970217.022 145643.332, ....
1   Queens          3.045213e+09    MULTIPOLYGON (((1029606.077 156073.814, ...
2   Brooklyn        1.937479e+09    MULTIPOLYGON (((1021176.479 151374.797, ...
3   Manhattan       6.364715e+08    MULTIPOLYGON (((981219.056 188655.316, ....
4   Bronx           1.186925e+09    MULTIPOLYGON (((1012821.806 229228.265, ...
>>> type(nybb)
<class 'geopandas.geodataframe.GeoDataFrame'>
>>> type(nybb["geometry"])
<class 'geopandas.geoseries.GeoSeries'>

nybb is a GeoDataFrame. A GeoDataFrame has rows, columns, and all the methods of a pandas DataFrame. The difference is that it typically includes a special geometry column, which stores geographic shapes instead of plain numbers or text.

The geometry column is a GeoSeries. It behaves like a normal pandas Series, but its values are spatial objects that you can map and run spatial queries against. In the nybb dataset, each borough’s geometry is a MultiPolygon—a shape made of several polygons—because every borough consists of multiple islands. Soon you’ll use these geometries to make maps and run spatial operations, such as finding which borough a point falls inside.

Mapping Data

Once you’ve loaded a GeoDataFrame, one of the quickest ways to understand your data is to visualize it. In this section, you’ll learn how to create both static and interactive maps. This allows you to inspect shapes, spot patterns, and confirm that your geometries look the way you expect.

Creating Static Maps

Read the full article at https://realpython.com/geopandas/ »


[ 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 ]

January 26, 2026 02:00 PM UTC


Kushal Das

replyfast a python module for signal

replyfast is a Python module to receive and send messages on Signal.

You can install it via

python3 -m pip install replyfast

or

uv pip install replyfast

I have to add Windows builds to CI though.

I have a script to help you to register as a device, and then you can send and receive messages.

I have a demo bot which shows both sending and rreceiving messages, and also how to schedule work following the crontab syntaxt.

    scheduler.register(
        "*/5 * * * *",
        send_disk_usage,
        args=(client,),
        name="disk-usage",
    )

This is all possible due to the presage library written in Rust.

January 26, 2026 12:16 PM UTC


Real Python

Quiz: GeoPandas Basics: Maps, Projections, and Spatial Joins

In this quiz, you’ll test your understanding of GeoPandas.

You’ll review coordinate reference systems, GeoDataFrames, interactive maps, and spatial joins with .sjoin(). You’ll also explore how projections affect maps and learn best practices for working with geospatial data.

This quiz helps you confirm that you can prepare, visualize, and analyze geospatial data accurately using GeoPandas.


[ 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 ]

January 26, 2026 12:00 PM UTC


Python Software Foundation

Your Python. Your Voice. Join the Python Developers Survey 2026!

This year marks the ninth iteration of the official Python Developers Survey. We intentionally launched the survey in January (later than years prior) so that data collection and results can be completed and shared within the same calendar year. The survey aims to capture the current state of the Python language and its surrounding ecosystem. By comparing the results with last year’s, the community can identify emerging trends and gain deeper insight into how Python continues to evolve.

We encourage you to contribute to our community’s knowledge by sharing your experience and perspective. Your participation is valued! The survey should only take you about 10-15 minutes to complete. 

Contribute to the Python Developers Survey 2026!

This year we aim to reach even more of our community and ensure accurate global representation by highlighting our localization efforts: 

If you have ideas about what else we can do to get the word out and encourage a diversity of responses, please comment on the corresponding Discuss thread

The survey is organized in partnership between the Python Software Foundation and JetBrains. After the survey is over, JetBrains will publish the aggregated results and randomly choose 20 winners (among those who complete the survey in its entirety), who will each receive a $100 Amazon Gift Card or a local equivalent.

January 26, 2026 08:36 AM UTC


Python Bytes

#467 Toads in my AI

<strong>Topics covered in this episode:</strong><br> <ul> <li><strong><a href="https://check.labs.greynoise.io?featured_on=pythonbytes">GreyNoise IP Check</a></strong></li> <li><strong><a href="https://pypi.org/project/tprof/?featured_on=pythonbytes">tprof: a targeting profiler</a></strong></li> <li><strong><a href="https://github.com/batrachianai/toad?featured_on=pythonbytes">TOAD is out</a></strong></li> <li><strong>Extras</strong></li> <li><strong>Joke</strong></li> </ul><a href='https://www.youtube.com/watch?v=24gBkjE8tOU' style='font-weight: bold;'data-umami-event="Livestream-Past" data-umami-event-episode="467">Watch on YouTube</a><br> <p><strong>About the show</strong></p> <p>Sponsored by us! Support our work through:</p> <ul> <li>Our <a href="https://training.talkpython.fm/?featured_on=pythonbytes"><strong>courses at Talk Python Training</strong></a></li> <li><a href="https://courses.pythontest.com/p/the-complete-pytest-course?featured_on=pythonbytes"><strong>The Complete pytest Course</strong></a></li> <li><a href="https://www.patreon.com/pythonbytes"><strong>Patreon Supporters</strong></a></li> </ul> <p><strong>Connect with the hosts</strong></p> <ul> <li>Michael: <a href="https://fosstodon.org/@mkennedy">@mkennedy@fosstodon.org</a> / <a href="https://bsky.app/profile/mkennedy.codes?featured_on=pythonbytes">@mkennedy.codes</a> (bsky)</li> <li>Brian: <a href="https://fosstodon.org/@brianokken">@brianokken@fosstodon.org</a> / <a href="https://bsky.app/profile/brianokken.bsky.social?featured_on=pythonbytes">@brianokken.bsky.social</a></li> <li>Show: <a href="https://fosstodon.org/@pythonbytes">@pythonbytes@fosstodon.org</a> / <a href="https://bsky.app/profile/pythonbytes.fm">@pythonbytes.fm</a> (bsky)</li> </ul> <p>Join us on YouTube at <a href="https://pythonbytes.fm/stream/live"><strong>pythonbytes.fm/live</strong></a> to be part of the audience. Usually <strong>Monday</strong> at 11am PT. Older video versions available there too.</p> <p>Finally, if you want an artisanal, hand-crafted digest of every week of the show notes in email form? Add your name and email to <a href="https://pythonbytes.fm/friends-of-the-show">our friends of the show list</a>, we'll never share it.</p> <p><strong>Michael #1:</strong> <a href="https://check.labs.greynoise.io?featured_on=pythonbytes">GreyNoise IP Check</a></p> <ul> <li>GreyNoise watches the internet's background radiation—the constant storm of scanners, bots, and probes hitting every IP address on Earth.</li> <li>Is your computer sending out bot or other bad-actor traffic? What about the myriad of devices and IoT things on your local IP?</li> <li>Heads up: If your IP has recently changed, it might not be you (false positive).</li> </ul> <p>Brian #2: <a href="https://pypi.org/project/tprof/?featured_on=pythonbytes">tprof: a targeting profiler</a></p> <ul> <li>Adam Johnson</li> <li>Intro blog post: <a href="https://adamj.eu/tech/2026/01/14/python-introducing-tprof/?featured_on=pythonbytes"><strong>Python: introducing tprof, a targeting profiler</strong></a></li> </ul> <p><strong>Michael #3: <a href="https://github.com/batrachianai/toad?featured_on=pythonbytes">TOAD is out</a></strong></p> <ul> <li>Toad is a unified experience for AI in the terminal</li> <li>Front-end for AI tools such as <a href="https://openhands.dev/?featured_on=pythonbytes">OpenHands</a>, <a href="https://www.claude.com/product/claude-code?featured_on=pythonbytes">Claude Code</a>, <a href="https://geminicli.com/?featured_on=pythonbytes">Gemini CLI</a>, and many more.</li> <li>Better TUI experience (e.g. @ for file context uses fuzzy search and dropdowns)</li> <li>Better prompt input (mouse, keyboard, even colored code and markdown blocks)</li> <li>Terminal within terminals (for TUI support)</li> </ul> <p><strong>Brian #4</strong>: <a href="https://github.com/fastapi/fastapi/pull/14706/files?featured_on=pythonbytes">FastAPI adds Contribution Guidelines around AI usage</a></p> <ul> <li>Docs commit: <a href="https://github.com/fastapi/fastapi/pull/14706/files?featured_on=pythonbytes"><strong>Add contribution instructions about LLM generated code and comments and automated tools for PRs</strong></a></li> <li>Docs section: <a href="https://fastapi.tiangolo.com/contributing/?h=contributin#automated-code-and-ai">Development - Contributing : Automated Code and AI</a></li> <li>Great inspiration and example of how to deal with this for popular open source projects <ul> <li>“If the <strong>human effort</strong> put in a PR, e.g. writing LLM prompts, is <strong>less</strong> than the <strong>effort we would need to put</strong> to <strong>review it</strong>, please <strong>don't</strong> submit the PR.”</li> </ul></li> <li>With sections on <ul> <li>Closing Automated and AI PRs</li> <li>Human Effort Denial of Service</li> <li>Use Tools Wisely</li> </ul></li> </ul> <p><strong>Extras</strong></p> <p>Brian:</p> <ul> <li><a href="https://techcrunch.com/2026/01/14/digg-launches-its-new-reddit-rival-to-the-public/?featured_on=pythonbytes">Apparently Digg is back</a> and there’s a <a href="https://digg.com/python?featured_on=pythonbytes">Python Community</a> there</li> <li><a href="https://marijkeluttekes.dev/blog/articles/2026/01/21/why-light-weight-websites-may-one-day-save-your-life/?featured_on=pythonbytes">Why light-weight websites may one day save your life</a> - Marijke LuttekesHome</li> </ul> <p>Michael:</p> <ul> <li>Blog posts about Talk Python AI Integrations <ul> <li><a href="https://talkpython.fm/blog/posts/announcing-talk-python-ai-integrations/?featured_on=pythonbytes">Announcing Talk Python AI Integrations</a> <em><em></em></em>on Talk Python’s Blog</li> <li><a href="https://mkennedy.codes/posts/why-hiding-from-ai-crawlers-is-a-bad-idea/?featured_on=pythonbytes">Blocking AI crawlers might be a bad idea</a> on Michael’s Blog</li> </ul></li> <li>Already using the compile flag for faster app startup on the containers: <ul> <li><code>RUN --mount=type=cache,target=/root/.cache uv pip install --compile-bytecode --python /venv/bin/python</code></li> <li>I think it’s speeding startup by about 1s / container.</li> </ul></li> <li><a href="https://blobs.pythonbytes.fm/big-prompt-or-what-2026-01.png">Biggest prompt yet?</a> <strong>72 pages</strong>, 11, 000</li> </ul> <p><strong>Joke: <a href="https://www.reddit.com/r/ProgrammerHumor/comments/1q2tznx/forgotthebasecase/?featured_on=pythonbytes">A date</a></strong></p> <ul> <li>via From Pat Decker</li> </ul>

January 26, 2026 08:00 AM UTC


Reuven Lerner

What’s new in Pandas 3?

🎉 Pandas 3 is out!

As of last week, saying “pip install pandas” or “uv add pandas” gives you the latest version.

What’s new? What has changed?

I’ve got a whole YouTube playlist, explaining what you need to know: https://www.youtube.com/playlist?list=PLbFHh-ZjYFwFWHVT0qeg9Jz1TBD0TlJJT

The post What’s new in Pandas 3? appeared first on Reuven Lerner.

January 26, 2026 07:05 AM UTC

January 25, 2026


Ned Batchelder

Testing: exceptions and caches

Two testing-related things I found recently.

Unified exception testing

Kacper Borucki blogged about parameterizing exception testing, and linked to pytest docs and a StackOverflow answer with similar approaches.

The common way to test exceptions is to use pytest.raises as a context manager, and have separate tests for the cases that succeed and those that fail. Instead, this approach lets you unify them.

I tweaked it to this, which I think reads nicely:

from contextlib import nullcontext as produces


import pytest
from pytest import raises

@pytest.mark.parametrize(
    "example_input, result",
    [
        (3, produces(2)),
        (2, produces(3)),
        (1, produces(6)),
        (0, raises(ZeroDivisionError)),
        ("Hello", raises(TypeError)),
    ],
)
def test_division(example_input, result):
    with result as e:
        assert (6 / example_input) == e

One parameterized test that covers both good and bad outcomes. Nice.

AntiLRU

The @functools.lru_cache decorator (and its convenience cousin @cache) are good ways to save the result of a function so that you don’t have to compute it repeatedly. But, they hide an implicit global in your program: the dictionary of cached results.

This can interfere with testing. Your tests should all be isolated from each other. You don’t want a side effect of one test to affect the outcome of another test. The hidden global dictionary will do just that. The first test calls the cached function, then the second test gets the cached value, not a newly computed one.

Ideally, lru_cache would only be used on pure functions: the result only depends on the arguments. If it’s only used for pure functions, then you don’t need to worry about interactions between tests because the answer will be the same for the second test anyway.

But lru_cache is used on functions that pull information from the environment, perhaps from a network API call. The tests might mock out the API to check the behavior under different API circumstances. Here’s where the interference is a real problem.

The lru_cache decorator makes a .clear_cache method available on each decorated function. I had some code that explicitly called that method on the cached functions. But then I added a new cached function, forgot to update the conftest.py code that cleared the caches, and my tests were failing.

A more convenient approach is provided by pytest-antilru: it’s a pytest plugin that monkeypatches @lru_cache to track all of the cached functions, and clears them all between tests. The caches are still in effect during each test, but can’t interfere between them.

It works great. I was able to get rid of all of the manually maintained cache clearing in my conftest.py.

January 25, 2026 08:32 PM UTC

January 23, 2026


Talk Python to Me

#535: PyView: Real-time Python Web Apps

Building on the web is like working with the perfect clay. It’s malleable and can become almost anything. But too often, frameworks try to hide the web’s best parts away from us. Today, we’re looking at PyView, a project that brings the real-time power of Phoenix LiveView directly into the Python world. I'm joined by Larry Ogrodnek to dive into PyView.<br/> <br/> <strong>Episode sponsors</strong><br/> <br/> <a href='https://talkpython.fm/training'>Talk Python Courses</a><br> <a href='https://talkpython.fm/devopsbook'>Python in Production</a><br/> <br/> <h2 class="links-heading mb-4">Links from the show</h2> <div><strong>Guest</strong><br/> <strong>Larry Ogrodnek</strong>: <a href="https://hachyderm.io/@ogrodnek?featured_on=talkpython" target="_blank" >hachyderm.io</a><br/> <br/> <strong>pyview.rocks</strong>: <a href="https://pyview.rocks?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>Phoenix LiveView</strong>: <a href="https://github.com/phoenixframework/phoenix_live_view?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>this section</strong>: <a href="https://pyview.rocks/getting-started/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>Core Concepts</strong>: <a href="https://pyview.rocks/core-concepts/liveview-lifecycle/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>Socket and Context</strong>: <a href="https://pyview.rocks/core-concepts/socket-and-context/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>Event Handling</strong>: <a href="https://pyview.rocks/core-concepts/event-handling/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>LiveComponents</strong>: <a href="https://pyview.rocks/core-concepts/live-components/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>Routing</strong>: <a href="https://pyview.rocks/core-concepts/routing/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>Templating</strong>: <a href="https://pyview.rocks/templating/overview/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>HTML Templates</strong>: <a href="https://pyview.rocks/templating/html-templates/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>T-String Templates</strong>: <a href="https://pyview.rocks/templating/t-string-templates/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>File Uploads</strong>: <a href="https://pyview.rocks/features/file-uploads/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>Streams</strong>: <a href="https://pyview.rocks/streams-usage/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>Sessions &amp; Authentication</strong>: <a href="https://pyview.rocks/features/authentication/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>Single-File Apps</strong>: <a href="https://pyview.rocks/single-file-apps/?featured_on=talkpython" target="_blank" >pyview.rocks</a><br/> <strong>starlette</strong>: <a href="https://starlette.dev?featured_on=talkpython" target="_blank" >starlette.dev</a><br/> <strong>wsproto</strong>: <a href="https://github.com/python-hyper/wsproto?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>apscheduler</strong>: <a href="https://github.com/agronholm/apscheduler?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>t-dom project</strong>: <a href="https://github.com/t-strings/tdom?featured_on=talkpython" target="_blank" >github.com</a><br/> <br/> <strong>Watch this episode on YouTube</strong>: <a href="https://www.youtube.com/watch?v=g0RDxN71azs" target="_blank" >youtube.com</a><br/> <strong>Episode #535 deep-dive</strong>: <a href="https://talkpython.fm/episodes/show/535/pyview-real-time-python-web-apps#takeaways-anchor" target="_blank" >talkpython.fm/535</a><br/> <strong>Episode transcripts</strong>: <a href="https://talkpython.fm/episodes/transcript/535/pyview-real-time-python-web-apps" 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>

January 23, 2026 07:29 PM UTC


Real Python

The Real Python Podcast – Episode #281: Continuing to Improve the Learning Experience at Real Python

If you haven't visited the Real Python website lately, then it's time to check out a great batch of updates on realpython.com! Dan Bader returns to the show this week to discuss improvements to the site and more ways to learn Python.


[ 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 ]

January 23, 2026 12:00 PM UTC