skip to navigation
skip to content

Planet Python

Last update: November 20, 2025 10:43 AM UTC

November 19, 2025


Django Weblog

Twenty years of Django releases

On November 16th 2005, Django co-creator Adrian Holovaty announced the first ever Django release, Django 0.90. Twenty years later, today here we are shipping the first release candidate of Django 6.0 🚀.

Since we’re celebrating Django’s 20th birthday this year, here are a few release-related numbers that represent Django’s history:

This is what decades’ worth of a stable framework looks like. Expect more gradual improvements and bug fixes over the next twenty years’ worth of releases. And if you like this kind of data, check out the State of Django 2025 report by JetBrains, with lots of statistics on our ecosystem (and there’s a few hours left on their Get PyCharm Pro with 30 % Off & Support Django offer).


Support Django

If you or your employer counts on Django’s 20 years of stability, consider whether you can support the project via donations to our non-profit Django Software Foundation.

Once you’ve done it, post with #DjangoBirthday and tag us on Mastodon / on Bluesky / on X / on LinkedIn so we can say thank you!

59%

Of our US $300,000.00 goal for 2025, as of November 19th, 2025, we are at:

  • 58.7% funded
  • $176,098.60 donated

Donate to support Django

November 19, 2025 03:27 PM UTC


Real Python

Build a Python MCP Client to Test Servers From Your Terminal

Follow this Python project to build an MCP client that discovers MCP server capabilities and feeds an AI-powered chat with tool calls.

November 19, 2025 02:00 PM UTC


PyCharm

At JetBrains, we love seeing the developer community grow and thrive. That’s why we support open-source projects that make a real difference — the ones that help developers learn, build, and create better software together. We’re proud to back open-source maintainers with free licenses and to contribute to initiatives that strengthen the ecosystem and the […]

November 19, 2025 01:40 PM UTC


Django Weblog

Django 6.0 release candidate 1 released

Django 6.0 release candidate 1 is now available. It represents the final opportunity for you to try out a mosaic of modern tools and thoughtful design before Django 6.0 is released.

The release candidate stage marks the string freeze and the call for translators to submit translations. Provided no major bugs are discovered that can't be solved in the next two weeks, Django 6.0 will be released on or around December 3. Any delays will be communicated on the on the Django forum.

Please use this opportunity to help find and fix bugs (which should be reported to the issue tracker), you can grab a copy of the release candidate package from our downloads page or on PyPI.

The PGP key ID used for this release is Natalia Bidart: 2EE82A8D9470983E

November 19, 2025 12:00 PM UTC


Real Python

Quiz: Build a Python MCP Client to Test Servers From Your Terminal

Learn how to create a Python MCP client, start an AI-powered chat session, and run it from the command line. Check your understanding.

November 19, 2025 12:00 PM UTC


Django Weblog

Going build-free with native JavaScript modules

For the last decade and more, we've been bundling CSS and JavaScript files. These build tools allowed us to utilize new browser capabilities in CSS and JS while still supporting older browsers. They also helped with client-side network performance, minimizing the content to be as small as possible and combining files into one large bundle to reduce network handshakes. We've gone through a lot of build tools iterations in the process; from Grunt (2012) to Gulp (2013) to Webpack (2014) to Parcel (2017) to esbuild (2020) and Vite (2020).

And with modern browser technologies there is less need for these build tools.

These build processes are complex, particularly for beginners to Django. The tools and associated best practices move quickly. There is a lot to learn and you need to understand how to utilize them with your Django project. You can build a workflow that stores the build results in your static folder, but there is no core Django support for a build pipeline, so this largely requires selecting from a number of third party packages and integrating them into your project.

The benefit this complexity adds is no longer as clear cut, especially for beginners. There are still advantages to build tools, but you can can create professional results without having to use or learn any build processes.

Build-free JavaScript tutorial

To demonstrate modern capabilities, let's expand Django’s polls tutorial with some newer JavaScript. We’ll use modern JS modules and we won’t require a build system.

