Introduction
Beyond built-in model signals, Django lets you define your own signals for application-specific events. Custom signals decouple components and enable extensible architectures.
Key Concepts
- Signal: A
Signal()instance that acts as a notification channel. - Sender: The class or object that sends the signal.
- Receiver: A function connected to the signal that reacts when it fires.
Real World Context
Custom signals are used for publishing events (article published, payment received), cross-module side effects (update search index), and plugin systems where third-party code hooks into your application.
Deep Dive
Defining and Sending
To fire a signal, call .send() with the sender class and any keyword arguments your receivers expect:
python# myapp/signals.py from django.dispatch import Signal article_published = Signal() order_completed = Signal()
python# myapp/services.py from .signals import article_published def publish_article(article, user): article.status = 'published' article.save() article_published.send(sender=article.__class__, article=article, user=user)
The example above illustrates the pattern in practice. Now let's look at the next approach.
Receiving
Connect receiver functions to signals using the @receiver decorator. Each receiver runs when the signal is sent:
pythonfrom django.dispatch import receiver from myapp.signals import article_published @receiver(article_published) def notify_subscribers(sender, article, user, **kwargs): for sub in article.author.subscribers.all(): send_email(sub.email, f'New: {article.title}')
The example above illustrates the pattern in practice. Now let's look at the next approach.
send() vs send_robust()
Django offers two ways to send signals: send() propagates exceptions from receivers, while send_robust() catches them and returns results:
python# send() raises if any receiver raises article_published.send(sender=Article, article=article) # send_robust() catches exceptions results = article_published.send_robust(sender=Article, article=article) for receiver, response in results: if isinstance(response, Exception): logger.error(f'Handler failed: {response}')
The example above illustrates the pattern in practice. Now let's look at the next approach.
Register in AppConfig
Signal handlers must be imported at startup for Django to register them. The standard pattern is importing your handlers module in AppConfig.ready():
pythonclass NotificationsConfig(AppConfig): name = 'notifications' def ready(self): import notifications.handlers # noqa: F401
Common Pitfalls
- Forgetting
**kwargs— Receivers must accept**kwargsfor forward compatibility. - Side effects without
on_commit— Receivers inside transactions should usetransaction.on_commitfor email/HTTP calls. - Duplicate connections — Use
dispatch_uidto prevent the same handler being connected twice.
Best Practices
- Use
send_robust()in production — Prevents one failing handler from breaking the chain. - Keep handlers lightweight — Defer heavy work to background tasks.
- Register in
AppConfig.ready()— Ensures single registration.
Summary
- Custom signals are defined with
Signal()and sent with.send()or.send_robust(). - Connect receivers with
@receiveror.connect(dispatch_uid=...). - Register handlers in
AppConfig.ready(). - Use
send_robust()in production for fault tolerance.
Code Examples
from django.dispatch import Signal, receiver
article_published = Signal()
@receiver(article_published)
def notify(sender, article, **kwargs):
send_email(article.author.email, f'Published: {article.title}')
# Send:
article_published.send(sender=Article, article=article)