When you start building a Django application, you quickly discover that the framework does more than just route URLs and render templates. Two of its most powerful, yet often misunderstood, features are signals and middleware. Mastering these tools lets you inject custom logic at the right moment, keep your code DRY, and maintain a clean separation of concerns. In this guide we’ll demystify Django signals and middleware, show you how to create and use them effectively, and explain when each tool is the best choice for your project.
Understanding Django Signals
What Are Signals?
Signals are a publish‑subscribe mechanism built into Django. They allow decoupled applications to get notified when certain actions occur—such as a model being saved, a user logging in, or a request finishing. Instead of tightly coupling code, you simply connect a receiver function to a signal and let Django handle the rest.
How Signals Work Behind the Scenes
- Sender: The object that emits the signal (e.g.,
django.db.models.Model). - Receiver: A callable that processes the signal payload.
- Signal Instance: An instance of
django.dispatch.Signalthat holds a list of receivers. - Dispatch: When the sender calls
signal.send(), Django loops through all registered receivers and passes them the provided arguments.
Built‑In Signals You Should Know
pre_save/post_save– Triggered before and after a model’ssave()method.pre_delete/post_delete– Fired around a model’s deletion.m2m_changed– Emitted when a many‑to‑many relationship is altered.request_started/request_finished– Sent at the beginning and end of each HTTP request.user_logged_in,user_logged_out,user_login_failed– Part of Django’s authentication system.
Creating a Custom Signal
Sometimes the built‑in signals aren’t enough. You can define your own signal in any app’s signals.py file.
# myapp/signals.py
from django.dispatch import Signal
# Provide a clear name and any arguments you expect to send
order_completed = Signal(providing_args=['order', 'user'])
Connecting Receivers
Connect a receiver using the @receiver decorator or the signal.connect() method. The decorator is preferred for readability.
# myapp/receivers.py
from django.dispatch import receiver
from .signals import order_completed
from .models import Notification
@receiver(order_completed)
def send_order_notification(sender, **kwargs):
order = kwargs['order']
user = kwargs['user']
# Example business logic
Notification.objects.create(
user=user,
message=f"Your order #{order.id} has been completed!"
)
Don’t forget to import the module that registers the receivers (usually in apps.py) so Django loads them at startup.
# myapp/apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = 'myapp'
def ready(self):
# Import signal handlers
import myapp.receivers
Best Practices for Signals
- Keep receivers lightweight: Heavy processing can slow down the request that triggered the signal.
- Use async tasks for long‑running work: Send the signal, then delegate to Celery or Django‑Q.
- Avoid circular imports: Import receivers inside
AppConfig.ready()rather than at module top level. - Document signal contracts: Clearly list the arguments each signal provides.
Deep Dive into Django Middleware
What Is Middleware?
Middleware is a stack of components that process a request before it reaches the view and a response before it’s sent to the client. Each middleware class implements at least one of the following methods:
process_request(self, request)process_view(self, request, view_func, view_args, view_kwargs)process_template_response(self, request, response)process_response(self, request, response)process_exception(self, request, exception)
Middleware Lifecycle Illustrated
request → process_request → process_view → view → process_template_response →
process_response → response → client
Each middleware can modify the request or response objects, raise an exception, or short‑circuit the chain by returning a response early.
Common Built‑In Middleware
SecurityMiddleware– Adds security‑related headers.SessionMiddleware– Enables session support.AuthenticationMiddleware– Associates users with requests.MessageMiddleware– Enables Django’s messaging framework.CommonMiddleware– Handles URL normalization and ETag generation.
Writing Your Own Middleware
Let’s create a simple middleware that logs the execution time of each view. Save it as myproject/middleware.py.
import time
import logging
logger = logging.getLogger(__name__)
class ExecutionTimeMiddleware:
"""
Logs how long a view takes to process.
"""
def __init__(self, get_response):
# One‑time configuration and initialization.
self.get_response = get_response
def __call__(self, request):
start = time.monotonic()
response = self.get_response(request)
duration = (time.monotonic() - start) * 1000 # ms
logger.info(
"%s %s completed in %.2f ms",
request.method,
request.path,
duration,
)
return response
Then add it to settings.py:
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
# ... other default middleware ...
'myproject.middleware.ExecutionTimeMiddleware', # ← custom middleware
]
Middleware vs. Signals: When to Use Which
- Use middleware when you need to interact with the request/response cycle itself—e.g., authentication, CORS handling, or request throttling.
- Use signals for model‑level events or cross‑app communication that doesn’t belong in a view or request flow—e.g., sending a welcome email after a user registers.
- Performance tip: Middleware runs on every request, while signals fire only when the associated event occurs. Choose the lighter tool for the job.
Practical Examples Combining Signals and Middleware
Example 1: Auto‑Assigning a User to a Group After Registration
We’ll use a post_save signal for the User model and a custom middleware to ensure the user is authenticated before the signal runs.
# accounts/signals.py
from django.contrib.auth.models import User, Group
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=User)
def add_user_to_default_group(sender, instance, created, **kwargs):
if created:
default_group, _ = Group.objects.get_or_create(name='customers')
instance.groups.add(default_group)
Middleware to enforce login for certain URLs:
# accounts/middleware.py
from django.shortcuts import redirect
from django.urls import reverse
class LoginRequiredMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path.startswith('/secure/') and not request.user.is_authenticated:
return redirect(f"{reverse('login')}?next={request.path}")
return self.get_response(request)
Example 2: Auditing API Calls with Signals and Middleware
First, a middleware that captures request metadata:
# api/middleware.py
class AuditMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
Leave a Reply