To give us a reason to need JS let's add a new requirement to the polls; to allow our users to add their own suggestions, instead of only being able to vote on the existing options. We update our form to have a new option under the selection code:

or add your own <input type="text" name="choice_text" maxlength="200" />

Now our users can add their own options to polls if the existing ones don't fit. We can update the voting view to handle this new option. We add a new choice_text input, and if there is no vote selection we will potentially handle adding the new option, while still providing an error message if neither is supplied. We also provide an error if both are selected.

def vote(request, question_id):
    if request.POST['choice'] and request.POST['choice_text']:
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You can't vote and provide a new option.",
        })

    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        if request.POST['choice_text']:
            selected_choice = Choice.objects.create(
                question=question,
                choice_text=request.POST['choice_text'],
            )
        else:
            return render(request, 'polls/detail.html', {
                'question': question,
                'error_message': "You didn't select a choice or provide a new one.",
            })
    selected_choice.votes += 1
    selected_choice.save()
    return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

Now that our logic is a bit more complex it would be nicer if we had some JavaScript to do this. We can build a script that handles some of the form validation for us.

function noChoices(choices, choice_text) {
  return (
    Array.from(choices).some((radio) => radio.checked) ||
    (choice_text[0] && choice_text[0].value.trim() !== "")
  );
}

function allChoices(choices, choice_text) {
  return (
    !Array.from(choices).some((radio) => radio.checked) &&
    choice_text[0] &&
    choice_text[0].value.trim() !== ""
  );
}

export default function initFormValidation() {
  document.getElementById("polls").addEventListener("submit", function (e) {
    const choices = this.querySelectorAll('input[name="choice"]');
    const choice_text = this.querySelectorAll('input[name="choice_text"]');

    if (!noChoices(choices, choice_text)) {
      e.preventDefault();
      alert("You didn't select a choice or provide a new one.");
    }
    if (!allChoices(choices, choice_text)) {
      e.preventDefault();
      alert("You can't select a choice and also provide a new option.");
    }
  });
}

Note how we use export default in the above code. This means form_validation.js is a JavaScript module. When we create our main.js file, we can import it with the import statement:

import initFormValidation from "./form_validation.js";

initFormValidation();

Lastly, we add the script to the bottom of our details.html file, using Django’s usual static template tag. Note the type="module" this is needed to tell the browser we will be using import/export statements.

<script type="module" src="{% static 'polls/js/main.js' %}"></script>

That’s it! We got the modularity benefits of modern JavaScript without needing any build process. The browser handles the module loading for us. And thanks to parallel requests since HTTP/2, this can scale to many modules without a performance hit.

In production

To deploy, all we need is Django's support for collecting static files into one place and its support for adding hashes to filenames. In production it is a good idea to use ManifestStaticFilesStorage storage backend. It stores the file names it handles by appending the MD5 hash of the file’s content to the filename. This allows you to set far future cache expiries, which is good for performance, while still guaranteeing new versions of the file will make it to users’ browsers.

This backend is also able to update the reference to form_validation.js in the import statement, with its new versioned file name.

Future work

ManifestStaticFilesStorage works, but a lot of its implementation details get in the way. It could be easier to use as a developer.

We discussed those possible improvements at the Django on the Med đŸ–ïž sprints and I’m hopeful we can make progress.

I built django-manifeststaticfiles-enhanced to attempt to fix all these. The core work is to switch to a lexer for CSS and JS, based on Ned Batchelder’s JsLex that was used in Django previously. It was expanded to cover modern JS and CSS by working with Claude Code to do the grunt work of covering the syntax.

It also switches to using a topological sort to find dependencies, whereas before we used a more brute force approach of repeated processing until we saw no more changes, which lead to more work, particularly on storages that used the network. It also meant we couldn't handle circular dependencies.

To validate it works, I ran a performance benchmark on 50+ projects, it’s been tested issues and with similar (often improved) performance. On average, it’s about 30% faster.


