Signals — and When Not to Use Them

Decouple side effects with pre_save/post_save/m2m_changed, and know the senior's case against overusing them.

Signals let one piece of code react to an event elsewhere without a direct call. When a User is created, a signal receiver can create their Profile; when an Order is saved, a receiver can invalidate a cache. The sender emits, any number of receivers listen.

The signals you'll actually use

  • pre_save / post_save — before/after a model is saved (with a created flag).
  • pre_delete / post_delete — around deletions.
  • m2m_changed — when a ManyToMany set is modified (the only way to hook M2M changes).
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

Register receivers in your app config's ready() method so they load exactly once. Always accept **kwargs — Django passes extra arguments and will keep adding them.

The senior caveat: signals make control flow invisible. A save in one app triggering an email in another, with nothing in the call site to hint at it, is a debugging nightmare at 2am. Prefer an explicit service function or an overridden save() when the logic belongs to the model. Reserve signals for genuine cross-app decoupling — especially reacting to models you don't own, like User or a third-party app.

Example

Example · python
# apps.py — wire receivers up exactly once, on app load.
from django.apps import AppConfig


class OrdersConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'orders'

    def ready(self):
        from . import signals   # noqa: F401  (import registers receivers)


# signals.py
from django.db import transaction
from django.db.models.signals import post_save, m2m_changed
from django.dispatch import receiver

from .models import Order
from .tasks import send_confirmation, reindex_search


@receiver(post_save, sender=Order)
def on_order_saved(sender, instance, created, **kwargs):
    if not created:
        return
    # Defer the side effect until the surrounding transaction commits.
    transaction.on_commit(lambda: send_confirmation.delay(instance.pk))


@receiver(m2m_changed, sender=Order.products.through)
def on_products_changed(sender, instance, action, pk_set, **kwargs):
    # Only react once the DB actually changed, not on the 'pre_' phases.
    if action in {'post_add', 'post_remove', 'post_clear'}:
        instance.recalculate_total()
        transaction.on_commit(lambda: reindex_search.delay(instance.pk))


# To make a signal robust to being registered twice, give it a dispatch_uid:
#   @receiver(post_save, sender=Order, dispatch_uid='orders.on_order_saved')

When to use it

  • A developer connects to the post_save signal on the User model to automatically create a UserProfile record.
  • A team uses the post_delete signal to remove associated S3 files when a media object is deleted.
  • A developer uses pre_save to automatically generate a unique slug from the title before a Post is saved.

More examples

Auto-create UserProfile on user save

Listens for new User creation and automatically creates a linked UserProfile record.

Example · python
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from .models import UserProfile

User = get_user_model()

@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
    if created:
        UserProfile.objects.create(user=instance)

pre_save to auto-generate slug

Generates a URL slug from the post title before saving if one has not already been set.

Example · python
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.utils.text import slugify

@receiver(pre_save, sender=Post)
def set_slug(sender, instance, **kwargs):
    if not instance.slug:
        instance.slug = slugify(instance.title)

post_delete to remove related file

Removes the physical file from disk when a Document record is deleted from the database.

Example · python
from django.db.models.signals import post_delete
from django.dispatch import receiver
import os

@receiver(post_delete, sender=Document)
def delete_file(sender, instance, **kwargs):
    if instance.file:
        if os.path.isfile(instance.file.path):
            os.remove(instance.file.path)

Discussion

  • Be the first to comment on this lesson.