Django

Custom user models — the decision you must make before your first migration

Django ships a default User model, but swapping it out later means a full database migration for every table with a foreign key to users. AUTH_USER_MODEL has to be decided before the first migrate — one of Django's few genuinely irreversible-if-you-wait defaults.

Advanced

3 min read

The trap: the default User model is hard to change later

from django.contrib.auth import get_user_model
User = get_user_model()   # ALWAYS reference the user model this way, never import User directly
 
# settings.py — must be set BEFORE the first `manage.py migrate` ever runs
AUTH_USER_MODEL = "accounts.User"

Django's built-in django.contrib.auth.models.User covers the common case (username, email, password, is_staff, is_superuser), but real projects frequently need to add fields (a phone number, a profile picture) or change what field is used to log in (email instead of username). The catch: every other model with a ForeignKey(User, ...) — and Django's own internal tables — bakes the user table's name into the database schema at migration time. Switching AUTH_USER_MODEL after those migrations already exist means rewriting migration history across the entire project, not just adding a field. This is why Django's own documentation is blunt about it: decide on a custom user model before the first migrate, even in a brand-new project where it feels premature.

AbstractUser vs AbstractBaseUser: how much do you actually need to replace?

# accounts/models.py — extends the default fields, keeps username/password/permissions logic
from django.contrib.auth.models import AbstractUser
 
class User(AbstractUser):
    phone_number = models.CharField(max_length=20, blank=True)
    is_verified = models.BooleanField(default=False)

AbstractUser keeps everything the default User already provides (username, email, password hashing, permissions, is_staff/is_superuser) and just adds new fields on top — the right choice for the common case of "the default is fine, I just need one or two extra fields." AbstractBaseUser provides almost nothing — no username field, no permissions mixin — and requires implementing the authentication-relevant pieces by hand (typically combined with PermissionsMixin for permissions). Reach for AbstractBaseUser specifically when the login field itself needs to change — most commonly, authenticating by email with no username field at all, which AbstractUser can't cleanly express since it still has username baked in.

Registering the custom model with a manager

class User(AbstractUser):
    email = models.EmailField(unique=True)
    phone_number = models.CharField(max_length=20, blank=True)
 
# settings.py
AUTH_USER_MODEL = "accounts.User"

Beyond the model itself, AUTH_USER_MODEL = "app_label.ModelName" is what tells every part of Django — get_user_model(), the admin, createsuperuser, authentication backends — to use the custom model instead of the built-in one. Forgetting this line is a common early mistake: the custom User model exists and migrates fine, but Django keeps using its own default User everywhere else, since nothing told it not to.

Authentication: authenticate() and login() are two separate steps

from django.contrib.auth import authenticate, login
 
def login_view(request):
    username = request.POST["username"]
    password = request.POST["password"]
    user = authenticate(request, username=username, password=password)  # step 1: verify credentials
    if user is not None:
        login(request, user)   # step 2: attach the user to request.session
        return redirect("dashboard")
    return render(request, "login.html", {"error": "Invalid credentials"})

authenticate() checks credentials against Django's configured authentication backends and returns either a User instance (valid) or None (invalid) — it does not log anyone in; it's a pure check. login() is the separate step that actually establishes the session (covered in the sessions and cookies lesson), attaching the authenticated user to request.user for the rest of that session. Calling login() without first calling authenticate() is a real security bug — it logs in whatever user object is passed, with no credential verification at all, if the calling code doesn't do that verification itself first.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why must AUTH_USER_MODEL be decided before the first `migrate` ever runs?

2. When should you reach for AbstractBaseUser instead of AbstractUser?

3. What does authenticate() do, as distinct from login()?

4. What's the correct way to reference the active user model in application code?