Django Signals Guide

Django's signal system lets decoupled components react to events. Learn pre/post save, delete, request signals, custom signals, and safe patterns.

1. Built-in Model Signals

from django.db.models.signals import pre_save, post_save, pre_delete, post_delete
from django.dispatch import receiver
from django.contrib.auth import get_user_model

User = get_user_model()

# post_save — runs after model.save()
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

# pre_save — runs before model.save() — can modify data
@receiver(pre_save, sender=Article)
def auto_slug(sender, instance, **kwargs):
    if not instance.slug:
        from django.utils.text import slugify
        instance.slug = slugify(instance.title)

# post_delete — cleanup after deletion
@receiver(post_delete, sender=Article)
def delete_cover_image(sender, instance, **kwargs):
    if instance.cover_image:
        instance.cover_image.delete(save=False)

# Connect signals in AppConfig.ready()
# myapp/apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = "myapp"

    def ready(self):
        import myapp.signals  # noqa — registers receivers

2. receiver Decorator Patterns

from django.db.models.signals import post_save
from django.dispatch import receiver

# Multiple senders
@receiver(post_save, sender=Order)
@receiver(post_save, sender=Subscription)
def send_confirmation_email(sender, instance, created, **kwargs):
    if created:
        send_email(instance.user.email, "Order confirmed", ...)

# Using update_fields to avoid infinite loops
@receiver(post_save, sender=Article)
def update_author_stats(sender, instance, **kwargs):
    update_fields = kwargs.get("update_fields")
    # Only run if update_fields is None (full save) or includes relevant fields
    if update_fields is None or "status" in update_fields:
        Article.objects.filter(author=instance.author).aggregate(...)

# Safe signal with transaction
from django.db import transaction

@receiver(post_save, sender=Order)
def notify_fulfillment(sender, instance, created, **kwargs):
    if created:
        transaction.on_commit(lambda: process_order.delay(instance.pk))

3. Custom Signals

# myapp/signals.py
from django.dispatch import Signal

# Define custom signals
article_published  = Signal()   # args: article, publisher
payment_completed  = Signal()   # args: order, amount, method

# Send a custom signal
def publish_article(article, user):
    article.status = "published"
    article.save(update_fields=["status"])
    # Send signal — all connected receivers are called synchronously
    article_published.send(sender=Article, article=article, publisher=user)

# Receive a custom signal
@receiver(article_published)
def on_article_published(sender, article, publisher, **kwargs):
    Notification.objects.create(
        user=article.author,
        message=f'Your article "{article.title}" was published by {publisher}',
    )
    invalidate_article_cache(article.pk)

4. Request / Response Signals

from django.core.signals import request_started, request_finished
from django.test.signals import setting_changed

# Track active connections
@receiver(request_started)
def on_request_started(sender, environ, **kwargs):
    import threading
    current = getattr(on_request_started, "_count", 0) + 1
    on_request_started._count = current

# Django auth signals
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed

@receiver(user_logged_in)
def on_login(sender, request, user, **kwargs):
    user.last_login_ip = request.META.get("REMOTE_ADDR")
    user.save(update_fields=["last_login_ip"])

@receiver(user_login_failed)
def on_login_failed(sender, credentials, request, **kwargs):
    email = credentials.get("username")
    LoginAttempt.objects.create(email=email, ip=request.META.get("REMOTE_ADDR"), success=False)

5. Disconnecting Signals

from django.db.models.signals import post_save

# Manual connect
def my_handler(sender, instance, **kwargs):
    print(f"Saved: {instance}")

post_save.connect(my_handler, sender=MyModel)

# Disconnect
post_save.disconnect(my_handler, sender=MyModel)

# Temporarily disconnect — useful in tests
from contextlib import contextmanager

@contextmanager
def mute_signals(*signals_):
    handlers = []
    for signal in signals_:
        handlers.append((signal, signal.receivers[:]))
        signal.receivers = []
    try:
        yield
    finally:
        for signal, receivers in handlers:
            signal.receivers = receivers

6. Built-in Signals Reference

SignalModuleWhen Fired
pre_savedjango.db.models.signalsBefore model.save()
post_savedjango.db.models.signalsAfter model.save()
pre_deletedjango.db.models.signalsBefore model.delete()
post_deletedjango.db.models.signalsAfter model.delete()
m2m_changeddjango.db.models.signalsManyToMany change
user_logged_indjango.contrib.auth.signalsSuccessful login
user_login_faileddjango.contrib.auth.signalsFailed login
request_starteddjango.core.signalsHTTP request begins