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

SignalFires when...
post_saveA model instance has just been saved — includes a created flag telling you whether this was an insert or an update
pre_deleteA model instance is about to be deleted, before it's actually removed from the database
post_deleteA model instance has just been deleted
m2m_changedA 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.

Note: This is correct, real signal-handling code — but this website's own project doesn't run a Django server, so you won't see create_profile actually fire on this page. Wire it up in your own TaskFlow project and create a user to watch it run.
Next up: Signals are one way TaskFlow can react to events automatically. Sending Email covers a concrete thing worth reacting with — notifying a user the moment they're assigned a new task.