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
| Signal | Module | When Fired |
|---|---|---|
| pre_save | django.db.models.signals | Before model.save() |
| post_save | django.db.models.signals | After model.save() |
| pre_delete | django.db.models.signals | Before model.delete() |
| post_delete | django.db.models.signals | After model.delete() |
| m2m_changed | django.db.models.signals | ManyToMany change |
| user_logged_in | django.contrib.auth.signals | Successful login |
| user_login_failed | django.contrib.auth.signals | Failed login |
| request_started | django.core.signals | HTTP request begins |