While those improvements would be welcome, do go ahead with trying build-free JavaScript and CSS in your Django projects today! Modern browsers make it possible to create great frontend experiences without the complexity.

November 19, 2025 08:13 AM UTC


Python GUIs

Getting Started With DearPyGui for GUI Development — Your First Steps With the DearPyGui Library for Desktop Python GUIs

Getting started with a new GUI framework can feel daunting. This guide walks you through the essentials of DearPyGui. From installation and first app to widgets, layouts, theming, and advanced tooling.

November 19, 2025 08:00 AM UTC

November 18, 2025


The Python Coding Stack

I Don’t Like Magic ‱ Exploring The Class Attributes That Aren’t Really Class Attributes ‱ [Club]

This syntax, used for data classes and typing.NamedTuple, confused me when first learning about these topics. Here’s why, and why it’s no longer confusing.

November 18, 2025 10:01 PM UTC


PyCoder’s Weekly

Issue #709: deepcopy(), JIT, REPL Tricks, and More (Nov. 18, 2025)

November 18, 2025 07:30 PM UTC


Real Python

Break Out of Loops With Python's break Keyword

Learn how Python’s break lets you exit for and while loops early, with practical demos from simple games to everyday data tasks.

November 18, 2025 02:00 PM UTC


Mike Driscoll

Black Friday Python Deals Came Early

Black Friday deals came early this year. You can get 50% off of any of my Python books or courses until the end of November. You can use this coupon code at checkout: BLACKISBACK  The following links already have the discount applied: Python eBooks Python 101 Python 201: Intermediate Python The Python Quiz Book Automating […]

The post Black Friday Python Deals Came Early appeared first on Mouse Vs Python.

November 18, 2025 01:41 PM UTC


PyCharm

Open Source in Focus: Projects We’re Proud to Support

November 18, 2025 12:07 PM UTC


PyCon

Join us in “Trailblazing Python Security” at PyCon US 2026

November 18, 2025 11:17 AM UTC


Seth Michael Larson

BrotliCFFI has two new maintainers

November 18, 2025 12:00 AM UTC

November 17, 2025


Rodrigo GirĂŁo SerrĂŁo

Floodfill algorithm in Python

Learn how to implement and use the floodfill algorithm in Python.

What is the floodfill algorithm?

Click the image below to randomly colour the region you click.

Go ahead, try it!

IMG_WIDTH = 160 IMG_HEIGHT = 160 PIXEL_SIZE = 2 import asyncio import collections import random from pyscript import display from pyodide.ffi import create_proxy import js from js import fetch canvas = js.document.getElementById("bitmap") ctx = canvas.getContext("2d") URL = "/blog/floodfill-algorithm-in-python/_python.txt" async def load_bitmap(url: str) -> list[list[int]]: # Fetch the text file from the URL response = await fetch(url) text = await response.text() bitmap: list[list[int]] = [] for line in text.splitlines(): line = line.strip() if not line: continue row = [int(ch) for ch in line if ch in "01"] if row: bitmap.append(row) return bitmap def draw_bitmap(bitmap): rows = len(bitmap) cols = len(bitmap[0]) if rows > 0 else 0 if rows == 0 or cols == 0: return for y, row in enumerate(bitmap): for x, value in enumerate(row): if value == 1: ctx.fillStyle = "black" else: ctx.fillStyle = "white" ctx.fillRect(x * PIXEL_SIZE, y * PIXEL_SIZE, PIXEL_SIZE, PIXEL_SIZE) _neighbours = [(1, 0), (-1, 0), (0, 1), (0, -1)] async def fill_bitmap(bitmap, x, y): if bitmap[y][x] == 1: return ctx = canvas.getContext("2d") r, g, b = (random.randint(0, 255) for _ in range(3)) ctx.fillStyle = f"rgb({r}, {g}, {b})" def draw_pixel(x, y): ctx.fillRect(x * PIXEL_SIZE, y * PIXEL_SIZE, PIXEL_SIZE, PIXEL_SIZE) pixels = collections.deque([(x, y)]) seen = set((x, y)) while pixels: nx, ny = pixels.pop() draw_pixel(nx, ny) for dx, dy in _neighbours: x_, y_ = nx + dx, ny + dy if x_ < 0 or x_ >= IMG_WIDTH or y_ < 0 or y_ >= IMG_HEIGHT or (x_, y_) in seen: continue if bitmap[y_][x_] == 0: seen.add((x_, y_)) pixels.appendleft((x_, y_)) await asyncio.sleep(0.0001) is_running = False def get_event_coords(event): """Return (clientX, clientY) for mouse/pointer/touch events.""" # PointerEvent / MouseEvent: clientX/clientY directly available if hasattr(event, "clientX") and hasattr(event, "clientY") and event.clientX is not None: return event.clientX, event.clientY # TouchEvent: use the first touch point if hasattr(event, "touches") and event.touches.length > 0: touch = event.touches.item(0) return touch.clientX, touch.clientY # Fallback: try changedTouches if hasattr(event, "changedTouches") and event.changedTouches.length > 0: touch = event.changedTouches.item(0) return touch.clientX, touch.clientY return None, None async def on_canvas_press(event): global is_running if is_running: return is_running = True try: # Avoid scrolling / zooming taking over on touch if hasattr(event, "preventDefault"): event.preventDefault() clientX, clientY = get_event_coords(event) if clientX is None: # Could not read coordinates; bail out gracefully return rect = canvas.getBoundingClientRect() # Account for CSS scaling: map from displayed size to canvas units scale_x = canvas.width / rect.width scale_y = canvas.height / rect.height x_canvas = (clientX - rect.left) * scale_x y_canvas = (clientY - rect.top) * scale_y x_idx = int(x_canvas // PIXEL_SIZE) y_idx...

