Understanding and Implementing Django Signals

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's save() method.
  • pre_delete / post_delete: Triggered before/after a model's delete() 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.

Tags: Django signals Observer Pattern python Event-Driven Architecture

Posted on Sat, 23 May 2026 20:53:42 +0000 by pavanpuligandla