Signals
Back in Custom User Model, creating a
Profile automatically whenever a new User registered was left as "the
full picture is covered in Signals" — this lesson delivers on that. Signals are
Django's built-in way to let two parts of your app react to the same event without either one
importing or calling the other directly. The part that creates users doesn't need to know
anything about profiles at all; a signal receiver, registered separately, listens for "a User
was saved" and reacts on its own.
The Problem Signals Solve: Decoupling Reactions from Causes
Without signals, making sure every new User gets a Profile would mean
finding every single place in the codebase that creates a User — the registration
view, a management command, the admin, a data migration — and adding
Profile.objects.create(user=user) to each one by hand. Miss one, and that user
silently has no profile. Signals flip this around: instead of every cause needing to
remember every effect, one receiver function subscribes to the event itself,
post_save on User, and fires no matter which code path actually
triggered it.
Commonly Used Built-in Signals
| Signal | Fires when... |
|---|---|
post_save | A model instance has just been saved — includes a created flag telling you whether this was an insert or an update |
pre_delete | A model instance is about to be deleted, before it's actually removed from the database |
post_delete | A model instance has just been deleted |
m2m_changed | A ManyToMany relationship changes — a tag is added to or removed from a Task's tags field, for example |
All four follow the same shape: a receiver function, connected to a specific signal, optionally
filtered down to one specific sender model so it only fires for that model rather
than every save in the whole project.
A Full Example: Auto-Creating a Profile with post_save
Here's the complete version of the example foreshadowed in
Custom User Model — a receiver connected to
post_save, filtered to only User:
# accounts/signals.py
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
from .models import Profile
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
sender is the model class that sent the signal (User), instance
is the specific row that was just saved, and created is a boolean — True
only on the very first save that inserts the row, False on every later update. That
distinction is exactly what this receiver depends on: without checking created, every
time an existing user's account is merely updated (changing their email, say) would try to create a
second Profile and fail on the OneToOneField's uniqueness constraint.
Where Receivers Live: signals.py and apps.py
The receiver function itself is conventionally placed in a signals.py module inside
the app it belongs to — accounts/signals.py here, since Profile lives in
the accounts app. But defining that function isn't enough on its own: the
@receiver decorator only connects the signal at the moment the module is actually
imported, and nothing imports signals.py automatically. The standard place to trigger
that import is the app's AppConfig.ready() method, in apps.py:
# accounts/apps.py
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'
def ready(self):
import accounts.signals # noqa
ready() is called exactly once, when Django finishes loading the app registry at
startup — after every app's models are available, but before the project starts actually serving
requests. That timing is precisely why it's the right hook for this: import
accounts.signals any earlier, and the User and Profile
models it needs might not be fully loaded yet; import it any later, and the first request could
arrive before the receiver is connected.
A Word of Caution: Signals Can Hide the Story
It's worth being honest about a real trade-off here, one that comes up often among experienced
Django developers: signals make the connection between cause and effect invisible at the call
site. Reading User.objects.create_user(...) in a view gives absolutely no hint that a
Profile row also gets created — you have to already know a signal exists, then go
find accounts/signals.py separately to see the full effect. For something as narrow
and one-directional as "every user gets exactly one profile," that's a reasonable price. But
signals are easy to reach for out of habit, and a codebase where ordinary business logic
("recalculate a project's completion percentage when a task is marked done") is scattered across
a dozen loosely-connected signal receivers becomes genuinely hard to trace and debug — a plain
function call, or overriding a model's own save() method, is usually clearer for
logic that isn't truly cross-cutting. Reserve signals for the cases where decoupling is the actual
goal, not as a default way to react to a save.
create_profile actually fire on
this page. Wire it up in your own TaskFlow project and create a user to watch it run.