16 Jun 2026
Django community aggregator: Community blog posts
Cheating as a programming discipline
Great programmers cheat. A hard problem gets quietly swapped for an easier one; a transaction-grade database is replaced by a flat file nobody misses; machinery everyone else considers mandatory simply never gets built. They know a lot - and that's exactly why they get away with it.

16 Jun 2026 11:00am GMT
15 Jun 2026
Django community aggregator: Community blog posts
LLM Inspired Development
How Claude inadvertently suggested new features for my personal site.
15 Jun 2026 3:28pm GMT
14 Jun 2026
Django community aggregator: Community blog posts
Understanding Memory Usage in Django Webserver Workers

If you are coming from the PHP world, you might be used to thinking that when a request reaches the web server, everything is parsed and processed from scratch. In Python, however, the behavior is a little different.
A Python web server (for example, Gunicorn) starts one or more worker processes and then continuously accepts and processes requests as a running server application. In this article, I will explore how memory is managed in that environment.
Startup time vs. request execution
When you run a Django web server (either the development server or a Gunicorn-based deployment), there are two phases of execution: startup, when the server application is initialized, and the request/response cycle, when a request is received and processed to return a response.
During startup, wsgi.py and get_wsgi_application() are executed once per worker at boot time, Django settings are evaluated, applications listed in INSTALLED_APPS are imported and registered, ready() methods of app configs are called, and URL patterns are compiled.
During request execution, the middleware chain processes the request, the URL is resolved, the view is executed, context managers run within the view, the template is rendered, the middleware chain processes the response, and the response is returned.
The Django development server runs a single worker process but uses threads to handle concurrent requests. Gunicorn runs multiple worker processes (separate copies of the server application). A common rule of thumb for Gunicorn configuration is (2 × number_of_CPU_cores) + 1 workers.
Little demo of shared memory between requests
If you have mutable globals in your Django code, they will be shared within the same worker process, even across threads.
Here's a simple example for demonstration purposes (note that this is an antipattern and should never be used in production):
from django.http import JsonResponse
from django.utils.timezone import now
TIMESTAMPS = []
def show_timestamps(request):
TIMESTAMPS.append(now())
return JsonResponse({"timestamps": TIMESTAMPS})
The output would be (manually indented for readability):
{"timestamps": [
"2026-06-08T19:48:42.044Z",
"2026-06-08T19:48:43.875Z",
"2026-06-08T19:48:44.776Z"
]}
If you serve this application with Gunicorn using multiple workers and open the view in several tabs, refreshing them a few times, you will see that the timestamp list keeps growing. However, because each request may be served by a different worker, the timestamps returned by each worker will differ.
How RAM is managed
1. Compilation/setup state is persistent
When Django starts and the application server is initialized, the following objects live in process memory for the lifetime of the worker process:
django.apps.registry.Apps(the app registry with all models)- URL resolvers (
urlpatterns) - Middleware instances (if instantiated at startup)
- Cached template engines
- Database connection configurations (not connections themselves)
This memory normally remains allocated until the worker process restarts.
2. Execution state lives per-request
Each request creates objects that are local to that request's call stack-views, forms, querysets, serializers, and so on. Once the response is sent:
- Python's garbage collector normally reclaims those objects.
- There is no explicit Django cleanup; Django relies on Python's normal memory management.
- Request-scoped objects are cleaned up only after they go out of scope and no references to them remain.
Common bugs to avoid
1. Mutable module-level state
A mutable object defined at the module level persists across all requests handled by that worker process and will accumulate data from every user.
❌ BAD:
_recent_users = []
def dashboard(request):
# leaks across requests!
_recent_users.append(request.user.id)
return render(
request,
"dashboard.html",
{"recent": _recent_users}
)
✅ GOOD - state belongs in the DB, cache, or session:
def dashboard(request):
recent_users = get_recent_users_from_db()
return render(
request,
"dashboard.html",
{"recent": recent_users}
)
2. Storing per-request data on a shared instance
Middleware instances live for the lifetime of the worker process. Storing anything request-specific on self means it will be shared across every request handled by that worker.
❌ BAD:
class AuditMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# shared across all requests!
self.current_user = None
def __call__(self, request):
# User A's data visible when User B's request runs
self.current_user = request.user
return self.get_response(request)
✅ GOOD - attach data to the request object, which is scoped to a single request:
class AuditMiddleware:
def __init__(self, get_response):
# only immutable config on self
self.get_response = get_response
def __call__(self, request):
# scoped to this request's lifetime
request.current_user = request.user
return self.get_response(request)
3. Class-level cache shared across instances
A class attribute is shared across all instances of that class and therefore across all requests. The first user's data may be cached and returned to everyone else.
❌ BAD:
class ReportGenerator:
# class attribute, shared across all instances and all requests!
_cache = {}
def get_data(self, user):
if "data" not in self._cache:
# first user's data cached for everyone
self._cache["data"] = expensive_query(user)
return self._cache["data"]
✅ GOOD - use the cache framework with a user-scoped key:
from django.core.cache import cache
class ReportGenerator:
def get_data(self, user):
key = f"report:{user.id}"
return cache.get_or_set(
key,
lambda: expensive_query(user),
timeout=300,
)
Mental model
Before writing module-level code, ask yourself:
"If 1000 different users hit this worker, and this variable exists for all of them, is that safe?"
| Location | Lifetime | Safe for user data? |
|---|---|---|
| Local variable in a view/function | Single request | ✅ Yes |
request object attributes |
Single request | ✅ Yes |
threading.local() |
Single thread | ✅ Yes |
| Database / cache with scoped keys | Persistent but keyed | ✅ Yes |
self on a middleware/class instance |
Worker lifetime | ⚠️ Only immutable config |
| Module-level mutable variable | Worker lifetime | ❌ Never user data |
| Class-level mutable attribute | Worker lifetime | ❌ Never user data |
Rule of thumb: immutable configuration (numbers, booleans, strings, bytes, tuples, frozensets, and None) belongs at module level; anything that varies by request, user, or time belongs in the request cycle.
Conclusion
A Django worker process has two separate memory areas: a startup state that lives for the lifetime of the worker process (app registry, URL resolvers, middleware instances, template engines) and a request state that exists only while handling a request (views, querysets, forms) and is reclaimed by Python's garbage collector afterward.
The main rule is that immutable configuration can live at the module level, but anything that depends on the current user or request should stay inside the request cycle, such as local variables or the request object.
That's because module-level variables, class attributes, and middleware instance state are shared by all requests handled by the same worker, making them unsafe for user-specific data and a common cause of memory leaks and data exposure bugs.
Cover Picture by Jon Tyson
14 Jun 2026 5:00pm GMT
12 Jun 2026
Django community aggregator: Community blog posts
Issue 341: Django 2026 Fundraising Goals
News
DSF 2026 Fundraising Goals
The Django Software Foundation is raising its 2026 annual fundraising goal from $300,000 to $500,000. The money supports the Django Fellows program, legal and trademark work, community grants and events, ongoing infrastructure, and progress toward hiring an Executive Director.
Announcing Our DjangoCon US 2026 Talks!
DjangoCon US 2026 released its tutorial and talk lineup for Aug 24 to Aug 26, with live access for online-only ticket holders and free YouTube uploads after the conference. Expect sessions spanning Django 6.1 and modern ORM patterns, performance testing, Wagtail routing, Postgres updates, and deployment topics, with the final schedule to follow soon.
Vulnerability and malware checks in uv
uv introduces uv audit to scan locked dependencies for known vulnerabilities and adverse statuses, positioned as a faster uv-native alternative to pip-audit.
Updates to Django
Today, "Updates to Django" is presented by Hwayoung from Djangonaut Space! 🚀
Last week we had 16 pull requests merged into Django by 11 different contributors - including 5 first-time contributors! Congratulations to jodizzle, Bankai, Chris Rose, esperonus-karolis and Wes P. for having their first commits merged into Django - welcome on board!
This week's Django highlights: 🦄
- Made DjangoJSONEncoder consistently omit the .000 fractional-second suffix when serializing datetime and time values that have no meaningful microseconds. (#37108)
- Added a new listurls management command to display all registered URL patterns in a Django project. 🐍 (#28800)
- Limited the number of related objects shown in inline formset validation error messages to prevent excessively long error output. (#36984)
- Changed SIGNED_COOKIE_LEGACY_SALT_FALLBACK to default to False and began deprecating the transitional compatibility setting. (CVE-2026-6873)
Releases
Python 3.14.6 and 3.13.14 are now available!
Python 3.14.6 is out as the sixth 3.14 maintenance release, with about 179 bugfixes plus build and documentation updates since 3.14.5. Python 3.13.14 follows as the fourteenth 3.13 maintenance release, adding around 240 bugfixes along with build and documentation changes since 3.13.13.
Core Dispatch #5
Python 3.15.0 beta 2 landed June 2, with another round of milestones due June 9 and June 23. Expect the practical stuff in 3.15 including a fixed O(n^2) blowup in unicodedata.normalize, XML multi-byte encoding support, and fresh deprecation warnings around ast and abc abstract* helpers, alongside an initial documented Python security policy in the Devguide.
Sponsored Link
Django middleware composes request handlers. Harnesses do the same for AI agents - Claude Code, Codex, Gemini in one coordinated system. Learn what a harness actually is, why it's a new primitive, and how to engineer one that holds in production. Apache 2.0, open source.

Articles
My Local LLM Setup - Fast Agentic Development with No Token Bill
From Peter Grandstaff, a write-up of his maxxed-out local LLM setup using an Nvidia GeForce RTX 4090 video card. A very cool setup and one which many of us will likely be using some version of in the future.
Logical optimizations
Nested if blocks can often be rewritten as a single conjunction, but only when each if fully owns the body with no else or trailing code.
Browser Push Notifications for a Django Website
Set up browser push notifications end to end: store push subscriptions in Django, register/unregister via authenticated endpoints, and send notifications from a Huey background task using pywebpush with VAPID.
Anything new?
Maintainer Matthias Kestenholz explains why stepping away from django-mptt is hard, and how entitlement in issue trackers turns "free labor" into a burnout trap.
PyCon US 2026 - Open Source Community in Long Beach - Peter Grandstaff
Another one from Peter Grandstaff, a day-by-day PyCon US 2026 trip focused on building Django connections, sponsorship work for DjangoCon US, and catching talks on developer experience and open source community support.
Django Forum
Switch to Playwright tests for integration testing
This project aims to modernize Django's integration testing by introducing Playwright as an alternative to Selenium.
Proposal: Leverage Oracle Test Pilot for Django CI
A proposal from the Oracle Test Pilot for Third-Party Software program building Oracle Test Pilot to provide access to the latest versions of the Oracle databases to Django on GitHub, for free.
Django Fellow Reports
Natalia Bidart
This week was quite intense, with most of the focus 🔍 on getting the security release out the door 🚪. Issuing the release for the 5 CVEs took a fair amount of coordination and attention to detail, and definitely consumed a good chunk of brain power 🧠 ⚡.
Alongside that, there were a number of meetings throughout the week, so overall it was a mix of high-focus release work and keeping in sync with the different groups 🤝. Bonus: the final DEP 0018 for MAILERS was approved, moved to the accepted folder, and merged ✅.
Jacob Walls
A highlight this week was landing the listurls command modeled on django-extensions. Many tickets triaged, reviewed, authored, and discussed.
Events
Announcing Our DjangoCon US 2026 Talks!
The complete lineup of talks, August 24-26, is now live! So many great talks coming up.
Django Meetup Vol. 78 / Beyond Boilerplate: Building Maintainable CRUD in Django
Django Meetup Cologne Vol. 78 will take place on the 16th of June 2026, online and in person.
Django Job Board
Founding ML/Data Scientist (Remote, UK) at MyDataValue 🆕
Projects
django-helpdesk/django-helpdesk
A Django application to manage tickets for an internal helpdesk. Formerly known as Jutda Helpdesk.
emmett-framework/granian
A Rust HTTP server for Python applications.
12 Jun 2026 3:00pm GMT
10 Jun 2026
Django community aggregator: Community blog posts
Running Fallout London on Bazzite
I'm a huge Fallout fan, and Fallout London is one of the most impressive mods I've seen in years: a full DLC-sized expansion set in post-apocalyptic London, made by a community team. Running it on Bazzite (my gaming OS of choice) wasn't completely straightforward, so here's what actually worked for me. Consider this a note to future me, but hopefully it saves someone else an afternoon of trial and error.
What you'll need
- Bazzite installed on your machine
- Heroic Games Launcher
- Fallout 4 (the mod requires it as a base)
- The "Fallout London One Click Mod" (available through Heroic)
The steps
1. Install Heroic Games Launcher
If you don't have it yet, install Heroic from the Bazzite app store or via Flatpak. It's a fantastic open-source launcher that handles GOG, Epic, and Amazon games, and it plays very nicely with Proton.
After you install it, login with your GOG account

2. Install the Fallout London One Click Mod
Search for "Fallout London" in Heroic and install the One Click Mod version. This bundles everything together so you don't have to manually manage mod files. Let it do its thing.

3. Disable UMU (yes, it needs to be disabled)
This is the counterintuitive part. Once the mod is installed, go to its settings in Heroic, then the Advanced tab. You'll see an option called "Disable UMU". Enable it (meaning: check the checkbox to disable UMU). I know, "enable the disable" is a confusing way to phrase it, but that's what it says.
Without this, the game won't launch correctly on Bazzite.

4. Run it once in Desktop Mode
Before adding it to Steam, launch the game once directly from Heroic while you're in Desktop Mode. This lets everything install and configure properly: shaders, redistributables, the works. Wait until you're actually in the game and confirmed it runs without issues, then close it.
5. Add to Steam
Click the three-dot menu on the game in Heroic and select "Add to Steam". From this point on you can launch it from Game Mode like any other game in your library.

Play!
That's it. Boot into Game Mode, find Fallout London in your library, and enjoy one of the best Fallout experiences made outside of Bethesda.


See you in the next one!
10 Jun 2026 5:00am GMT
09 Jun 2026
Django community aggregator: Community blog posts
Logical optimizations
The second article in the series. The first was about control flow; this one stays with the same tactic - reshaping code - one layer down, at the condition. Here: merging ifs, factoring shared decisions, and dropping checks that earn nothing. The Boolean algebra of conditions - De Morgan and friends - is a different lever, and gets its own installment next time.

09 Jun 2026 11:00am GMT
05 Jun 2026
Django community aggregator: Community blog posts
Browser Push Notifications for a Django Website

For DjangoTricks, and some other websites, I intentionally didn't set email notifications when a feedback message arrived - I didn't want to pay for an email server or spam my inbox. While checking the messages in the database from time to time, sometimes I found out about them too late.
Last weekend, I decided to implement Web Push notifications to get notified about the feedback in my OS, just like in this example:

This tutorial walks through adding Web Push notifications to a Django project from scratch. When a visitor submits a feedback form the site owner receives a native browser notification - even if the admin tab is closed - thanks to a service worker and a Huey background task.
How it works
Push Notifications work in such a way: at first, people who want to get notifications need to subscribe to the notifications in their browser. The subscribers are stored on the push notification servers and also their identifiers are stored in Django website database. Whenever we need to send the messages to those subscribers, we send them to push notification server that passes the message to all subscribers if their browsers are open at the moment. If the browsers are closed at that moment, the message's TTL (time to live) in seconds set long enough, and the message is not expired yet, they will get the message later.
The push service depends on the browser:
- Chrome - Google's FCM (Firebase Cloud Messaging) -
fcm.googleapis.com - Firefox - Mozilla Autopush -
updates.push.services.mozilla.com - Safari - Apple Push Notification Service (APNs)

The two standard Web Push prerequisites are a VAPID key pair (identifies your server to the push service) and a service worker (a background JS script that receives the push and shows the notification even when the tab is closed).
The workflow would be as follows:
- I as a superuser visit the Django administration page that has sw.js file with a service worker and a JavaScript to subscribe to notidications.
- JavaScript request Notification permission. I accept it.
- JS calls
pushManager.subscribe()with the VAPID public key. - JS POSTs the subscription to
/notifications/push/subscribe/. - Django stores it in PushSubscription model.
Visitor submits feedback form
- A view saves
FeedbackMessageto the database. transaction.on_commitqueues a Huey task.- Huey task calls
pywebpush- browser's push service (FCM / Mozilla). - Push service wakes the service worker.
- Service worker shows a native OS notification.
- Clicking it opens the Django admin change page.
Prerequisites
- Django project using Huey for background tasks
- Python virtual environment
Install two dependencies:
(.venv)$ pip install pywebpush py-vapid
pywebpushis the library to communicate with the Push Notification server.py-vapidwill only be needed once to generate keys.
You'll need HTTPS to test the Web Push notifications if your host is not 127.0.0.1. If you use a custom domain in your /etc/hosts, such as djangotricks.localhost, you will also need to set up HTTPS with mkcert.
Step 1. The Feedback App
Create feedback app with FeedbackMessage model that will store submitted feedback messages:
# myproject/apps/feedback/models.py
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
class FeedbackMessage(models.Model):
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
submitter_name = models.CharField(_("Submitter name"), max_length=200)
submitter_email = models.EmailField(_("Submitter email")
content = models.TextField(_("Content")
class Meta:
verbose_name = _("Feedback Message")
verbose_name_plural = _("Feedback Messages")
ordering = ("-created_at",)
def __str__(self):
return _("Feedback message from {}").format(self.submitter_name)
Create the form:
# myproject/apps/feedback/forms.py
from django import forms
from django.utils.translation import gettext_lazy as _
from .models import FeedbackMessage
class FeedbackMessageForm(forms.ModelForm):
class Meta:
model = FeedbackMessage
fields = [
"content",
"submitter_name",
"submitter_email",
]
widgets = {
"content": forms.Textarea(
attrs={
"rows": 5,
"placeholder": _("Your message")
}
),
}
labels = {
"submitter_name": _("Your name"),
"submitter_email": _("Your email"),
}
Create a view to handle that form:
# myproject/apps/feedback/views.py
from django.db import transaction
from django.shortcuts import render, redirect
from django.urls import reverse
from .forms import FeedbackMessageForm
from .tasks import send_feedback_push_notification
def feedback_form(request):
if request.method == "POST":
form = FeedbackMessageForm(data=request.POST)
if form.is_valid():
message = form.save(commit=False)
if request.user.is_authenticated:
message.user = request.user
message.save()
transaction.on_commit(
lambda: send_feedback_push_notification(message.pk)
)
return redirect(reverse("feedback:complete"))
else:
form = FeedbackMessageForm()
return render(request, "feedback/form.html", {"form": form})
def feedback_complete(request):
return render(request, "feedback/complete.html")
Create the app config, migrations, templates, URLs, and Django administration for it.
Step 2. VAPID keys
Generate the key pair once. These stay on the server and are never committed to version control.
(.venv)$ vapid --gen
(.venv)$ vapid --applicationServerKey --private-key private_key.pem
--gen writes private_key.pem and public_key.pem to the current directory.
The private_key.pem file will contain the key like:
-----BEGIN PRIVATE KEY-----
<Multiline private key data>
-----END PRIVATE KEY-----
--applicationServerKey prints the base64url-encoded public key the browser needs, such as:
Application Server Key = <Public key data as base64url>
For the secrets.json or .env file where you store your secrets, you will need the content of <Private key data with newlines removed> and <Public key data as base64url>.
{
...
"VAPID_PRIVATE_KEY": "<Private key data with newlines removed>",
"VAPID_PUBLIC_KEY": "<Public key data as base64url>"
}
Don't commit the *.pem files or the secrets to the Git repo!
Step 3. Django settings
# myproject/settings.py
### WEB PUSH ###
VAPID_PRIVATE_KEY = get_secret("VAPID_PRIVATE_KEY")
VAPID_PUBLIC_KEY = get_secret("VAPID_PUBLIC_KEY")
VAPID_ADMIN_EMAIL = "admin@mydomain.com"
Step 4. The Notifications App
Create notifications app with the PushSubscription model to track the Push notification subscribers:
# myproject/apps/notifications/models.py
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
class PushSubscription(models.Model):
"""One browser push subscription for one device."""
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
user = models.ForeignKey(
_("User"),
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="push_subscriptions",
)
endpoint = models.TextField(_("Endpoint"), unique=True)
p256dh = models.TextField(_("Browser ECDH public key"))
auth = models.TextField(_("16-byte auth secret"))
class Meta:
verbose_name = _("Push Subscription")
verbose_name_plural = _("Push Subscriptions")
ordering = ("-created_at",)
def __str__(self):
return f"{self.user} - {self.endpoint[:60]}"
Create app configuration, migrations, and Django administration for it.
Step 5. Subscribe / unsubscribe views
Create the views that will be called after the user subscribes or unsubscribes to Push notifications. Also a view for the service worker sw.js:
# myproject/apps/notifications/views.py
import json
from django.contrib.auth.decorators import login_required
from django.contrib.staticfiles import finders
from django.http import HttpResponse, JsonResponse
from django.views.decorators.http import require_POST
from .models import PushSubscription
@login_required
@require_POST
def push_subscribe(request):
try:
data = json.loads(request.body)
endpoint = data["endpoint"]
p256dh = data["keys"]["p256dh"]
auth = data["keys"]["auth"]
except (KeyError, json.JSONDecodeError):
return JsonResponse({"error": "Invalid subscription data"}, status=400)
PushSubscription.objects.update_or_create(
endpoint=endpoint,
defaults={"user": request.user, "p256dh": p256dh, "auth": auth},
)
return JsonResponse({"status": "subscribed"})
@login_required
@require_POST
def push_unsubscribe(request):
try:
endpoint = json.loads(request.body)["endpoint"]
except (KeyError, json.JSONDecodeError):
return JsonResponse(
{"error": "Invalid data"},
status=400
)
PushSubscription.objects.filter(
user=request.user,
endpoint=endpoint,
).delete()
return JsonResponse({"status": "unsubscribed"})
def service_worker(request):
"""Serve sw-feedback.js as sw.js at the admin root"""
path = finders.find("admin/js/sw-feedback.js")
with open(path) as fh:
content = fh.read()
return HttpResponse(
content,
content_type="application/javascript"
)
Step 6. Wire URLs into myproject/urls.py
Plug the notification views into URLs:
# myproject/apps/notifications/urls.py
from django.urls import path
from . import views
app_name = "notifications"
urlpatterns = [
path("push/subscribe/", views.push_subscribe, name="push_subscribe"),
path("push/unsubscribe/", views.push_unsubscribe, name="push_unsubscribe"),
]
The service worker URL must be mounted at the same prefix as ADMIN_URL so its scope covers the admin. Add both patterns before the admin.site.urls line:
# myproject/urls.py
from notifications import views as notifications_views
urlpatterns = [
# ...
path(
f"{settings.ADMIN_URL}sw.js",
notifications_views.service_worker,
name="admin_service_worker",
),
path(
"notifications/",
include("notifications.urls", namespace="notifications"),
),
path(settings.ADMIN_URL, admin.site.urls),
# ...
]
Step 7. Service worker JavaScript
Save as myproject/static/admin/js/sw-feedback.js:
self.addEventListener("push", function (event) {
const data = event.data ? event.data.json() : {};
const title = data.title || "New feedback message";
const options = {
body: data.body || "",
icon: "/static/admin/img/icon-yes.svg",
data: { url: data.url || "/" },
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
self.addEventListener("notificationclick", function (event) {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
Step 8. Admin subscription JavaScript
Save as myproject/static/admin/js/push-subscribe.js:
(function () {
"use strict";
// Injected by the Django template override in Part 9.
const VAPID_PUBLIC_KEY = window.VAPID_PUBLIC_KEY;
const SUBSCRIBE_URL = window.PUSH_SUBSCRIBE_URL;
const UNSUBSCRIBE_URL = window.PUSH_UNSUBSCRIBE_URL;
const ADMIN_URL = window.ADMIN_URL;
// Tracks the endpoint last registered on the server so we can delete it even
// when the browser subscription has already been silently revoked (e.g. user
// cleared site data or the push subscription expired without blocking).
const STORAGE_KEY = "pushSubscriptionEndpoint";
// Read the CSRF token from the hidden input injected by {% csrf_token %}.
// Do not use the cookie: this project has CSRF_USE_SESSIONS = True.
const CSRF_TOKEN = document.querySelector("[name=csrfmiddlewaretoken]")?.value || "";
function urlBase64ToUint8Array(base64String) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = atob(base64);
return Uint8Array.from([...rawData].map((c) => c.charCodeAt(0)));
}
function post(url, body) {
return fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json", "X-CSRFToken": CSRF_TOKEN },
body: JSON.stringify(body),
});
}
async function syncSubscription() {
if (!VAPID_PUBLIC_KEY) return;
if (!("serviceWorker" in navigator) || !("PushManager" in window)) return;
const registration = await navigator.serviceWorker.register(
"/" + ADMIN_URL + "sw.js",
{ scope: "/" + ADMIN_URL }
);
await navigator.serviceWorker.ready;
const storedEndpoint = localStorage.getItem(STORAGE_KEY);
const existing = await registration.pushManager.getSubscription();
// The browser subscription was revoked (user blocked notifications, cleared
// site data, or the subscription expired) but the server record still exists
// - delete it using the endpoint we stored at subscription time.
if (storedEndpoint && (!existing || existing.endpoint !== storedEndpoint)) {
await post(UNSUBSCRIBE_URL, { endpoint: storedEndpoint });
localStorage.removeItem(STORAGE_KEY);
}
// Already subscribed and the server already knows about this endpoint.
if (existing && existing.endpoint === storedEndpoint) return;
const permission = await Notification.requestPermission();
if (permission !== "granted") return;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
await post(SUBSCRIBE_URL, subscription.toJSON());
localStorage.setItem(STORAGE_KEY, subscription.endpoint);
}
document.addEventListener("DOMContentLoaded", syncSubscription);
})();
localStorage is the key to reliable unsubscription. Notification.permission alone cannot detect silent revocations - when a user clears site data or a push subscription expires, the permission may still read "granted" while the browser-side subscription is gone. By storing the endpoint at subscribe time and comparing it on every page load, the script can call UNSUBSCRIBE_URL with the old endpoint even after the browser subscription object has disappeared.
Step 9. Context processor
A context processor injects the VAPID globals into every admin template response.
Create myproject/apps/notifications/context_processors.py:
from django.conf import settings
from django.urls import reverse
def push_notifications(request):
"""Inject push-notification globals into every admin page."""
if not request.path.startswith(f"/{settings.ADMIN_URL}"):
return {}
if not settings.VAPID_PUBLIC_KEY:
return {}
return {
"push_vapid_public_key": settings.VAPID_PUBLIC_KEY,
"push_subscribe_url": reverse("notifications:push_subscribe"),
"push_unsubscribe_url": reverse("notifications:push_unsubscribe"),
"push_admin_url": settings.ADMIN_URL,
}
Register it in the template settings:
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
os.path.join(BASE_DIR, "myproject", "templates"),
],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
# ...
"myproject.apps.notifications.context_processors.push_notification_settings",
],
},
},
]
Step 10. Inject globals via admin/base_site.html
Override myproject/templates/admin/base_site.html. If one already exists, add the extrahead block; otherwise create the file extending Django's built-in template:
{% extends "admin/base_site.html" %}
{% load static %}
{% block extrahead %}
{# ... any existing content such as a favicon include ... #}
{% if push_vapid_public_key %}
{% csrf_token %}
<script nonce="{{ request.csp_nonce }}">
window.VAPID_PUBLIC_KEY = "{{ push_vapid_public_key }}";
window.PUSH_SUBSCRIBE_URL = "{{ push_subscribe_url }}";
window.PUSH_UNSUBSCRIBE_URL = "{{ push_unsubscribe_url }}";
window.ADMIN_URL = "{{ push_admin_url }}";
</script>
<script src="{% static 'admin/js/push-subscribe.js' %}"></script>
{% endif %}
{% endblock %}
Step 11. Huey task
# myproject/apps/feedback/tasks.py
import json
from django.conf import settings
from django.urls import reverse
from huey.contrib.djhuey import db_task
@db_task()
def send_feedback_push_notification(feedback_message_id):
from pywebpush import webpush, WebPushException
from feedback.models import FeedbackMessage
from notifications.models import PushSubscription
private_key = settings.VAPID_PRIVATE_KEY
if not private_key:
return
if not (message := FeedbackMessage.objects.filter(
pk=feedback_message_id
).first()):
return
admin_url = settings.WEBSITE_URL + reverse(
"admin:feedback_feedbackmessage_change",
args=(message.pk,)
)
content = message.content
body = content[:120] + (
"..." if len(content) > 120 else ""
)
payload = json.dumps({
"title": f"{message.submitter_name}:",
"body": body,
"url": admin_url,
})
dead_endpoints = []
for sub in PushSubscription.objects.all():
try:
webpush(
subscription_info={
"endpoint": sub.endpoint,
"keys": {"p256dh": sub.p256dh, "auth": sub.auth},
},
data=payload,
vapid_private_key=private_key,
vapid_claims={"sub": f"mailto:{settings.VAPID_ADMIN_EMAIL}"},
)
except WebPushException as exc:
# 410 Gone / 404 Not Found means the subscription has expired
if exc.response is not None and exc.response.status_code in (404, 410):
dead_endpoints.append(sub.endpoint)
if dead_endpoints:
PushSubscription.objects.filter(
endpoint__in=dead_endpoints,
).delete()
The Huey task is already called from the view in feedback app via transaction.on_commit. Using on_commit ensures the task is only queued after the database row is fully committed, so the task always finds the message when it runs.
Step 12. Content Security Policy
If the project uses Django CSP, two directives need adjusting:
CSP_WORKER_SRC = ["'self'"] # allows service worker registration from this origin
CSP_CONNECT_SRC = ["'self'"] # allows the subscribe POST fetch
The pywebpush HTTP call to the external push service (FCM, Mozilla) runs server-side inside the Huey worker process and is not subject to the browser's CSP.
User experience
It' safest to keep the body of the message up to 120 characters long - the rest will likely be cut on different OSes or browsers.
You can check the current status of notification permissions by:
Notification.permission // should be "granted", not "default" or "denied"
Based on that you can write a specific message in the User Interface what to do if the permission has been denied.
If you have many different types of notifications, you would set the configuration in a Django website. And let the visitor subscribe to the notifications in the browser once. Then your Huey tasks would check the notification settings and trigger send according messages to the subscribers.
For example, for DjangoTricks website, I would allow subscribing to new tricks, blog posts, and goodies in the notification configuration, and the visitors would grant permission to Web Push notifications just once.
Privacy and security
Messages themselves are stored on the Web Push servers encrypted, but back in the OS they are shown in plain text, and can be seen by people standing behind the user or possibly read out by other apps (or viruses) which have permissions to access OS notifications.
Practical recommendations for sensitive content:
- Send a generic notification ("You have a new message") and make the user open the app to see the actual content - this is what most banking/healthcare apps do.
- If you do include content, keep it minimal - avoid full message text, Personally Identifiable Information (PII), medical info, etc.
- Make sure your VAPID keys are securely stored and rotated if compromised.
- Set a short TTL (time-to-live) on the push message so it doesn't sit on Google's servers long if the device is offline.
For anything regulated (HIPAA, GDPR, financial data), the safest approach is the generic notification pattern, since you have no control over how the OS handles notification display and history once it leaves the browser.
OS permissions
For notification receivers, the permissions can be denied for the Browser globally as well.
One can set or unset them on MacOS at Settings ➔ Notifications ➔ Google Chrome (or another browser analogically) or on Windows at Focus Assist / Notification settings.
Marketing perspective
Opt-in: When asking for push notifications without context, only 5-25% would grant the permission. If the permission is asked in the UI at first and the reason is given, about 60% would grant the permission.
Opt-out: On average, nearly 8-10% of subscribers opt out from web push notifications per year. Even just 1 push notification per week leads to 10% of users disabling notifications. 46% of users disable notifications if they receive more than 6 notifications.
Final words
The technique of Web Push is not trivial, but with the help of py-vapid and pywebpush, it becomes manageable. The best use cases for push notifications are those where a SaaS project or a web platform suggests to use this technique intentionally: when waiting for something to happen, such as a new comment, a reply to a message, a new task to do from another person, or a new post of a favorite author who writes irregularly.
Cover picture by cottonbro studio
05 Jun 2026 5:00pm GMT
Issue 340: Django security releases 6.0.6 and 5.2.15
News
Django security releases issued: 6.0.6 and 5.2.15
Five CVEs are fixed in this latest release. As ever, perhaps the best security step you can take is to always update to the latest version of Django.
Updates to Django
Today, "Updates to Django" is presented by Hwayoung from Djangonaut Space! 🚀
Last week we had 13 pull requests merged into Django by 8 different contributors - including 4 first-time contributors! Congratulations to Vishwa, Tim Harris, Codequiver, and Joe Babbitt for having their first commits merged into Django - welcome on board!
This week's Django highlights: 🦄
- Deprecated the safe parameter of JsonResponse, as the browser vulnerability it protected against was fixed in ECMAScript 5. #36905
Releases
Python Release Python 3.15.0b2
Python 3.15.0b2, the second beta of four, is out with an explicit push for third-party maintainers to test now and file issues as early as possible. The release targets feature-complete beta with no ABI changes after beta 4, and recommends delaying production releases until 3.15.0rc1.
Python Software Foundation
PSF Strategic Plan 2026 Draft: Open for Community Feedback
PSF is publishing the full Strategic Plan 2026 draft and opening a three-week feedback window ending June 25. The board asks reviewers to focus on whether the goals and objectives are right, while implementation details will be shaped later by staff.
Sponsored Link
Django middleware composes request handlers. Harnesses do the same for AI agents - Claude Code, Codex, Gemini in one coordinated system. Learn what a harness actually is, why it's a new primitive, and how to engineer one that holds in production. Apache 2.0, open source.

Articles
Showcasing allauth IdP: build an MCP server | allauth
Learn how to use Django and django-allauth to secure MCP endpoints with OIDC, including token validation, client registration, and host authorization flows.
Django: introducing django-integrity-policy
From Adam Johnson, a new security header and detailed article laying out the "why."
Dependency Pruning
Tips on how to treat every lockfile entry as an attack surface and maintenance burden you do not want, then start by deleting dependencies you never import.
Loopwerk: uv is fantastic, but its package management UX is a mess
uv shines for Python toolchains, but its package maintenance UX is rough: there is no straightforward uv outdated, and the upgrade workflow (uv lock --upgrade) can aggressively pull in breaking major releases.
Python 3.15: features that didn't make the headlines
Python 3.15 beta highlights worth a look: TaskGroup.cancel for graceful cancellation, ContextDecorator fixing decorator lifecycles for async and generators, a new threading iterator helpers to avoid broken state, and immutable JSON support via frozendict and an array_hook.
Please add an RSS Feed to Your Site
RSS is still the cleanest way to keep up with the people you actually want to hear from. If you host a personal site with Django, add an RSS feed quickly with a simple, up-to-date tutorial and ship it.
Using Read the Docs to benefit Django
Read the Docs can integrate with EthicalAds, letting maintainers earn a little from their documentation.
The Pursuit Of Purity (The Right Way To Do AI)
A thoughtful look at competing takes on AI ethics, from safety-first big-lab work to open, locally run, consensually sourced models.
Django Forum
django-alauth 65.18.0 released: IdP demo time
django-allauth 65.18.0 was just shipped with a bunch of Identity Provider (IdP) improvements!
Daphne v4.2.2 release
Daphne v4.2.2 is now available on PyPI. It fixes a couple of moderate/low security issues and is a recommended update for all users.
Django Fellow Reports
Natalia Bidart
My primary focus this week was polishing the upcoming security release. I spent time going deeper into areas I am less familiar with to ensure everything was in good shape for release. As release manager, this included reviewing and completing release notes, preparing backports for all three supported stable branches, and crafting the corresponding CVE metadata so records are ready ahead of disclosure (this is part of our CNA responsibilities).
Sarah Boyce
I was at PyCon Italia this week, which was fantastic, highly recommend going if you get the chance.
Jacob Walls
After a Monday holiday in the US, I spent a week focusing on contributions from the prior week's PyCon sprint.
Events
PyBay 2026
October 3rd in San Francisco this year. The Call for Proposals (CP) is open until July 8th.
Django Job Board
Founding Engineer at MyDataValue
Projects
feincms/feincms3-cookiecontrol
Cookie banner with support for embedded media.
adamghill/dj-lite-tenant
Multi-tenant SQLite databases for Django.
05 Jun 2026 2:00pm GMT
03 Jun 2026
Django community aggregator: Community blog posts
Anything new?
Anything new?
A lot of time has passed since I officially announced that I want to step down from maintaining django-mptt. I started contributing around 2009, tagged the 0.3 release in April 2010, and have been the sole active maintainer since somewhere around 2019. The post about django-tree-queries has more background, but that's not today's topic.
Stepping away isn't easy
For me, abandoning a project is a bit like stepping out of a relationship: negative emotions end up being a somewhat necessary driver, because the absence of positive events alone rarely provides enough force on its own. I get a lot of satisfaction from a job well done, and walking away means letting that go.
Even with time set aside for open source in my work day, I still have to choose where that time goes. django-mptt stopped being where it needed to go.
The sense of entitlement
When a project is obviously unmaintained, asking for free labor is walking a tightrope. It takes real care not to rekindle exactly the frustrations that led maintainers away in the first place.
It takes energy not to clap back when someone is being rude or insensitive in the issue tracker. Asking "Anything new?" on a ticket where the next steps were outlined clearly and obviously nothing happened in the meantime is just one variant of this.
Quietly quitting isn't what I want to do - and as a user of django-mptt myself, I can't really do that either. Taking the high road is the professional choice. But it costs something.
I keep coming back to Mona Eltahawy on refusing to be civil. She's speaking about something quite different, and I'm aware I write this as a white man. The situations aren't the same at all. But she articulated something I haven't managed to put into words as well myself and I like the idea of speaking up and taking the fight to those who awaken these feelings instead of taking the high road.
Doing it with AI
No post these days is complete without the obligatory AI mention, but there's some relevancy to it.
I fixed and closed almost all open django-mptt issues in a two-hour Claude session. I've previously written about using LLMs for open source maintenance, and the productivity gain is real whatever the detractors say. And the quality isn't suddenly getting worse. Code wasn't perfect before either. The test suite allows a certain degree of trust in the result and according to my rules for releasing Open Source software we don't have to require more than that.
It doesn't change the underlying dynamic, though. rsync and outrage illustrates the trap neatly: Tridgell got flooded with AI-generated security reports, used AI to handle them, and then got criticized for using AI. The tools that created the workload aren't allowed to address it. The expectation is that the work has to involve sweat and tears and uncountable unpaid hours.
The common goal should be more and better open source software. What we get as Open Source maintainers is shit from both sides: One side took our free work and trained models on it without asking, the other side complains about the supposedly unethical use of AI while acting in unethical ways themselves.
There's something Kantian about how open source contribution gets framed. Kant's argument was that the only truly moral acts are those driven by duty and good will - not by desire, inclination, or any expectation of compensation. By that logic, I'm only acting morally if I keep going despite the burnout and the entitlement. If I stop, I'm not.
It's bleak. The problems with AI are real. The people controlling the large models are assholes. But I have to work in the world as it is while also trying to change it for the better.
03 Jun 2026 5:00pm GMT
02 Jun 2026
Django community aggregator: Community blog posts
You don't need React to be reactive — djust 1.0 is here
djust 1.0 is here - reactive UI for Django in pure Python. No client state, no JavaScript framework, no build step, no API layer. It brings the proven Phoenix LiveView model to Django with a Rust VDOM on the hot path. Try it live (multi-user, no install) at start.djust.org.
02 Jun 2026 6:00pm GMT
Code is cheap
The first time I said "code is cheap" out loud in a meeting, a manager waved at the budget - headcount, salaries, the tooling line - and asked which part of that looked cheap. He wasn't wrong about the number - he was wrong about what it was buying.

02 Jun 2026 10:15am GMT
01 Jun 2026
Django community aggregator: Community blog posts
My PyCon Italia 2026
A timeline of my PyCon Italia 2026 journey, in Bologna (IT), told through the Mastodon posts I shared along the way.
01 Jun 2026 3:00am GMT
31 May 2026
Django community aggregator: Community blog posts
Django: introducing django-integrity-policy
Back in January, Firefox's Security & Privacy Newsletter for 2025 Q4 piqued my interest with this mention:
Integrity-Policy: Firefox 145 has added support for the Integrity-Policy response header. The header allows websites to ensure that only scripts with an integrity attribute will load.
A new security header! That's right up my street: I've cared about getting security headers right since 2018, when I created django-permissions-policy to set the Permissions-Policy header. (At the time, it was called Feature-Policy: why they changed it, I can't say, people just liked it better that way.)
The new Integrity-Policy header helps with subresource integrity, a tool for securely including third-party scripts and stylesheets on your website. Browsers support the integrity attribute on <script> and <link> tags, which allows you to specify a hash of the expected content, like:
<script
src=https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta4/dist/htmx.min.js
integrity=sha384-aWZK1NtOs/aWb/+YZdTM8q2JkWEshlMc9mgZ189numT9bwFhyAyYEoO4nO/2dTXt
crossorigin=anonymous></script>
If the content downloaded from the external source doesn't match the expected hash, the browser blocks it from loading. This is a great defense against the target URL changing its contents, executing a supply chain attack against your visitors.
(Generally, I recommend you avoid loading anything from a third-party URL, per Reasons to avoid Javascript CDNs. But sometimes, you gotta do what you gotta do, and declaring integrity is a great idea, then.)
Integrity-Policy allows you to opt in to requiring integrity attributes on your page, ensuring that you can never load potentially-compromised resources. The header is fairly simple, at least right now-here's a complete example that requires integrity for all scripts and stylesheets:
Integrity-Policy: blocked-destinations=(script style)
You can add in endpoints to tell browsers where to send violation reports to, and there's the second Integrity-Policy-Report-Only header which lets you test a policy without enforcing it.
Note there's no possibility to differentiate between first- and third-party resources in Integrity-Policy. If you set it, you'll need to add integrity attributes to all your scripts and stylesheets, including those you host yourself. This is by design: the header is being developed as part of Web Application Integrity, Consistency and Transparency (WAICT), an initiative to bring app-store-level "code signing" to the web, where users can be sure that CDNs and other intermediaries haven't tampered with served code.
Integrity-Policy is supported on Firefox 145+ and Chrome 138+.
django-integrity-policy
My new package, django-integrity-policy provides a middleware for setting the Integrity-Policy headers in a familiar Django-style way. You install it, add the middleware:
MIDDLEWARE = [
...,
"django.middleware.security.SecurityMiddleware",
"django_integrity_policy.IntegrityPolicyMiddleware",
...,
]
…and then configure the appropriate setting(s):
INTEGRITY_POLICY = {
"blocked-destinations": ["script", "style"],
}
So far, it's pretty basic, but I expect WAICT will increase the complexity of Integrity-Policy over time, and I'll add support for new options as they come along.
Once Integrity-Policy is set, the browser will block any scripts or stylesheets (depending on configuration) that lack a valid integrity attribute, including your first-party resources. That means you need to add integrity attributes to all your static files. Luckily, this has been considered before by the legendary Jake Howard, who made a package called django-sri. It provides template tags to generate appropriately hashed HTML tags. For example:
{% load sri %}
{% sri_static "app.js" %}
{% sri_static "app.css" %}
…will output:
<script src="/static/app.js" integrity="sha256-..."></script>
<link rel="stylesheet" href="/static/app.css" integrity="sha256-..."/>
These tags would be allowed under a maximally strict integrity policy.
See the example application in the django-integrity-policy repository for a full working project.
LLM generation
I built this package using just two prompts to Claude. I copied the repository for my previous security header package, django-permissions-policy, and reset its Git history. I then used this prompt to Claude, inside Zed:
The current repository is a copy of my package django-permissions-policy
It's time to turn it into django-integrity-policy, for the relevant headers per the below mdn docs
(Integrity-Policy MDN docs from https://github.com/mdn/content/blob/main/files/en-us/web/http/reference/headers/integrity-policy/index.md?plain=1)
(Integrity-Policy-Report-Only MDN docs from https://github.com/mdn/content/blob/main/files/en-us/web/http/reference/headers/integrity-policy-report-only/index.md?plain=1)
Check and edit every file to be that new package, copyright 2026, keeping the general testing infrastructure and so on.
Don't run any commands yet, just check and edit every file
I then took a 20-minute nap and woke up to a near-complete package. I reviewed it, ran the tests, made some minor edits, committed, and pushed to PyPI!
LLMs are rightfully a hot topic, with heady supporters and heavy detractors. I use them begrudgingly and somewhat sparingly, and I cannot wait for the future where I can stick to local models. (Gemma 4 can run on my M1 Mac and approaches Claude's performance on many tasks, so we're getting there.)
This task, though, was a perfect fit for LLM code generation: the existing repository acted as great context for structure, the new package was very similar in shape ("same same but different"), and the documentation provided a clear specification for what to build. The LLM could mash things up for me with minimal oversight, and I could check the work quickly.
I would guess that overall, using an LLM saved me a couple of hours of mostly grunt-work, like checking every copied-over configuration file. That's pretty valuable for me, and honestly made creating the package feasible.
The future
I'm not sure how widely used Integrity-Policy will be, and therefore how popular django-integrity-policy will end up. But this was an interesting exercise and I am interested to see how the header and other work from WAICT evolves. I will try to keep the package updated, and we'll see if it ever reaches a point where proposing support in Django itself makes sense.
31 May 2026 4:00am GMT
29 May 2026
Django community aggregator: Community blog posts
Issue 339: Early Bird DjangoCon US Tickets Ending Soon
News
DjangoCon US 2026: Early Bird Tickets End May 31st!
Early bird ticket sales for DjangoCon US 2026 end on May 31, 2026, with discounted pricing available. The conference runs five days at Voco Chicago Downtown and includes community-selected talks plus Django contribution sprints.
Wagtail CMS News
Wagtail Space NL - June 12
A full-day conference in Rotterdam, The Netherlands on Wagtail, with talks covering a range of topics, lightning talks, hallway discussions, and more.
Updates to Django
Today, "Updates to Django" is presented by Pradhvan from Djangonaut Space! 🚀
Last week we had 16 pull requests merged into Django by 10 different contributors.
This week's Django highlights: 🦄
- Django's built-in error pages, admin, and registration templates now include the CSP nonce on
<script>,<link>, and<style>elements when available. (#36825) - Fixed
HttpResponse.reason_phraseto raiseBadHeaderErrorwhen set to a value containing control characters. (#37100) - Fixed
Query.clear_ordering()to recursively clear ordering on combined queries, preventing errors when using__inlookups on nestedunion()querysets. (#37097) - Admin change form actions now use
ModelAdmin.get_queryset(), ensuring custom annotations and filtering are consistently applied to form actions. (#37117)
If you haven't already, give Django 6.1 alpha 1 a spin and report anything suspicious to the issue tracker! 🎉
That's all for this week in Django development! 🐍🦄
Articles
Upgrade PostgreSQL from 17 to 18 on Ubuntu 26.04
After moving to Ubuntu 26.04, upgrade an existing 17/main cluster to 18 by running pg_upgradecluster 17 main -v 18, then verify the new 18/main cluster is online. Once confirmed, drop the old 17 cluster with pg_dropcluster 17 main and optionally purge postgresql-17 and postgresql-client-17 packages.
My not-so-static new static website
Jake Howard walks through his eighth website rewrite, this time ditching Wagtail for a custom "semi-static" Django setup that renders Markdown content into SQLite at startup and serves it dynamically with Jinja2 templates.
Improving First Byte and Contentful Paint on a Django Website
A look at how to use Django's StreamingHttpResponse to send the ` and above-the-fold content first, letting the browser fetch static assets and start painting while the rest of the page renders.
PyCon US 2026 Recap - Black Python Devs
A recap from from the community booth to open spaces, hallway track, and Jay Miller receiving the PSF Community Service Award.
django-removals 1.2.0 - Now with Django 6.1 deprecations
How the maintainers of django-removals shipped new warnings for the Django 6.1 deprecation wave.
Mentoring GSoC 2026: Experimental Flags - Software Crafts
Mentor and mentee are starting a GSoC 2026 project around an "Experimental Flags" framework for Django core, using the forum to gather requirements and drive early consensus. The plan balances fast iteration with faster-than-normal Django consensus, including an initial third-party package to test ideas before wider adoption.
Django Forum
GSoC 2026: Implementing a Formal Experimental API Framework for Django Core
A lively discussion around how experimental features can be merged into the main repository but remain explicitly non-stable.
Thoughts on advertising on djangoproject.com
New thoughts and comments on the age-old question.
Django Fellow Reports
Jacob Walls
Not much going on, "just" the 6.1 Feature Freeze/alpha release, a sprint at PyCon US, and a kickoff meeting with Google Summer of Code participants & mentors.
Sarah Boyce
As we had the feature freeze, focused on a few feature PRs I had prioritized for 6.1 release.
Natalia Bidart
This week was mostly about returning from PyCon, which was quite exhausting. I arrived back on Wednesday, fairly drained (and very hungry), so I worked during Thu and Fri catching up on a large backlog of email notifications and syncing with the other Fellows.
Events
Django on the Med - September 23-25 in Pescara, Italy
PyCon Italia this week has been Django members in attendance, so it is a good time to remind readers that Django on the Med will be back in Italy later in the year.
Django Job Board
Founding Engineer at MyDataValue
Projects
feincms/feincms3-cookiecontrol
Cookie banner with support for embedded media.
emfpdlzj/django-deploy-probes
HTTP deployment probes for Django applications.
29 May 2026 2:00pm GMT
27 May 2026
Django community aggregator: Community blog posts
Please add an RSS Feed to Your Site
Why syndication feeds are having a moment in 2026.
27 May 2026 9:57pm GMT
Mentoring GSoC 2026: Experimental Flags
Over the last couple of weeks, Google Summer of Code (GSoC) has started for 2026, I think along side my mentee, I will blog about it as we progress through the project. So far, there has been a kick-off meeting with all participants and I have started to chat with my mentee (Praful) about the first steps of our project - Experimental Flags. he has posted to the Forum about the project, asking for feedback on what we want from the project.
Before I say anymore, please go and pitch your opinion and any ideas you may have, the more we have to work with the better! We need you!
What set's this project apart from GSoC projects in recent years is that we have yet to have an agreed solution in place that 'just' needs implementing. So my initial guide will be to focus on consensus gathering and documentation. But being a GSoC project with a limited time availabilty, I do feel the need to push the process forward at a pace for consensus that is faster than the normal Django pace. That said, the potential for this project is wide and expansive, currently with a lot of open questions both as to why we need them and what should be implemented and that's before we get to the details of how to implement this.
So for me, the why of experimental feature flags most things can be done or can be experimented with as a third-party package. I think the requirement for an experimental feature flag is perhaps for that last 10% of a new API, or where you need where getting higher usage of a feature is required to flesh out all of the use cases with a wider audience, this audience is beyond that of the community. If we think of the adoption curve we're talking about the early majority, those developers who are more likely to enable a feature inside Django, with it's stablilty guarantees, than a third-party package. Or perhaps this is the project which allows us as a community to get more flexible with what in the release package(s?) of Django and what code is in the source control repository?
One thing is for sure, I do want to ensure Praful isn't completely stuck so we will be experimenting with these ideas in a third-party package while we build consensus and then perhaps dogfood the process with our this package once consensus has been reached!
Again, go to the Forum and make your opinion known!
27 May 2026 5:00am GMT