For AI agentsA Markdown index of these docs is at /llms.txt. Every page is also Markdown: add .md to its address.

Agent Rules / Frameworks

Why does my coding agent miss Django signal receivers?

Rule AR-31

Connect Django signals with an explicit sender, and keep the receivers in one signals.py per app, imported in AppConfig.ready().

A receiver has no caller in the source. Django calls it when a model is saved, so a search for who uses the function finds only its definition. Name the sender in the decorator, put every receiver of an app in one file, and import that file from ready(). Then a reader, a type checker or an agent can answer two questions from the source alone: which model triggers this function, and where does it get connected.

Why it happens

Django's signal system does its job well. It decouples the code that saves a model from the code that reacts, send() runs receivers in a fixed order, and the @receiver decorator keeps the registration next to the function. The cost is that the link between sender and receiver lives in Django's registry, not in a call a tool can follow.

The signals topic in the Django docs says what the registry does with each choice:

  • Where receivers live. "Strictly speaking, signal handling and registration code can live anywhere you like, although it's recommended to avoid the application's root module and its models module to minimize side-effects of importing code." Receivers belong in a signals submodule, connected in ready(): "If you're using the receiver() decorator, import the signals submodule inside ready(), this will implicitly connect signal receivers."
  • **What sender does.** For @receiver(pre_save, sender=MyModel), the docs say the function "will only be called when an instance of MyModel is saved." Leave sender out and the receiver is connected to every sender of that signal, so the function has to filter at runtime.
  • **When ready() runs.** The applications reference says it is "called as soon as the registry is fully populated", the third and final stage of initialization. It also warns that ready "might be called more than once" in tests that change installed apps, so registration should be idempotent.
  • Order. "All of the signal's receiver functions are called one at a time, in the order they were registered." Registration order follows import order, so a receiver connected as a side effect of importing models.py is placed by whatever imports models.py first.
  • Errors. send() "does not catch any exceptions raised by receivers; it simply allows errors to propagate." A receiver that breaks after a refactor fails the save() call that triggered it. CH-01 shows that failure end to end.

The agent's problem follows from this. Asked to change notify_warehouse(order), it searches for callers and finds none in the project. It has no way to know that every Order.save() calls it, or that a receiver without sender also runs for models the agent never opened.

Bad and good

An order service notifies the warehouse after an order is saved.

python# bad: orders/models.py from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver class Order(models.Model): status = models.CharField(max_length=20) @receiver(post_save) # connected for every model's save def notify_warehouse(sender, instance, created, **kwargs): if sender.__name__ != "Order": # runtime filter by name return if created: warehouse.send(instance.pk)

Three things hide here. The receiver is connected to every model's post_save. The filter compares a class name as a string. The receiver sits in models.py, which the Django docs say to avoid for registration code. An agent that renames Order finds the string "Order" only by text search, and the receiver quietly stops matching.

python# good: orders/signals.py from typing import Any from django.db.models.signals import post_save from django.dispatch import receiver from orders import warehouse from orders.models import Order @receiver(post_save, sender=Order) def notify_warehouse( sender: type[Order], instance: Order, created: bool, **kwargs: Any ) -> None: if created: warehouse.send(instance.pk)
python# good: orders/apps.py from django.apps import AppConfig class OrdersConfig(AppConfig): name = "orders" def ready(self) -> None: # Importing the module connects the @receiver functions in it. from orders import signals # noqa: F401

models.py no longer mentions signals. sender=Order is a reference to the class, so a rename tool updates it and a search for Order lists the receiver file. All receivers of the app are in one file, so "what reacts to saves in this app" is one file to open.

Trade-offs and when the other way is fine

  • The decorator keeps things short. @receiver is the most readable form Django offers. The rule asks for one more argument and one more file, not a different mechanism.
  • A receiver with no sender is right for cross-cutting work. Audit logging for every model, or a metrics hook on request_finished, should not name a sender. Keep those in a signals.py that says so in a comment.
  • Signals are the wrong tool when you own both sides. If a view saves an order and sends one email, calling send_confirmation(order) from the view is a plain call that every tool follows. Use signals when the sender can't know about the receiver, as with third-party apps.
  • **ready() adds a file and a few lines per app.** That is the whole cost.
  • **Importing signals inside ready() hides it from a plain top-of-file import scan.** The noqa: F401 comment and the name signals are the convention. A comment in apps.py naming the receivers' file helps.

Enforce it

