Django: Change User Model

Django: Change User Model

Three weeks into my Django project, I realized I needed to add a phone number field to the User model. Simple, right? Just add the field and migrate.

Wrong.

Django’s default User model is hard to customize after you’ve created your first migrations. I tried to switch to a custom user model and got:

ValueError: Dependency on app with no migrations: accounts
auth.0001_initial depends on accounts.CustomUser but accounts has no migrations.

Then I tried to create migrations:

python manage.py makemigrations
SystemCheckError: AUTH_USER_MODEL refers to model 'accounts.CustomUser' 
that has not been installed

The solution? I had to start over. Drop the database. Delete all migrations. Start fresh with a custom user model from the beginning.

“If you’re starting a new project, it’s highly recommended to set up a custom user model, even if the default User model is sufficient for you.”

The reason: Changing user models after you’ve created migrations is extremely difficult. It requires:

  • Dropping the database
  • Deleting all migrations
  • Recreating everything
  • Losing all data (unless you write complex data migrations)

Or you live with the default User model forever and use a separate Profile model for extra fields.

Common Custom User Model Mistakes

Mistake 1: Creating Custom User After Migrations

Problem:

# You already ran initial migrations
python manage.py migrate

# Now you try to add custom user:
# accounts/models.py
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    phone = models.CharField(max_length=20)

# settings.py
AUTH_USER_MODEL = 'accounts.CustomUser'

# Try to migrate:
python manage.py makemigrations
# Error: Dependencies broken!

What happens: Django’s auth system already created tables using the default User model. You can’t just switch to a different model mid-project.

The only real fix:

# Nuclear option - start over
rm db.sqlite3
find . -path "*/migrations/*.py" -not -name "__init__.py" -delete
find . -path "*/migrations/*.pyc" -delete

# Now create migrations fresh:
python manage.py makemigrations
python manage.py migrate

This deletes all your data. In production, this isn’t an option.

Mistake 2: Wrong Base Class

The Problem:

# accounts/models.py
from django.db import models

# ❌ Wrong - inheriting from models.Model
class CustomUser(models.Model):
    email = models.EmailField(unique=True)
    password = models.CharField(max_length=128)
    # Missing all the auth functionality!

What happens: Your user model doesn’t have Django’s authentication methods. No check_password(), no is_authenticated, no groups or permissions.

The fix:

# ✅ Correct - inherit from AbstractUser or AbstractBaseUser
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    # Keeps all Django auth functionality
    # Add your custom fields
    phone = models.CharField(max_length=20, blank=True)
    date_of_birth = models.DateField(null=True, blank=True)

Mistake 3: Not Setting AUTH_USER_MODEL

The problem:

# You created CustomUser model but forgot to tell Django:

# accounts/models.py
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    phone = models.CharField(max_length=20)

# settings.py - Missing AUTH_USER_MODEL!

What happens: Django still uses the default User model. Your CustomUser table gets created but nothing uses it.

The fix:

# settings.py
AUTH_USER_MODEL = 'accounts.CustomUser'  # ✅ Add this

# Format: 'app_name.ModelName'

Mistake 4: Using get_user_model() Incorrectly

The problem:

# models.py
from django.contrib.auth.models import User  # ❌ Hard-coded

class Post(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)  # ❌ Wrong

What happens: If you switch to a custom user model later, this foreign key points to the wrong model.

The fix:

# models.py
from django.conf import settings

class Post(models.Model):
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,  # ✅ Correct
        on_delete=models.CASCADE
    )

Or in code (not models):

from django.contrib.auth import get_user_model

User = get_user_model()  # ✅ Gets whatever AUTH_USER_MODEL is set to

# Now use User normally:
user = User.objects.get(email='test@example.com')

Mistake 5: Wrong Manager in CustomUser

The problem:

from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    email = models.EmailField(unique=True)
    
    # ❌ Missing: USERNAME_FIELD and required field settings

What happens: You can’t create users with User.objects.create_user() or login fails because Django doesn't know which field is the username.

The fix:

from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager

class CustomUserManager(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 CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    first_name = models.CharField(max_length=30, blank=True)
    last_name = models.CharField(max_length=30, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    date_joined = models.DateTimeField(auto_now_add=True)
    
    objects = CustomUserManager()  # ✅ Custom manager
    
    USERNAME_FIELD = 'email'  # ✅ Login with email
    REQUIRED_FIELDS = []  # ✅ No extra required fields for createsuperuser
    
    def __str__(self):
        return self.email

The Right Way: Custom User From Start

Step 1: Create New Django Project

django-admin startproject myproject
cd myproject
python manage.py startapp accounts

Step 2: Create Custom User Model FIRST

Before running any migrations:

# accounts/models.py
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    """
    Custom user model that extends Django's default User.
    Add custom fields here from the start.
    """
    # Example custom fields:
    phone = models.CharField(max_length=20, blank=True)
    date_of_birth = models.DateField(null=True, blank=True)
    bio = models.TextField(blank=True)
    
    def __str__(self):
        return self.email or self.username

Step 3: Configure Settings

# settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'accounts',  # ✅ Your accounts app
]

# ✅ Set custom user model BEFORE first migration
AUTH_USER_MODEL = 'accounts.CustomUser'

Step 4: Create and Run Migrations

# Now create migrations
python manage.py makemigrations

# Output should show:
# Migrations for 'accounts':
#   accounts/migrations/0001_initial.py
#     - Create model CustomUser

# Run migrations
python manage.py migrate

# Create superuser (tests that it works)
python manage.py createsuperuser

Step 5: Update Admin

# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser

@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
    # Add custom fields to admin
    fieldsets = UserAdmin.fieldsets + (
        ('Custom Fields', {'fields': ('phone', 'date_of_birth', 'bio')}),
    )
    add_fieldsets = UserAdmin.add_fieldsets + (
        ('Custom Fields', {'fields': ('phone', 'date_of_birth', 'bio')}),
    )

Using Email as Username

Many projects want users to login with email instead of username:

# accounts/models.py
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.db import models

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('Email address 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)
        
        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)

class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    first_name = models.CharField(max_length=30, blank=True)
    last_name = models.CharField(max_length=30, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    date_joined = models.DateTimeField(auto_now_add=True)
    
    # Custom fields
    phone = models.CharField(max_length=20, blank=True)
    
    objects = CustomUserManager()
    
    USERNAME_FIELD = 'email'  # Login with email
    REQUIRED_FIELDS = []  # createsuperuser will prompt for email only
    
    class Meta:
        verbose_name = 'user'
        verbose_name_plural = 'users'
    
    def __str__(self):
        return self.email
    
    def get_full_name(self):
        return f'{self.first_name} {self.last_name}'.strip()
    
    def get_short_name(self):
        return self.first_name

Migrating Existing Project to Custom User

If you already have migrations, here’s the painful process:

Option 1: Start Fresh (Development Only)

# WARNING: Deletes all data!

# 1. Backup data if needed
python manage.py dumpdata > backup.json

# 2. Delete database
rm db.sqlite3

# 3. Delete all migrations
find . -path "*/migrations/*.py" -not -name "__init__.py" -delete

# 4. Create custom user model
# (See examples above)

# 5. Set AUTH_USER_MODEL in settings
AUTH_USER_MODEL = 'accounts.CustomUser'

# 6. Create fresh migrations
python manage.py makemigrations
python manage.py migrate

# 7. Create superuser
python manage.py createsuperuser

Option 2: Use Profile Model (Easier)

If you can’t start over, use a separate Profile model:

# accounts/models.py
from django.contrib.auth.models import User
from django.db import models

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
    phone = models.CharField(max_length=20, blank=True)
    date_of_birth = models.DateField(null=True, blank=True)
    bio = models.TextField(blank=True)
    
    def __str__(self):
        return f'{self.user.username} Profile'

# Auto-create profile when user is created
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        UserProfile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

Usage:

# Access profile:
user = User.objects.get(username='john')
phone = user.profile.phone

# Update profile:
user.profile.phone = '555-1234'
user.profile.save()

Option 3: Data Migration (Advanced)

For production with existing data:

# Create empty migration
python manage.py makemigrations accounts --empty

# Edit the migration file:
from django.db import migrations
from django.contrib.auth.models import User

def migrate_users_to_custom(apps, schema_editor):
    # This is complex and error-prone
    # Only attempt if you know what you're doing
    pass

class Migration(migrations.Migration):
    dependencies = [
        ('accounts', '0001_initial'),
        ('auth', '0012_alter_user_first_name_max_length'),
    ]
    
    operations = [
        migrations.RunPython(migrate_users_to_custom),
    ]

Forms and Authentication

Update your forms to use custom user:

# accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model

User = get_user_model()

class CustomUserCreationForm(UserCreationForm):
    email = forms.EmailField(required=True)
    phone = forms.CharField(max_length=20, required=False)
    
    class Meta:
        model = User
        fields = ('username', 'email', 'phone', 'password1', 'password2')
    
    def save(self, commit=True):
        user = super().save(commit=False)
        user.email = self.cleaned_data['email']
        if commit:
            user.save()
        return user

Testing With Custom User

# tests.py
from django.test import TestCase
from django.contrib.auth import get_user_model

User = get_user_model()

class UserTests(TestCase):
    def test_create_user(self):
        user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass123'
        )
        
        self.assertEqual(user.email, 'test@example.com')
        self.assertTrue(user.check_password('testpass123'))
        self.assertTrue(user.is_active)
        self.assertFalse(user.is_staff)
        self.assertFalse(user.is_superuser)
    
    def test_create_superuser(self):
        admin = User.objects.create_superuser(
            username='admin',
            email='admin@example.com',
            password='admin123'
        )
        
        self.assertEqual(admin.email, 'admin@example.com')
        self.assertTrue(admin.is_staff)
        self.assertTrue(admin.is_superuser)

Quick Reference

AbstractUser vs AbstractBaseUser

Use AbstractUser when:
You want to keep username, first_name, last_name fields
You just want to add extra fields
You want Django’s default behavior
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    phone = models.CharField(max_length=20)
Use AbstractBaseUser when:
You want complete control
You want to use email instead of username
You want to remove default fields
from django.contrib.auth.models import AbstractBaseUser

class CustomUser(AbstractBaseUser):
    email = models.EmailField(unique=True)
    # Define everything yourself

Always Use get_user_model()

# ❌ Don't do this:
from django.contrib.auth.models import User

# ✅ Do this in code:
from django.contrib.auth import get_user_model
User = get_user_model()

# ✅ Do this in models:
from django.conf import settings
ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

Deployment Checklist

Before deploying with custom user:

  • Custom user model created
  • AUTH_USER_MODEL set in settings
  • All models use settings.AUTH_USER_MODEL for foreign keys
  • Custom admin configuration added
  • Forms updated to use get_user_model()
  • Tests created and passing
  • Migrations created and tested on fresh database
  • createsuperuser works correctly

Key Takeaways

  1. Always create custom user model at project start — even if you don’t need it yet
  2. Use AbstractUser for simple customization (keeps all default fields)
  3. Use AbstractBaseUser for complete control (define everything yourself)
  4. Set AUTH_USER_MODEL before first migration — can’t easily change it later
  5. Use settings.AUTH_USER_MODEL in foreign keys, not direct import
  6. Use get_user_model() in code to get user model
  7. Profile model is easier if you already have migrations
  8. Test on fresh database before deploying

The Bottom Line

Django’s user model is hard to change after you start. The solution is simple: create a custom user model from day one, even if you don’t need custom fields yet.

It takes 5 minutes to set up. It saves days of pain later.

Start every new Django project with:

# 1. Create accounts app
python manage.py startapp accounts

# 2. Create CustomUser inheriting from AbstractUser
# 3. Set AUTH_USER_MODEL = 'accounts.CustomUser'
# 4. Then run first migration

This way, when you need to add user fields later, you just add them to your CustomUser model and migrate. No pain, no data loss, no starting over.

SUBSCRIBE FOR NEW ARTICLES

@
comments powered by Disqus