django/django
Signals and dispatch
django/dispatch/ is a tiny pub/sub primitive used throughout the framework. Receivers connect to a Signal instance; senders call Signal.send(sender, **kwargs) and every connected receiver runs in order.
Purpose
Decouple subsystems that need to react to events without importing each other. The ORM doesn't know about contenttypes; contenttypes connects to post_migrate and wires itself in. Test machinery uses signals to invalidate caches when settings change. Auth uses signals to track logins.
Directory layout
django/dispatch/
├── __init__.py # exposes Signal, receiver
└── dispatcher.py # Signal class (~250 lines)Plus the per-subsystem signal definitions:
| Module | Signals |
|---|---|
django/core/signals.py |
request_started, request_finished, got_request_exception, setting_changed |
django/db/models/signals.py |
pre_init, post_init, pre_save, post_save, pre_delete, post_delete, m2m_changed, pre_migrate, post_migrate, class_prepared |
django/db/backends/signals.py |
connection_created |
django/test/signals.py |
template_rendered, setting_changed (re-export) |
django/contrib/auth/signals.py |
user_logged_in, user_logged_out, user_login_failed |
Key abstractions
| Type | File | Role |
|---|---|---|
Signal |
dispatcher.py |
A named event with a list of receivers |
receiver |
dispatcher.py |
Decorator to connect a function to one or more signals |
How it works
graph LR
Sender["sender.send(...)"]
Signal["Signal._live_receivers"]
R1["receiver_1(sender, **kwargs)"]
R2["receiver_2(sender, **kwargs)"]
R3["receiver_3(sender, **kwargs)"]
Sender --> Signal
Signal --> R1
Signal --> R2
Signal --> R3Connection
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=MyModel)
def handler(sender, instance, created, **kwargs):
...@receiver is sugar for signal.connect(handler, sender=...). Connections are stored in a weak-keyed list — receivers are weak-referenced by default so a function going out of scope auto-disconnects.
dispatch_uid
Without precaution, a receiver registered twice runs twice. The dispatch_uid parameter gives a connection a stable ID so duplicate connects are no-ops. Always set dispatch_uid for receivers in modules that may be imported multiple times (e.g., during the autoreloader cycle).
@receiver(post_save, sender=MyModel, dispatch_uid="myapp.handler")
def handler(...):
...Sending
signal.send(sender, **kwargs) walks the receiver list, calling each with the sender and kwargs. Returns a list of (receiver, response) tuples.
signal.send_robust(sender, **kwargs) catches and returns exceptions instead of propagating them. Used by request_started/request_finished to ensure one bad receiver doesn't break the request lifecycle.
signal.asend(...) and signal.asend_robust(...) are async variants. Receivers can be either sync or async; the dispatcher uses asgiref.sync to bridge as needed.
Disconnecting
signal.disconnect(receiver, sender=...) removes a connection. Test code occasionally needs this to undo connections made by ready() hooks.
Built-in signals
Request lifecycle
request_started(environ)— fires when the handler receives a request.request_finished()— fires when the response has been sent.got_request_exception(request)— fires on any unhandled exception in the request cycle.
Model lifecycle
pre_init(sender, args, kwargs)— Python__init__is starting.post_init(sender, instance)— instance is constructed.pre_save(sender, instance, raw, using, update_fields)—save()about to insert/update.post_save(sender, instance, created, raw, using, update_fields)— save complete.pre_delete(sender, instance, using)—delete()about to remove.post_delete(sender, instance, using)— delete complete.m2m_changed(sender, instance, action, reverse, model, pk_set, using)— many-to-many add/remove/clear.class_prepared(sender)— fires once perModelsubclass at class-creation time.
Migrations
pre_migrate(sender, app_config, verbosity, interactive, using, plan, apps)— before applying migrations for an app.post_migrate(...)— after applying.contenttypesandauthuse this to seed permissions and content type rows.
Database
connection_created(sender, connection)— a database connection has been opened. Used to set per-connection settings (e.g.,PRAGMA foreign_keys = ONfor SQLite).
Auth
user_logged_in,user_logged_out,user_login_failed— emitted by the auth views and backends.
Settings
setting_changed— fired byoverride_settingsandsignals.setting_changed.send. Test-only; not for production.
Async signals
Signals support async receivers as of 4.1. Connect a coroutine function and the dispatcher will:
- Call it with
awaitif the sender usedasend(). - Bridge via
async_to_syncif the sender usedsend()(logs a warning the first time).
A signal that's exclusively async should document that requirement; mixed-mode is the norm in the framework.
Patterns
Connect in AppConfig.ready()
The recommended pattern for app-level signal connections:
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
from . import signals # connects receiversThis avoids the import-time side effect of connecting receivers when the model class is defined.
Use dispatch_uid
Especially in ready() — ready() may be called more than once in some test setups, and dispatch_uid ensures idempotence.
Use sender= for narrow signals
Most ORM signals take a sender argument that's the model class. Filter to only the model you care about:
@receiver(post_save, sender=MyModel)
def handler(...): ...A receiver without sender= runs for every model — that's almost never what you want.
Don't use signals as a control flow
Signals are good for cross-cutting concerns (logging, cache invalidation, contenttypes). They're bad for primary control flow — overriding Model.save() or middleware is more legible.
Integration points
- ORM — fires
pre_save/post_save/pre_delete/post_delete/m2m_changed. - Migrations — fires
pre_migrate/post_migrate. - Auth — fires
user_logged_in/user_logged_out/user_login_failed. - Sessions — clears its caches via
setting_changed. - Templates — fires
template_renderedfor each render (used heavily by tests). - Caching — listens to
setting_changedto rebuild backends.
Entry points for modification
- New signal: instantiate
Signal()in a module and document it. Send viasignal.send(sender=..., ...). - Hook into a built-in signal: use
@receiverwithdispatch_uidand connect from your app'sready().
Key source files
| File | Purpose |
|---|---|
django/dispatch/dispatcher.py |
Signal class (~250 lines) |
django/db/models/signals.py |
Built-in model signals |
django/core/signals.py |
Request signals |
django/test/signals.py |
setting_changed, template_rendered |
django/contrib/auth/signals.py |
Auth signals |
Where to read tests
tests/dispatch/— the dispatcher itself.tests/signals/— model signal coverage.tests/migrations/test_signals.py—pre_migrate/post_migrate.tests/auth_tests/test_signals.py— auth signals.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.