Introduction to the Signal Mechanism
Django includes a robust signal dispatcher that facilitates communication between different parts of the application. This mechanism operates on the Observer pattern (also known as Publish/Subscribe). It allows specific "sender" components to notify "receiver" components when a specific action or event occurs. This decouples the application logic, enabling distinct functions to execute in response to events without direct function calls.
The signal workflow involves three primary components:
- Sender: The source that emits the signal.
- Signal: The event object itself.
- Receiver: The callback function that subscribes to the signal.
Built-in Signals in Django
Django provides numerous built-in signals that hook into the framework's lifecycle. Key categories include:
Model Signals
Located in django.db.models.signals, these allow you to hook into the model lifecycle:
pre_init/post_init: Triggered before/after a model instance's__init__method executes.pre_save/post_save: Triggered before/after a model'ssave()method.pre_delete/post_delete: Triggered before/after a model'sdelete()method.m2m_changed: Fired when a ManyToManyField on a model instance is modified.
Request/Response Signals
Located in django.core.signals:
request_started/request_finished: Fired when Django starts or finishes processing an HTTP request.got_request_exception: Triggered when an exception occurs during request processing.
Management Signals
pre_migrate/post_migrate: Sent before or after the migration command executes.
Listening to Signals
To react to a signal, you must register a receiver function. The receiver must accept a sender argument and wildcard keyword arguments (**kwargs).
Connection Methods
There are two primary ways to connect a receiver. The first is manually calling the connect method:
from django.core.signals import request_finished
def cleanup_callback(sender, **kwargs):
print("Request cycle completed.")
# Manual connection
request_finished.connect(cleanup_callback)
The second, and more common approach, uses the @receiver decorator:
from django.core.signals import request_finished
from django.dispatch import receiver
@receiver(request_finished)
def cleanup_callback(sender, **kwargs):
print("Request cycle completed.")
Targeting Specific Senders
Often, you only want to listen to signals from a specific model or sender. You can restrict the receiver using the sender argument in the decorator.
from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import Account
@receiver(pre_save, sender=Account)
def validate_account(sender, **kwargs):
# Logic runs only before an Account is saved
pass
Preventing Duplicate Signals
If your code might be imported multiple times, you can prevent duplicate receiver registrations by providing a unique dispatch_uid.
request_finished.connect(cleanup_callback, dispatch_uid="my_unique_cleanup_id")
Creating and Sending Custom Signals
Beyond built-in events, you can define custom signals for your application logic. All signals are instances of django.dispatch.Signal.
Defining a Signal
import django.dispatch
# Define a custom signal
order_completed = django.dispatch.Signal()
Sending a Signal
To emit a signal, use Signal.send() or Signal.send_robust(). Both methods require a sender argument and accept additional keyword arguments.
class OrderService:
def finalize_order(self, order_id):
# Business logic to finalize order
# ...
# Emit the signal
order_completed.send(sender=self.__class__, order_id=order_id, status='completed')
send() raises errors if a receiver crashes, while send_robust() catches exceptions and returns them in the response list. Both return a list of tuples containing the receiver function and its return value.
Disconnecting Signals
To stop a receiver from listening, use the disconnect method.
order_completed.disconnect(receiver=my_receiver_func)
Practical Implementation Example
This example demonstrates a custom signal integrated into a Django view. We will define a signal that notifeis listeners when a specific API endpoint is hit.
views.py
import time
from django.dispatch import Signal, receiver
from django.http import HttpResponse
# 1. Define a custom signal
api_accessed = Signal()
def signal_view(request):
# 2. Send the signal when the view is accessed
api_accessed.send(
sender=signal_view,
timestamp=time.strftime("%Y-%m-%d %H:%M:%S"),
path=request.path
)
return HttpResponse("Signal triggered successfully.")
# 3. Define a receiver to handle the signal
@api_accessed.connect
def log_api_access(sender, **kwargs):
print(f"API Access Logged -> Time: {kwargs['timestamp']}, Path: {kwargs['path']}")
return "Logged"
urls.py
from django.urls import path
from . import views
urlpatterns = [
path('api/trigger/', views.signal_view),
]
When /api/trigger/ is accessed, the view sends the api_accessed signal. The log_api_access receiver executes immediately, printing the log details to the console. The send() method will return a list similar to [(<function log_api_access at ...>, 'Logged')].
How Signals Work Internally
Django signals function synchronously. This is a critical distinction from asynchronous message queues. When send() is called, Django iterates through the connected receivers and executes them one by one in the same thread. The caller is blocked until all receivers have finished processing. The return values from the receivers are collected and returned as a list of tuples.