skip to content

In Django, how would you create a Profile row automatically whenever a new user signs up, using a post_save receiver?

level: juniorimportance: must knowfreq 72%

answer

  1. signal fired at the end of save()
  2. filter by the user model as sender
  3. the created flag
  4. skip fixture loads via raw

basics

~10 s

Connect a receiver to post_save with sender=settings.AUTH_USER_MODEL and create the Profile only when the created argument is True, so later saves of the same user do not insert a second profile.

solid answer

~30 s

I write a function decorated with `@receiver(post_save, sender=settings.AUTH_USER_MODEL)` that takes `sender`, `instance`, `created` and `**kwargs`. `post_save` fires at the end of every `Model.save()`, so I guard with `if created:` and then call `Profile.objects.create(user=instance)`. I also skip when `raw` is True, because `loaddata` sends the signal with `raw=True` and the fixture usually carries its own profiles. The receiver lives in the app's `signals.py` and is imported from `AppConfig.ready()`, otherwise it is never connected. Because `create_user()` calls `save()`, every sign-up path that goes through the model gets a profile, but a bulk insert does not.

code

python · 12 lines
python
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver

from .models import Profile


@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_profile(sender, instance, created, raw, **kwargs):
    if raw or not created:
        return
    Profile.objects.create(user=instance)

go deeper

for a junior

Recall the receiver signature, sender, instance, created and **kwargs, and why the created check prevents a second profile on later saves.

for a middle

Explain the lazy string sender, the raw flag during loaddata, and why the receiver only exists once ready() imports its module.

for a senior

Name the paths that create users without save(), such as bulk_create and raw SQL, and how you detect and backfill users that ended up with no profile.

for a principal

Weigh the signal against one explicit call in the sign-up service: the signal covers users created from the admin and third-party packages, the explicit call is easier to trace and test.

## What the post_save signal is Django's **signal dispatcher** lets code register a function, called a **receiver**, that Django calls when something happens. The model system defines several signals in `django.db.models.signals`; `post_save` is sent at the very end of `Model.save()`, after the `INSERT` or `UPDATE` query has run. The receiver is called **synchronously**, in the same thread as the code that called `save()`, before `save()` returns. Every receiver is called with keyword arguments: - `sender` — the model class being saved. - `instance` — the actual object that was saved. - `created` — `True` if a new row was inserted, `False` if an existing row was updated. - `raw` — `True` when the instance is saved exactly as presented, which is what `loaddata` does with fixtures. - `using` — the database alias. - `update_fields` — the set passed to `save(update_fields=...)`, or `None`. A receiver must accept `**kwargs`; with `DEBUG` on, Django raises `ValueError` at connect time if it does not. ## The receiver for a sign-up profile ```python # accounts/signals.py from django.conf import settings from django.db.models.signals import post_save from django.dispatch import receiver from .models import Profile @receiver(post_save, sender=settings.AUTH_USER_MODEL) def create_profile(sender, instance, created, raw, **kwargs): if raw or not created: return Profile.objects.create(user=instance) ``` Three details carry the answer: 1. **`sender=settings.AUTH_USER_MODEL`** — model signals accept the sender as an `"app_label.ModelName"` string and resolve it lazily, which avoids importing a custom user model at module load and keeps the receiver correct for a swapped user model. 2. **`if created`** — `post_save` fires on *every* save, including a password change or a `last_login` update. Without the guard the second save tries to insert a second profile and fails on the one-to-one uniqueness constraint. 3. **`raw`** — when fixtures are loaded, the user and the profile both come from the fixture; creating another profile in the receiver collides with the fixture's own row. ## Connecting it A decorated function is only connected when its module is imported. The documented place is the app configuration: ```python # accounts/apps.py from django.apps import AppConfig class AccountsConfig(AppConfig): name = "accounts" def ready(self): from . import signals # noqa: F401 connects the receivers ``` If nothing imports `signals.py`, the code looks right and simply never runs, which is the most common "my signal does not fire" bug. ## Which sign-up paths trigger it | Path | Receiver runs? | |---|---| | `User.objects.create_user(...)` | Yes — it calls `save()` | | A registration `ModelForm.save()` | Yes | | The admin "Add user" page | Yes | | `createsuperuser` | Yes, it goes through the manager and `save()` | | `User.objects.bulk_create([...])` | **No** — no `pre_save`/`post_save` | | `loaddata` of a fixture | Yes, with `raw=True` (skipped by the guard) | ## When a signal is the wrong tool for this The receiver is a classic interview answer, but the Django docs themselves warn that signals make code harder to follow and suggest a manager method or an overridden method first. If sign-up happens in exactly one service function in your own code, calling `Profile.objects.create(user=user)` right there is easier to read and to test. A signal earns its place when users are created from places you do not control — the admin, a third-party auth package, a management command — and every one of them should end up with a profile. A defensive alternative some teams add is a lazy `get_or_create` of the profile on first access, which also repairs users created by a bulk path. ## Traps to mention - Accessing `instance.profile` inside the same receiver before creating it raises `RelatedObjectDoesNotExist`. - The receiver runs inside the caller's transaction if one is open; an exception in it propagates out of `save()`. - Calling `instance.save()` inside its own `post_save` receiver sends `post_save` again; without a guard that stops the second pass, the receiver recurses until Python gives up.

  • Why pass sender=settings.AUTH_USER_MODEL rather than importing User?
    Model signals accept an `"app_label.ModelName"` string and resolve it lazily once the app registry is ready. That keeps the receiver working for a custom user model, avoids importing `django.contrib.auth.models.User` when the project swapped it, and sidesteps circular imports between the accounts app and the user model's app.
  • Some users in production have no profile. How can that happen with this receiver in place?
    Anything that inserts users without `Model.save()` skips `post_save`: `bulk_create`, raw SQL, a data migration writing rows directly. A receiver that was not connected because `signals.py` was never imported has the same effect. Fix the data with a one-off backfill and make profile access tolerant, for example with `Profile.objects.get_or_create(user=user)`.

saying these in an interview costs you the question

  • Creates the profile on every post_save without checking created
  • Puts the receiver in signals.py but never imports it anywhere
  • Believes bulk_create of users also triggers post_save
  • Thinks post_save runs later in a background worker
  • Imports django.contrib.auth.models.User even though the project uses a custom user model