Signals

Django · Advanced · 10 min

Signals decouple apps from each other, but used carelessly they hide control flow and cause surprising bugs.

Signals let code subscribe to events sent elsewhere. Common built-ins:

  • model: pre_save, post_save (with a created flag), pre_delete, post_delete, m2m_changed
  • requests: request_started, request_finished
  • auth: user_logged_in, user_logged_out, user_login_failed

Connect a receiver with the @receiver decorator, filtering by sender:

@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs): ...

Receivers must accept **kwargs. Put them in signals.py and import that module in your app's AppConfig.ready(), otherwise the module may never be imported and the receiver never connected. Use dispatch_uid if a module might be imported twice.

You can define your own: order_paid = Signal() and order_paid.send(sender=Order, order=o).

Know the limits:

  • QuerySet.update(), bulk_create() and bulk_update() don't send pre_save/post_save
  • signals run synchronously, in the same transaction; a slow receiver slows the request, and an exception in one breaks the save
  • logic in signals is invisible at the call site, which makes code harder to follow and test

Good uses: letting a reusable app react to another app's events without importing it, cache invalidation, audit logs. If both sides are your own code, an explicit function call (create_user_with_profile()) or a model method is usually clearer.

Example

# accounts/signals.py
from django.conf import settings
from django.contrib.auth.signals import user_login_failed
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Profile

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

@receiver(user_login_failed)
def log_failed_login(sender, credentials, request, **kwargs):
    print("failed login for", credentials.get("username"))

# accounts/apps.py
from django.apps import AppConfig

class AccountsConfig(AppConfig):
    name = "accounts"
    def ready(self):
        from . import signals  # noqa: F401  (connects the receivers)

Remember

  • @receiver(signal, sender=Model) connects a function to an event
  • Import signal modules in AppConfig.ready()
  • update() and bulk operations don't send save signals
  • Prefer explicit calls when both sides are your code

Watch out for

  • Relying on post_save for logic that must also run after queryset.update() or bulk_create().

Check yourself

  1. Which operation does NOT trigger post_save receivers?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/topics/signals/

Builds on

Unlocks

Nothing builds on this yet.