Django signals cheatsheet.
Built-in signals
from django.db.models.signals import (
pre_save, post_save,
pre_delete, post_delete,
pre_init, post_init,
pre_migrate, post_migrate,
m2m_changed,
)
from django.dispatch import receiver
Receiver
@receiver(post_save, sender=Post)
def post_post_save(sender, instance, created, **kwargs):
if created:
print(f"new post: {instance.id}")
Register in apps.py
# blog/apps.py
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "blog"
def ready(self):
from . import signals # import to register
Or define receivers directly in signals.py and import in ready().
post_save
@receiver(post_save, sender=Post)
def update_search_index(sender, instance, created, update_fields, **kwargs):
if update_fields and "body" not in update_fields:
return
SearchIndex.update(instance)
pre_save
@receiver(pre_save, sender=Post)
def normalize_slug(sender, instance, **kwargs):
if not instance.slug:
instance.slug = slugify(instance.title)
post_delete
@receiver(post_delete, sender=Post)
def cleanup_files(sender, instance, **kwargs):
if instance.image:
instance.image.delete(save=False)
m2m_changed
@receiver(m2m_changed, sender=Post.tags.through)
def tags_changed(sender, instance, action, pk_set, **kwargs):
if action == "post_add":
for tag_id in pk_set:
update_tag_count(tag_id)
Actions: pre_add, post_add, pre_remove, post_remove, pre_clear, post_clear.
Custom signal
# signals.py
import django.dispatch
post_published = django.dispatch.Signal()
Send:
def publish(post):
post.published = True
post.save()
post_published.send(sender=Post, instance=post)
Receive:
@receiver(post_published)
def notify(sender, instance, **kwargs):
notify_subscribers(instance)
Connecting without decorator
def my_handler(sender, **kwargs): ...
post_save.connect(my_handler, sender=Post)
post_save.disconnect(my_handler, sender=Post)
Specific sender
@receiver(post_save, sender=Post) # only Post saves
@receiver(post_save) # ALL saves
dispatch_uid (prevent duplicate)
@receiver(post_save, sender=Post, dispatch_uid="blog.post_save_unique")
def handler(...):
...
Prevents the same handler being registered twice (e.g., via dev reload).
transaction.on_commit
from django.db import transaction
@receiver(post_save, sender=Order)
def order_saved(sender, instance, created, **kwargs):
if created:
transaction.on_commit(lambda: process_order.delay(instance.id))
Without on_commit, the Celery task may try to read the row before COMMIT.
When NOT to use signals
Signals are powerful but easy to abuse:
- Avoid for business logic — hard to find, hard to test.
- Don’t use to enforce invariants — call a service method instead.
- Avoid cross-app side effects — coupling without imports.
Better: explicit method calls.
# BAD: side effect via signal
@receiver(post_save, sender=Post)
def index_post(...):
SearchIndex.update(instance)
# GOOD: explicit
class Post(models.Model):
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
SearchIndex.update(self)
Or a service:
def publish_post(post):
post.published = True
post.save()
SearchIndex.update(post)
notify_subscribers(post)
Signals + bulk operations
⚠️ Signals don’t fire on bulk_create, bulk_update, qs.update(), qs.delete() by default.
Post.objects.filter(...).update(views=F("views") + 1)
# No post_save signal!
If you need signals, use a loop with .save().
App ready vs module-level
Register signals in apps.py:ready(), NOT module-level. Avoids early imports + circular imports.
Built-in signals worth knowing
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
from django.core.signals import request_started, request_finished, got_request_exception
from django.test.signals import setting_changed
Common mistakes
- Heavy work in signal handler — blocks request. Use Celery.
- Forgetting
dispatch_uid→ handler runs twice. - Signal handler in wrong file (not loaded).
- Triggering signals from loops without batching.
pre_savemodifyingpkfield on existing object — chaos.
Read this next
If you want my signals-as-service refactor patterns, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .