Customizing User Models in Django: Do it Early

django python

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:

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.

All posts