ToolSettingWhat it catchesChecked
ruffDJ013 non-leading-receiver-decorator (select the DJ family; the rule is not marked preview, and ruff's default rule set lists no DJ rule, so it is off until selected: https://docs.astral.sh/ruff/default-rules/)a decorator placed above @receiver; the docs say @receiver "does not return a wrapped function", so receivers listed after another decorator "will not be connected"2026-10-30
Django system checksignals.E001 (run python manage.py check)a receiver connected with a string sender app_label.Model where the app isn't installed or has no such model2026-10-30
Djangodispatch_uid on connect()duplicate registration of a bound-method receiver; module-level functions are registered once already2026-10-30
Djangoweak=False on connect()a local-function receiver being garbage collected, since receivers are stored as weak references by default2026-10-30

No ruff or Django check in the pages read flags a @receiver without sender. That one is a review rule, and the AGENTS.md block below is how an agent learns it.

Rule block for AGENTS.md / CLAUDE.md

markdown## Django signals - Every @receiver names its sender: @receiver(post_save, sender=Order). A receiver with no sender needs a comment saying why it listens to all senders. - Receivers live in <app>/signals.py. Nothing in models.py or the app's __init__.py connects signals. - apps.py imports the signals module inside AppConfig.ready(). - @receiver is the first decorator above the function. - Before changing a receiver's signature or deleting it, search for the signal name and the sender class. Callers of the receiver itself are not in the source. - Don't call handler functions by names built from data (getattr, f-strings). Use an explicit match (AR-07, AR-08).

Edge cases

  • Bound-method receivers. Connecting a method of an instance that is created again registers another receiver. Pass dispatch_uid (the docs: "The receiver will only be bound to the signal once for each unique dispatch_uid value").
  • Receivers defined inside a function. The docs: "if your receiver is a local function, it may be garbage collected." Define it at module level, or pass weak=False.
  • String senders. The applications reference allows pre_save.connect(receiver, sender="app_label.MyModel") inside ready(). Use it to avoid an import cycle between apps; the signals.E001 check covers a wrong label.
  • **A plain "orders" entry in INSTALLED_APPS.** For an app listed by package name, Django uses the one AppConfig subclass it finds in apps.py ("if Django finds exactly one AppConfig subclass in the apps.py submodule, it uses that configuration"), and then "runs the ready() method of each application configuration". With no subclass, the base AppConfig is used and its ready() does nothing; with several, the one with default = True is chosen. https://docs.djangoproject.com/en/stable/ref/applications/
  • Async receivers. The docs say receivers may be async def, and "an asynchronous receiver registered before a synchronous receiver may be executed after the synchronous receiver", so don't rely on registration order across the two.
  • Tests. ready() may run more than once during testing. Module-level function receivers connected by import are safe.
  • Dispatch by status inside a receiver. A post_save receiver that routes by a status field belongs to AR-07 and AR-08 as well. See CH-01.

What LLMGRAPH shows

A function connected with the receiver decorator prints 'no calls to it found in this repo'; LLMGRAPH does not mark it as an entry point.

References

Django (fetched 2026-10-30 from the stable docs):

  • Signals topic (where to put receivers, ready(), @receiver(sender=...), dispatch_uid, weak references, call order, send() errors): https://docs.djangoproject.com/en/stable/topics/signals/
  • Applications reference (AppConfig.ready(), initialization stages, string sender labels, ready() more than once): https://docs.djangoproject.com/en/stable/ref/applications/
  • System check framework (signals.E001): https://docs.djangoproject.com/en/stable/ref/checks/

Ruff (fetched 2026-10-30):

  • DJ013: https://docs.astral.sh/ruff/rules/django-non-leading-receiver-decorator/ · rule list: https://docs.astral.sh/ruff/rules/

Chimera: CH-01.

Changelog

  • 2026-10-30: draft 1.
  • 2026-10-30: docs checks resolved
  • 2026-10-30: remaining checks closed (claims reduced to documented behaviour)
  • AR-07 Why does my coding agent miss calls made through getattr?
  • AR-08 How do I write dispatch that type checkers can follow?
  • AR-25 What to check before changing a function signature?
  • AR-32 Do string model references hide dependencies in Django?
  • CH-01 Why does my Django signal break after a refactor?

Chimeras that use this rule

  • CH-01 Why does my Django signal break after a refactor?

Last verified

For agentsthis page as Markdown/agent-readable/ar-31.mdllms.txt