November 17, 2025 03:49 PM UTC


Real Python

How to Serve a Website With FastAPI Using HTML and Jinja2

Use FastAPI to render Jinja2 templates and serve dynamic sites with HTML, CSS, and JavaScript, then add a color picker that copies hex codes.

November 17, 2025 02:00 PM UTC

Quiz: How to Serve a Website With FastAPI Using HTML and Jinja2

Review how to build dynamic websites with FastAPI and Jinja2, and serve HTML, CSS, and JS with HTMLResponse and StaticFiles.

November 17, 2025 12:00 PM UTC


Python Bytes

#458 I will install Linux on your computer

Topics include , aiosqlitepool, deptry, and browsr.

November 17, 2025 08:00 AM UTC

November 16, 2025


Ned Batchelder

Why your mock breaks later

In Why your mock doesn’t work I explained this rule of mocking:

Mock where the object is used, not where it’s defined.

That blog post explained why that rule was important: often a mock doesn’t work at all if you do it wrong. But in some cases, the mock will work even if you don’t follow this rule, and then it can break much later. Why?

Let’s say you have code like this:

# user.py

def get_user_settings():
    with open(Path("~/settings.json").expanduser()) as f:
        return json.load(f)

def add_two_settings():
    settings = get_user_settings()
    return settings["opt1"] + settings["opt2"]

You write a simple test:

def test_add_two_settings():
    # NOTE: need to create ~/settings.json for this to work:
    #   {"opt1": 10, "opt2": 7}
    assert add_two_settings() == 17

As the comment in the test points out, the test will only pass if you create the correct settings.json file in your home directory. This is bad: you don’t want to require finicky environments for your tests to pass.

The thing we want to avoid is opening a real file, so it’s a natural impulse to mock out open():

# test_user.py

from io import StringIO
from unittest.mock import patch

@patch("builtins.open")
def test_add_two_settings(mock_open):
    mock_open.return_value = StringIO('{"opt1": 10, "opt2": 7}')
    assert add_two_settings() == 17

Nice, the test works without needing to create a file in our home directory!

Much later...

One day your test suite fails with an error like:

...
  File ".../site-packages/coverage/python.py", line 55, in get_python_source
    source_bytes = read_python_source(try_filename)
  File ".../site-packages/coverage/python.py", line 39, in read_python_source
    return source.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
           ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
