PROFESSIONAL DJANGO ENGINEERING SERIES #3
Change your user model after the first migration and you will spend a painful day fixing it. Here is what to do instead — before your first commit.
This is the single piece of Django advice that appears in the official documentation, in every serious Django resource, and from every experienced Django developer who has ever ignored it on an early project and paid the price: create a custom user model before your first migration.
If you are starting a new project today, this article will save you hours. If you are working on an existing project that uses the default user model, this article will help you understand exactly what you are dealing with.
The best time to create a custom user model was before your first commit. The second best time is right now, before you run makemigrations for the first time.
Why the Default User Model Is a Problem?
Django’s default auth.User model makes decisions for you: username is the primary identifier, the field names are fixed, and the model lives in Django’s own package where you cannot modify it. The most common pain point:
- Most modern applications want email as the login identifier. The default model has both a username field and an email field, and uses username for authentication. Working around this requires a custom authentication backend — possible, but inelegant.
- Every field you want to add to the user (bio, avatar, preferences, organisation) requires either a separate Profile model (an extra database join on every user access) or monkey-patching (an anti-pattern).
- Once you have migrations that reference auth.User, changing to a custom model requires either a fresh database or a complex, risky surgical migration.
AbstractUser vs AbstractBaseUser — Which to Choose
AbstractUser: extend the default behaviour
If you are happy with Django’s authentication behaviour and just want to add fields, inherit from AbstractUser. You get everything Django provides, plus the ability to add your own fields:
# apps/users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
"""
Custom user model. Add project-specific fields here.
Even if you add nothing now, this gives you flexibility later.
"""
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
def __str__(self):
return self.email
AbstractBaseUser: full control
If you need fundamentally different logic like authentication, inherit from AbstractBaseUser. You build everything yourself, which gives full control at the cost of more setup.
Email-as-username
If you want to create email-based-authentication system (instead of the default username) it's achievable with both AbstractBaseUser and AbstractUser:
# apps/users/managers.py — email-as-username pattern
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import UserManager
from django.utils.translation import gettext_lazy as _
class EmailBasedUserManager(UserManager):
"""
Custom user model manager where email is the unique identifier
for authentication instead of username.
"""
def create_user(self, email, password, **extra_fields):
"""
create and save new user with the given email and password
"""
if not email:
raise ValueError(_("Email must be set"))
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.password = make_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password, **extra_fields):
"""
create and save new superuser with the given email and password
"""
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get("is_staff") is not True:
raise ValueError(_("Superuser must have is_staff=True."))
if extra_fields.get("is_superuser") is not True:
raise ValueError(_("Superuser must have is_superuser=True."))
return self.create_user(email, password, **extra_fields)
then in models.py:
# apps/users/models.py — email-as-username pattern
# using AbstractUser
from django.contrib.auth.models import AbstractUser
from .managers import EmailBasedUserManager
class User(AbstractUser):
username = None
email = models.EmailField(unique=True)
first_name = models.CharField(max_length=150, blank=True)
last_name = models.CharField(max_length=150, blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = 'email' # email is the login identifier
REQUIRED_FIELDS = [] # email is already required via USERNAME_FIELD
objects = EmailBasedUserManager()
The Three Things You Must Do
- Register it in settings
# config/settings/base.py
AUTH_USER_MODEL = 'users.User'
# This one line must be in place BEFORE running the very first makemigrations.
# Everything in Django resolves 'the user model' through this setting.
- Reference it correctly in code
# In Python code: use get_user_model()
from django.contrib.auth import get_user_model
User = get_user_model() # respects AUTH_USER_MODEL
# In model ForeignKey fields: use settings.AUTH_USER_MODEL
from django.conf import settings
class Order(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, # string reference, not the class
on_delete=models.CASCADE,
)
# NEVER: from django.contrib.auth.models import User
# NEVER: from apps.users.models import User (in ForeignKey fields)
- Update the Admin When you replace the default user model, Django’s built-in UserAdmin breaks because it references the username field. Subclass it and remove the references:
# apps/users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User
@admin.register(User)
class UserAdmin(BaseUserAdmin):
list_display = ('email', 'first_name', 'last_name', 'is_staff')
search_fields = ('email', 'first_name', 'last_name')
ordering = ('-date_joined',)
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {'fields': ('first_name', 'last_name')}),
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
)
add_fieldsets = (
(None, {'classes': ('wide',), 'fields': ('email', 'password1', 'password2')}),
)
⚠ Already past the first migration?
If your project has existing migrations that reference auth.User, you have two options. In development: drop the database, delete all migration files (not Django's own), set AUTH_USER_MODEL, and run makemigrations and migrate fresh. In production with existing data: this is a documented but complex multi-step migration that requires a maintenance window. Django's official documentation covers it, but it is a significant operation.
The lesson for every future project: AUTH_USER_MODEL before the first makemigrations.
Top comments (0)