Open-Source Wikis

/

Django

/

Systems

/

Signals and dispatch

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 --> R3

Connection

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 per Model subclass 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. contenttypes and auth use 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 = ON for SQLite).

Auth

  • user_logged_in, user_logged_out, user_login_failed — emitted by the auth views and backends.

Settings

  • setting_changed — fired by override_settings and signals.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 await if the sender used asend().
  • Bridge via async_to_sync if the sender used send() (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 receivers

This 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_rendered for each render (used heavily by tests).
  • Caching — listens to setting_changed to rebuild backends.

Entry points for modification

  • New signal: instantiate Signal() in a module and document it. Send via signal.send(sender=..., ...).
  • Hook into a built-in signal: use @receiver with dispatch_uid and connect from your app's ready().

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.pypre_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.

Signals and dispatch – Django wiki | Factory