When you start building a Python Django application, two powerful concepts often surface: signals and middleware. Both let you hook into the request/response cycle or model lifecycle without cluttering your core business logic. This guide walks you through everything you need to know about Django signals and Django middleware, from the basics to advanced custom implementations, so you can write cleaner, more maintainable code.
Understanding Django Signals
What Are Django Signals?
Signals are a built‑in messaging framework that lets decoupled applications get notified when certain events occur. Think of them as “hooks” that fire automatically when actions like saving a model, creating a user, or completing a request happen.
How Signals Work Under the Hood
- Sender: The object (usually a model class) that triggers the signal.
- Receiver: A callable (function or method) that runs when the signal is sent.
- Dispatch: Django’s
Signal.send()method notifies all registered receivers.
Common Built‑In Signals
pre_save/post_save– Fired before/after a model.save().pre_delete/post_delete– Triggered around model deletion.m2m_changed– Emitted when a many‑to‑many relationship changes.request_started/request_finished– Tied to the HTTP request lifecycle.got_request_exception– Sent when an unhandled exception bubbles up.
Creating a Custom Signal
Sometimes the built‑in signals aren’t enough. Defining your own signal is straightforward:
from django.dispatch import Signal
# Define a signal that provides the user and a custom message
user_action = Signal(providing_args=["user", "message"])
Connecting Receivers
Use the @receiver decorator or the connect() method to bind a function to a signal.
from django.dispatch import receiver
from myapp.signals import user_action
@receiver(user_action)
def log_user_action(sender, **kwargs):
user = kwargs.get('user')
msg = kwargs.get('message')
logger.info(f"User {user.username}: {msg}")
Disconnecting Receivers
If you need to detach a receiver (e.g., during tests), call disconnect():
user_action.disconnect(log_user_action)
Best Practices for Signals
- Keep receivers lightweight: Avoid heavy DB queries or long‑running tasks.
- Use explicit imports: Import signals in
apps.pyready() method to guarantee registration. - Document intent: Clearly comment why a signal is used; it can be confusing for newcomers.
- Prefer model methods for simple logic: Only use signals when you truly need decoupling.
Django Middleware Explained
What Is Middleware?
Middleware is a stack of lightweight components that process requests before they reach the view and responses before they’re 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_exception(self, request, exception)process_template_response(self, request, response)process_response(self, request, response)
Built‑In Middleware Examples
SecurityMiddleware– Enforces HTTPS, HSTS, and other security headers.SessionMiddleware– Manages session data on each request.AuthenticationMiddleware– Associates users with requests viarequest.user.CommonMiddleware– Handles URL normalization and APPEND_SLASH.
Writing Custom Middleware
Below is a minimal example that logs the execution time of each request:
import time
import logging
logger = logging.getLogger(__name__)
class RequestTimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response # Called once per request
def __call__(self, request):
start = time.monotonic()
response = self.get_response(request)
duration = (time.monotonic() - start) * 1000 # ms
logger.info(f"{request.method} {request.path} took {duration:.2f}ms")
return response
Ordering Middleware Correctly
Django processes middleware in the order they appear in settings.MIDDLEWARE for process_request and process_view, but reverses the order for process_response. Remember:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # runs first
'myapp.middleware.RequestTimingMiddleware', # runs second
'django.middleware.common.CommonMiddleware', # runs third
# ... more middleware
]
Accessing Request and Response Objects
- Request: You can read
request.GET,request.POST,request.user, and even add custom attributes (e.g.,request.start_time). - Response: Modify headers (
response['X-My-Header'] = 'value') or alter the content before returning.
Middleware vs. Signals: When to Use Which?
- Middleware is ideal for cross‑cutting concerns that need access to the raw
HttpRequestorHttpResponse(e.g., authentication, CORS, request logging). - Signals shine when you want to react to model events or other Django‑level actions without touching the request/response flow (e.g., sending a welcome email after
User.save()).
Integrating Signals with Middleware
Real‑World Example: Auditing User Activity
Suppose you want to log every time a user creates, updates, or deletes an object, and also capture the originating IP address. You can combine a middleware that stores the IP in request with a signal that records the action.
# middleware.py
class CaptureIPMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.ip_address = request.META.get('REMOTE_ADDR')
return self.get_response(request)
# signals.py
from django.dispatch import receiver
from django.db.models.signals import post_save, post_delete
from myapp.models import MyModel
from django.contrib.auth import get_user_model
import logging
logger = logging.getLogger('audit')
@receiver(post_save, sender=MyModel)
def log_save(sender, instance, created, **kwargs):
user = getattr(instance, '_changed_by', None)
ip = getattr(instance, '_request_ip', 'unknown')
action = 'created' if created else 'updated'
logger.info(f"{user} {action} {sender.__name__} (ID={instance.pk}) from {ip}")
@receiver(post_delete, sender=MyModel)
def log_delete(sender, instance, **kwargs):
user = getattr(instance, '_changed_by', None)
ip = getattr(instance, '_request_ip', 'unknown')
logger.info(f"{user} deleted {sender.__name__} (ID={instance.pk}) from {ip}")
In your view, attach the user and IP to the model before saving:
def my_view(request):
obj = MyModel(...)
obj._changed_by = request.user
obj._request_ip = getattr(request, 'ip_address', 'unknown')
obj.save()
# middleware already set request.ip_address
Testing Signals and Middleware Together
When writing unit tests, you can use django.test.override_settings to replace middleware temporarily, and django.test.signals to capture emitted signals:
from django.test import TestCase, override_settings
from django.dispatch import receiver
from myapp.signals import user_action
class SignalMiddlewareTest(TestCase):
@override_settings(MIDDLEWARE=['myapp.middleware.CaptureIPMiddleware'])
def test_audit_signal(self):
captured = {}
@receiver(user_action)
def capture(sender, **kwargs):
captured.update(kwargs)
response = self.client.post('/my-url/', {'field': 'value'})
self.assertIn('user', captured)
self.assertIn('message', captured)
Leave a Reply