07 · The Custom User Model¶
Django's documentation gives one piece of advice more emphatically than almost any other: if you're starting a new project, set up a custom user model, even if the default one is sufficient for now. This lesson explains why, sets one up properly, and shows exactly what goes wrong when a project tries to switch after its first migration. We ran both scenarios on Django 6.1.1.
Why bother if the default works?¶
The default django.contrib.auth.models.User has a fixed set of fields: username,
first_name, last_name, email, password, is_staff, is_active,
is_superuser, last_login, date_joined, plus groups and permissions. Sooner or later
most products want to change something about it: make email unique, log in by email,
add a display name or a time zone, drop first_name/last_name in favour of one name
field.
You can always add a separate Profile model with a one-to-one link (lesson 01). But
some changes, such as uniqueness of email, the login field and the table itself, can only
be made on the user model. And AUTH_USER_MODEL, the setting that says which model is
the user, is referenced by the very first migrations of admin, auth and every app
with a foreign key to users. It's effectively fixed once the database exists. Starting
with your own subclass costs ten lines and keeps every option open.
Setting it up (before the first migrate)¶
Create an app for it:
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
email = models.EmailField("email address", unique=True)
display_name = models.CharField(max_length=80, blank=True)
def __str__(self):
return self.display_name or self.username
Register it with the admin, reusing Django's UserAdmin (which handles password
hashing and the two-step add form):
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
@admin.register(User)
class CustomUserAdmin(UserAdmin):
fieldsets = UserAdmin.fieldsets + (("Profile", {"fields": ["display_name"]}),)
add_fieldsets = UserAdmin.add_fieldsets + ((None, {"fields": ["email", "display_name"]}),)
list_display = ["username", "email", "display_name", "is_staff"]
Then the first migrations:
On our run, accounts.0001_initial was applied before admin.0001_initial, and
sqlmigrate admin 0001 showed the admin log table pointing at the new model:
REFERENCES "accounts_user".
The model behaves as you'd hope:
>>> U = get_user_model(); U, U._meta.db_table
(<class 'accounts.models.User'>, 'accounts_user')
>>> U.objects.create_user("ana", email="ana@example.com", password="...")
<User: ana>
>>> U.objects.create_user("ana2", email="ana@example.com", password="...")
IntegrityError: UNIQUE constraint failed: accounts_user.email
The database now enforces unique emails. (Forms should catch it first with a friendly
message; the constraint is the guarantee.) Be aware that unique=True is
case-sensitive on most databases, so Ana@example.com and ana@example.com are
different values. Normalise emails to lower case on save, or add a unique constraint on
Lower("email").
AbstractUser vs AbstractBaseUser¶
AbstractUser: the full default user, to which you add fields. Right for almost everyone.AbstractBaseUser(+PermissionsMixin): only password and last-login handling. You define every other field, setUSERNAME_FIELD,REQUIRED_FIELDS, and write a custom manager withcreate_userandcreate_superuser. Choose it only when the default fields genuinely don't fit (e.g. no username at all, login by phone number).
To log in by email with AbstractUser, you can set USERNAME_FIELD = "email" and
REQUIRED_FIELDS = ["username"] (or remove username, which requires a custom manager).
Do it before the first migration.
Referencing the user model¶
Never import User from django.contrib.auth.models in a project with a custom user.
When we tried to use the default class after the swap:
The right references:
| Where | Use |
|---|---|
| Model fields | models.ForeignKey(settings.AUTH_USER_MODEL, ...) (a string, safe at import time) |
| Everywhere else (views, forms, tests) | from django.contrib.auth import get_user_model; User = get_user_model() |
| Built-in forms | UserCreationForm and UserChangeForm work with custom models that keep AbstractUser's fields; subclass them and set Meta.model if you added required fields |
Writing reusable apps this way also means they work in other people's projects, whatever user model those use.
What happens if you switch late¶
We simulated the common situation: a project that has already run migrate with the
default user, then adds accounts.User and sets AUTH_USER_MODEL.
First, if you define a model that subclasses AbstractUser but forget the setting,
the system check stops everything:
accounts.User.groups: (fields.E304) Reverse accessor 'Group.user_set' for
'accounts.User.groups' clashes with reverse accessor for 'auth.User.groups'.
(Both user models are active and both want group.user_set.) With the setting in place,
makemigrations accounts succeeded, but migrate on the existing database failed:
django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.0001_initial
is applied before its dependency accounts.0001_initial on database 'default'.
The admin's first migration depends on whatever AUTH_USER_MODEL is. It was applied
long ago against auth.User, so the history is now contradictory. There is no
makemigrations fix for this.
Your options on a live project:
- Development only: delete the database and migrations, start again.
- Production: a careful manual migration. The common recipe is to create the new
model reusing the existing table (
db_table = "auth_user"), fake its initial migration, update thedjango_content_typerow and the recorded migration history, then make further changes normally. It's fiddly, must be rehearsed on a copy of production, and is easy to get wrong. Many teams instead keep the default user and put new fields on aProfilemodel.
That cost, against ten lines on day one, is the whole argument.
How It Actually Works¶
AUTH_USER_MODEL makes the user model swappable. auth.User declares
swappable = "AUTH_USER_MODEL" in its Meta. When the setting names another model, the
app registry marks auth.User as swapped out: it creates no table and its manager is
replaced by a descriptor that raises the AttributeError we saw.
Migrations reference the user through migrations.swappable_dependency(settings.AUTH_USER_MODEL),
which resolves at migrate time to "the first migration of whichever app holds the user
model". That's how admin.0001_initial came to depend on accounts.0001_initial in the
new project and on auth.0001_initial in the old one, and why the recorded history
can't be reconciled automatically. Foreign keys declared with
settings.AUTH_USER_MODEL are stored in migrations as the setting reference, not a
fixed model, so the same migration files work in projects with different user models.
get_user_model() simply looks up settings.AUTH_USER_MODEL in the app registry, which
is why it must not be called at module import time in models.py (the registry may not
be ready); the string form exists for that case.
Common mistakes¶
- Running
migratebefore creating the custom user, then discovering it's too late. from django.contrib.auth.models import Userin app code.- Forgetting
AUTH_USER_MODELafter writing the model (the E304 clash). - Case-sensitive unique emails letting
Ana@andana@register twice. - Choosing
AbstractBaseUserwhenAbstractUserplus a field would do, and then re-implementing the manager, admin and forms. - Putting frequently changing profile data on the user (preferences, avatars); it's
fine on a related
Profilemodel, which keeps the user table small.
Exercise¶
- Start a new project with an
accounts.User(AbstractUser)model before the firstmigrate. Add adisplay_nameand a unique email. - Register it with a
UserAdminsubclass and create a superuser. - Write a sign-up form subclassing
UserCreationFormwithMeta.model = Userandfields = ["username", "email", "display_name"]; lower-case the email inclean_email(). - In a scratch copy, reproduce the
InconsistentMigrationHistoryerror by switching user models after migrating. Read the error, then delete the scratch copy.