Using Django Signals for Decoupled Event Handling

Django signals enable decoupled communication between different parts of an application by allowing senders to notify receivers when certain actions occur.

Built-in Signal Types

Django provides several categories of built-in signals:

  • Model signals

    • pre_init: emitted before model instance initialization
    • post_init: emitted after model instance initialization
    • pre_save: emitted before a model instance is saved
    • post_save: emitted after a model instance is saved
    • pre_delete: emitted before a model instance is deleted
    • post_delete: emitted after a model instance is deleted
    • m2m_changed: emitted when a many-to-many relation changes via add, remove, or clear
    • class_prepared: emitted when a model class has been prepared and registered
  • Management signals

    • pre_migrate: emitted before applying migrations
    • post_migrate: emitted after applying migrations
  • Request/response signals

    • request_started: emitted at the beginning of a request
    • request_finished: emitted at the end of a request
    • got_request_exception: emitted when an exception occurs during request handling
  • Test signals

    • setting_changed: emitted when a test modifies settings
    • template_rendered: emitted when a template is rendered during tests
  • Database wrapper signals

    • connection_created: emitted when a new database conneciton is created

Connecting to Built-in Signals

To react to a signal, register a callable using either Signal.connect() or the @receiver decorator.

from django.core.signals import request_finished, request_started, got_request_exception
from django.db.models.signals import (
    class_prepared, pre_init, post_init,
    pre_save, post_save, pre_delete,
    post_delete, m2m_changed,
    pre_migrate, post_migrate
)
from django.test.signals import setting_changed, template_rendered
from django.db.backends.signals import connection_created
from django.dispatch import receiver

@receiver(request_finished)
def log_request_completion(sender, **kwargs):
    print("Request has completed")

The built-in senders trigger these signals automatically; no manual invocation is required.

Defining Custom Signals

Custom signals allow application-specific events to be dispatched and handled.

  1. Declare the signal
import django.dispatch
order_processed = django.dispatch.Signal(providing_args=["item_count", "total_price"])
  1. Connect a handler
def handle_order(sender, **kwargs):
    print(f"Order processed: {kwargs['item_count']} items, total ${kwargs['total_price']}")

order_processed.connect(handle_order)
  1. Emit the signal
order_processed.send(sender='checkout_view', item_count=3, total_price=75.50)

Unlike built-in signals, custom ones must be explicitly sent where appropriate in the codebase.

Example: Logging on Model Creation

To record a log each time a model instance is added, connect handlers to pre_save and post_save.

from django.db.models import signals

def log_pre_save(instance, **ctx):
    print(f"Pre-save triggered for {instance.__class__.__name__}", ctx)

def another_pre_save(instance, **ctx):
    print(f"Another action before saving {instance.__class__.__name__}", ctx)

def log_post_save(instance, created, **ctx):
    if created:
        print(f"New instance created: {instance.__class__.__name__}", ctx)

# Register handlers
signals.pre_save.connect(log_pre_save)
signals.pre_save.connect(another_pre_save)
signals.post_save.connect(log_post_save)

These handlers will run automatically whenever a model's save() method is invoked, enabling side effects such as logging without modifying the model's core logic.

Tags: Django signals event-driven python web development

Posted on Mon, 20 Jul 2026 17:32:11 +0000 by binarylime