What are Django signals and when should you use them?
Django's signal framework is a publish-subscribe mechanism that allows decoupled components to react to events elsewhere in the codebase. The core classes are `Signal`, `receiver`, and `connect`.
The built-in signals cover the ORM lifecycle (`pre_save`, `post_save`, `pre_delete`, `post_delete`, `m2m_changed`), request/response cycle (`request_started`, `request_finished`), and migrations. You can define custom signals for your own events.
Basic usage:
```python from django.db.models.signals import post_save from django.dispatch import receiver from myapp.models import User from notifications.tasks import send_welcome_email
@receiver(post_save, sender=User) def on_user_created(sender, instance, created, **kwargs): if created: send_welcome_email.delay(instance.id) ```
**When signals are appropriate:** - Reactions in a different Django app to events in another app, where directly importing the other app would create a circular dependency - Pluggable, reusable apps that shouldn't know about the host project's behavior (e.g., `django-allauth` hooks) - Framework-level cross-cutting concerns: audit logging, cache invalidation that spans multiple apps
**When signals are a code smell:** - When sender and receiver are in the same app — a direct function call is clearer, easier to test, and shows up in stack traces - When you need transactional guarantees — `post_save` fires after the ORM save but still within the open transaction if called from within `atomic()`. However, signal handlers are not automatically wrapped, and failures in handlers don't roll back the triggering transaction unless you handle exceptions explicitly - When you need to test in isolation — signals make code harder to follow (the call graph is invisible) and harder to unit test
**Pitfalls:** - Receivers are registered globally. If your `apps.py` ready() method registers a receiver (the recommended way), double-registering by also importing the module directly creates duplicate calls - Use `dispatch_uid` to prevent duplicate registration: `@receiver(post_save, sender=User, dispatch_uid='myapp.on_user_created')` - Signals are synchronous and blocking. Don't do heavy work in a signal handler — push to a task queue (Celery) instead - `post_save` does not fire on `bulk_create` or `QuerySet.update()` — common source of bugs
The Django docs themselves note that signals are often overused. Direct method calls, service objects, or explicit hooks are preferable when the coupling is intentional.
Explains the pub-sub mechanism, gives a correct use case (cross-app decoupling), and names at least one pitfall (synchronous, doesn't fire on bulk operations, or duplicate registration).
Covers the mechanism, appropriate vs inappropriate use cases, dispatch_uid deduplication, the bulk_create/update gap, transactional nuances, and when to prefer direct calls or service objects.
Reading the answer is step one. Explaining it unprompted — under interview pressure — is what actually matters. Get AI-graded feedback on your answer with follow-up probes on your weak points.
Get Graded — Free Assessment