Customizing User Models in Django: Do it Early
The advice is simple: Always use a custom user model, even if you don’t need customization yet. Here’s why and how.
Why Custom from Day One
The Problem
Django’s AUTH_USER_MODEL is hardcoded into your database schema:
# This creates foreign keys to auth.User
class Article(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
Changing the user model after you have data requires:
- Complex migrations
- Data migration of all foreign keys
- High risk of failures
The Solution
Start with a custom user model, even if it’s identical to the default:
# accounts/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
# settings.py
AUTH_USER_MODEL = 'accounts.User'
Now you can add fields later without migration headaches.
Setting Up a Custom User Model
Option 1: Extend AbstractUser
Keep Django’s default fields, add your own:
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
# Additional fields
phone = models.CharField(max_length=20, blank=True)
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True)
# Settings
email_verified = models.BooleanField(default=False)
def __str__(self):
return self.email or self.username
Option 2: AbstractBaseUser (Full Control)
Build from scratch:
# accounts/models.py
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.db import models
from django.utils import timezone
class UserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('Email is required')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
return self.create_user(email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin):
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(default=timezone.now)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
def get_full_name(self):
return f'{self.first_name} {self.last_name}'.strip()
def get_short_name(self):
return self.first_name
Configuration
Settings
# settings.py
AUTH_USER_MODEL = 'accounts.User'
# If using email as username
AUTHENTICATION_BACKENDS = [
'accounts.backends.EmailBackend',
'django.contrib.auth.backends.ModelBackend',
]
Custom Authentication Backend
# accounts/backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
UserModel = get_user_model()
# Try email
email = kwargs.get('email') or username
try:
user = UserModel.objects.get(email=email)
except UserModel.DoesNotExist:
return None
if user.check_password(password) and self.user_can_authenticate(user):
return user
return None
Admin Configuration
# accounts/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):
# Fields to display in list view
list_display = ['email', 'first_name', 'last_name', 'is_staff', 'is_active']
list_filter = ['is_staff', 'is_active', 'date_joined']
search_fields = ['email', 'first_name', 'last_name']
ordering = ['-date_joined']
# For email-based user (AbstractBaseUser)
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {'fields': ('first_name', 'last_name')}),
('Permissions', {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
}),
('Important dates', {'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2'),
}),
)
Referencing Users Correctly
In Models
from django.conf import settings
class Article(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL, # Not 'auth.User'
on_delete=models.CASCADE
)
In Code
from django.contrib.auth import get_user_model
User = get_user_model()
# Query users
users = User.objects.filter(is_active=True)
In Forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm
User = get_user_model()
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = User
fields = ('email',)
Common Patterns
Profile Model (Separate)
class Profile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='profile'
)
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True)
location = models.CharField(max_length=100, blank=True)
# Auto-create profile
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
Proxy Models
class AdminUser(User):
class Meta:
proxy = True
def has_admin_access(self):
return self.is_staff and self.is_active
class CustomerManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_staff=False)
class Customer(User):
objects = CustomerManager()
class Meta:
proxy = True
Migration from Default User
If you didn’t start custom (don’t do this if you can avoid it):
# 1. Create custom user identical to default
class User(AbstractUser):
class Meta:
db_table = 'auth_user' # Use existing table
# 2. Fake migrations
python manage.py makemigrations accounts
python manage.py migrate accounts --fake-initial
# 3. Update all ForeignKeys
# This is the painful part
This is error-prone. Start custom from the beginning.
Final Thoughts
The 5 minutes it takes to set up a custom user model saves hours of migration pain later.
Every Django project should start with:
# accounts/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
# settings.py
AUTH_USER_MODEL = 'accounts.User'
Before first migration. No exceptions.
The best migration is the one you never have to write.