TypeErrorreplace() argument 1 must be str, not bytes

What happened!? Coverage.py code runs during your tests, invoked by the Python interpreter. The mock in the test changed the builtin open, so any use of it anywhere during the test is affected. In some cases, coverage.py needs to read your source code to record the execution properly. When that happens, coverage.py unknowingly uses the mocked open, and bad things happen.

When you use a mock, patch it where it’s used, not where it’s defined. In this case, the patch would be:

@patch("myproduct.user.open")
def test_add_two_settings(mock_open):
    ... etc ...

With a mock like this, the coverage.py code would be unaffected.

Keep in mind: it’s not just coverage.py that could trip over this mock. There could be other libraries used by your code, or you might use open yourself in another part of your product. Mocking the definition means anything using the object will be affected. Your intent is to only mock in one place, so target that place.

Postscript

I decided to add some code to coverage.py to defend against this kind of over-mocking. There is a lot of over-mocking out there, and this problem only shows up in coverage.py with Python 3.14. It’s not happening to many people yet, but it will happen more and more as people start testing with 3.14. I didn’t want to have to answer this question many times, and I didn’t want to force people to fix their mocks.

From a certain perspective, I shouldn’t have to do this. They are in the wrong, not me. But this will reduce the overall friction in the universe. And the fix was really simple:

open = open

This is a top-level statement in my module, so it runs when the module is imported, long before any tests are run. The assignment to open will create a global in my module, using the current value of open, the one found in the builtins. This saves the original open for use in my module later, isolated from how builtins might be changed later.

This is an ad-hoc fix: it only defends one builtin. Mocking other builtins could still break coverage.py. But open is a common one, and this will keep things working smoothly for those cases. And there’s precedent: I’ve already been using a more involved technique to defend against mocking of the os module for ten years.

Even better!

No blog post about mocking is complete without encouraging a number of other best practices, some of which could get you out of the mocking mess:

November 16, 2025 12:55 PM UTC

November 15, 2025


Kay Hayen

Nuitka Release 2.8

This is to inform you about the new stable release of Nuitka. It is the extremely compatible Python compiler, “download now”.

November 15, 2025 01:52 PM UTC

November 14, 2025


Real Python

The Real Python Podcast – Episode #274: Preparing Data Science Projects for Production

How do you prepare your Python data science projects for production? What are the essential tools and techniques to make your code reproducible, organized, and testable? This week on the show, Khuyen Tran from CodeCut discusses her new book, "Production Ready Data Science."

November 14, 2025 12:00 PM UTC


EuroPython Society

Recognising Michael Foord as an Honorary EuroPython Society Fellow

Hi everyone. Today, we are honoured to announce a very special recognition.

The EuroPython Society has posthumously elected Michael Foord (aka voidspace) as an Honorary EuroPython Society Fellow.


Michael Foord (1974–2025)

Michael was a long-time and deeply influential member of the Python community. He began using Python in

November 14, 2025 09:00 AM UTC

November 13, 2025


Paolo Melchiorre

How to use UUIDv7 in Python, Django and PostgreSQL

Learn how to use UUIDv7 today with stable releases of Python 3.14, Django 5.2 and PostgreSQL 18. A step by step guide showing how to generate UUIDv7 in Python, store them in Django models, use PostgreSQL native functions and build time ordered primary keys without writing SQL.

November 13, 2025 11:00 PM UTC


Python Engineering at Microsoft

Python in Visual Studio Code – November 2025 Release

The November 2025 release brings new Pylance features including improvements to Copilot Hover Summaries and a Code Action to convert wildcard imports to explicit imports. Keep on reading to learn more!

The post Python in Visual Studio Code – November 2025 Release appeared first on Microsoft for Python Developers Blog.

November 13, 2025 06:41 PM UTC

November 12, 2025


Python Software Foundation

Python is for everyone: Join in the PSF year-end fundraiser & membership drive!

November 12, 2025 05:03 PM UTC