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:
- 447 releases over 20 years. Thatâs about 22 per year on average. Weâre at 38 so far for 2025. Fun fact: 33 of those releases predate PyPI, and were published via the Django website only!
- 131 security vulnerabilities addressed in those Django releases. Our security issues archive is a testament to our stellar track-record.
- 262,203 releases of Django-related packages. Djangoâs community ecosystem is gigantic. Thereâs tens of releases of Django packages per day as of 2025. There were 52 just today. With the caveat this depends a lot on what you classify as a "Django" package.
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.
- â ïž today only - Get PyCharm Pro for 30% off - all the revenue goes to our Foundation.
- Donate on the Django website
- Donate on GitHub sponsors
- Check out how to become a Corporate Member
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!
Of our US $300,000.00 goal for 2025, as of November 19th, 2025, we are at:
- 58.7% funded
- $176,098.60 donated
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.
- Modern CSS supports many of the features natively that the build tools were created for. CSS nesting to organize code, variables, @supports for feature detection.
- JavaScript ES6 / ES2015 was a big step forward, and the language has been progressing steadily ever since. It now has native module support with the import / export keywords
- Meanwhile, with HTTP/2 performance improvements, parallel requests can be made over the same connection, removing the constraints of the HTTP/1.x protocol.
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.
- The support for
import/exportwith hashed files is not very robust. - Comments in CSS with references to files can lead to errors during collectstatic.
- Circular dependencies in CSS/JS can not be processed.
- Errors during collectstatic when files are missing are not always clear.
- Differences between implementation of StaticFilesStorage and ManifestStaticFilesStorage can lead to errors in production that don't show up in development (like #26329, about leading slashes).
- Configuring common options means subclassing the storage when we could use the existing OPTIONS dict.
- Collecting static files could be faster if it used parallelization (pull request: #19935 Used a threadpool to parallelise collectstatic)
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")
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
TypeError: replace() 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:
- Use
autospec=Trueto make your mocks strictly behave like the original object: see Why your mock still doesnât work. - Make assertions about how your mock was called to be sure everything is connected up properly.
- Use verified fakes instead of auto-generated mocks: Fast tests for slow services: why you should use verified fakes.
- Separate your code so that computing functions like our
add_two_settingsdon’t also do I/O. This makes the functions easier to test in the first place. Take a look at Function Core, Imperative Shell. - Dependency injection lets you explicitly pass test-specific objects where they are needed instead of relying on implicit access to a mock.
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!
