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 acreatedflag),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()andbulk_update()don't sendpre_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().