If you have ever started a Django project and realized halfway through that you needed users to log in with their email addresses instead of usernames, or that you needed to store a user’s phone number and social media profile directly on the user object, you have likely encountered the limitations of the default Django User model. While Django’s built-in User model is fantastic for getting a prototype off the ground, it is rarely sufficient for production-grade applications that require flexibility and scalability.
The challenge is that changing your user model in the middle of a project is a documented nightmare. It involves complex database migrations, breaking foreign key relationships, and potentially losing data. This is why the official Django documentation strongly recommends setting up a custom user model at the very beginning of every project—even if the default one seems “good enough” for now.
In this comprehensive guide, we will dive deep into the world of Django authentication. We will explore the differences between AbstractUser and AbstractBaseUser, learn how to implement an email-based login system, and discuss best practices for managing user data. By the end of this article, you will have a rock-solid foundation for building secure, flexible, and professional authentication systems in Django.
Why Use a Custom User Model?
By default, Django provides a User model located in django.contrib.auth.models. It includes fields like username, first_name, last_name, email, password, and several boolean flags like is_staff and is_active. While this covers the basics, modern web development often demands more:
- Authentication Methods: Most modern apps use email as the primary identifier rather than a username.
- Custom Data: You might need to store a user’s date of birth, bio, profile picture, or subscription tier directly in the user table to optimize query performance.
- Third-Party Integration: If you are building a system that integrates with OAuth providers (like Google or GitHub), you may need specific fields to store provider-specific IDs.
- Future-Proofing: Requirements change. Starting with a custom user model ensures you can add any of the above without rewriting your entire database schema later.
AbstractUser vs. AbstractBaseUser: Choosing Your Path
When creating a custom user model, Django offers two primary classes to inherit from. Choosing the right one depends on how much of the default behavior you want to keep.
1. AbstractUser
This is the “safe” choice for 90% of projects. It keeps the default fields (username, first name, etc.) but allows you to add extra fields. You inherit everything Django’s default user has and simply extend it.
2. AbstractBaseUser
This is the “blank slate” choice. It provides the core authentication machinery (password hashing, etc.) but leaves everything else to you. You must define every field, including how the user is identified (e.g., email vs. username). Use this if you want a radically different user structure.
Step-by-Step: Implementing a Custom User Model
In this walkthrough, we will implement a custom user model using AbstractUser. This is the most common and recommended approach for beginners and intermediate developers. We will also modify it to use email as the unique identifier for login.
Step 1: Start a New Django Project
First, create a fresh project. Do not run migrations yet! This is the most critical step.
# Create a virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install Django
pip install django
# Start project and app
django-admin startproject myproject .
python manage.py startapp accounts
Step 2: Create the Custom User Model
Open accounts/models.py. We will import AbstractUser and create our class. We will also create a custom manager, which is required if we want to change how users are created (e.g., ensuring emails are unique).
from django.contrib.auth.models import AbstractUser, BaseUserManager
from django.db import models
from django.utils.translation import gettext_lazy as _
class CustomUserManager(BaseUserManager):
"""
Custom user model manager where email is the unique identifiers
for authentication instead of usernames.
"""
def create_user(self, email, password, **extra_fields):
if not email:
raise ValueError(_('The Email must be set'))
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password, **extra_fields):
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)
class CustomUser(AbstractUser):
# Remove username field
username = None
# Make email unique and required
email = models.EmailField(_('email address'), unique=True)
# Add extra fields for our app
phone_number = models.CharField(max_length=15, blank=True, null=True)
date_of_birth = models.DateField(blank=True, null=True)
# Set email as the login identifier
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
objects = CustomUserManager()
def __str__(self):
return self.email
Step 3: Update Settings
We need to tell Django to use our CustomUser instead of the default one. Open myproject/settings.py and add the following line:
# myproject/settings.py
# Add 'accounts' to INSTALLED_APPS
INSTALLED_APPS = [
...
'accounts',
]
# Tell Django to use our custom user model
AUTH_USER_MODEL = 'accounts.CustomUser'
Step 4: Create and Run Migrations
Now that we have defined our model and told Django where to find it, we can create the initial database schema.
python manage.py makemigrations accounts
python manage.py migrate
By running these commands, Django will create the accounts_customuser table in your database. Because we haven’t run migrations before this, all foreign keys in Django’s built-in apps (like Admin and Sessions) will automatically point to our new table.
Handling Forms and the Django Admin
Django’s built-in forms for creating and editing users (UserCreationForm and UserChangeForm) are hardcoded to use the default User model. If you try to use them in the Admin panel now, you will run into errors because they will still look for a username field.
Updating Custom Forms
Create a file named accounts/forms.py and extend the default forms:
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = CustomUser
fields = ('email', 'phone_number', 'date_of_birth')
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = ('email', 'phone_number', 'date_of_birth')
Registering with the Admin
Finally, update accounts/admin.py to use these forms so you can manage users through the Django Admin dashboard.
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = ['email', 'is_staff', 'is_active',]
list_filter = ['email', 'is_staff', 'is_active',]
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {'fields': ('phone_number', 'date_of_birth')}),
('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', 'password', 'phone_number', 'date_of_birth', 'is_staff', 'is_active')}
),
)
search_fields = ('email',)
ordering = ('email',)
admin.site.register(CustomUser, CustomUserAdmin)
Advanced Concepts: Signals and Profiles
Sometimes, you don’t want to clutter the User model with every single piece of information. For example, if you have a social media app, you might want to keep the User model lean for authentication purposes and put display data (like a bio, website, and profile picture) in a Profile model.
We can use Django Signals to automatically create a profile whenever a new user is registered.
# accounts/models.py
from django.db.models.signals import post_save
from django.dispatch import receiver
class Profile(models.Model):
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)
bio = models.TextField(max_length=500, blank=True)
location = models.CharField(max_length=30, blank=True)
birth_date = models.DateField(null=True, blank=True)
@receiver(post_save, sender=CustomUser)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=CustomUser)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()
This “One-to-One” relationship pattern is excellent for separating concerns. It keeps your authentication logic clean while allowing you to extend user data indefinitely without constantly modifying the primary user table.
Common Mistakes and How to Avoid Them
Implementing custom users is a common source of bugs for developers. Here are the pitfalls you must avoid:
1. Referencing the User Model Directly
Incorrect: from accounts.models import CustomUser in other apps.
Correct: Use settings.AUTH_USER_MODEL or get_user_model().
If you hardcode the import, your app will break if you ever rename the model or move it. By using the dynamic reference, Django ensures the correct model is always used.
# In another app's models.py
from django.conf import settings
from django.db import models
class Post(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
2. Forgetting the Manager
If you use AbstractBaseUser or change the unique identifier to an email, you must rewrite the create_user and create_superuser methods in a custom manager. Without this, the python manage.py createsuperuser command will fail because it won’t know which fields to ask for.
3. Changing the User Model Mid-Project
If you have already run migrations and created a database with the default User model, switching to a custom one is difficult. You will likely get InconsistentMigrationHistory errors. If you are in development, the easiest fix is to delete your database and all migration files (except __init__.py) and start over. If you are in production, you will need a sophisticated migration script to move the data.
Summary and Key Takeaways
Creating a custom user model is a hallmark of professional Django development. It provides the flexibility required for modern web applications and protects your database schema from future headaches.
- Always start a new project with a custom user model.
- Use
AbstractUserif you want to keep standard fields but add more. - Use
AbstractBaseUseronly if you need complete control over the authentication process. - Always use
settings.AUTH_USER_MODELwhen defining ForeignKeys to the user. - Don’t forget to update your
UserCreationFormandUserChangeFormfor the Admin panel.
Frequently Asked Questions (FAQ)
1. Can I use multiple user types (e.g., Student and Teacher)?
Yes. The best approach is usually to have one CustomUser model with a “type” field (using choices) or a boolean flag like is_teacher. You can then use Proxy Models or Profile models to handle the different behaviors and data required for each type.
2. What happens if I forget to set AUTH_USER_MODEL?
Django will continue to use its built-in auth.User. If you later try to change it to your CustomUser after the database is already created, you will face significant migration issues.
3. Is it possible to use both email and username for login?
Yes, but this requires creating a Custom Authentication Backend. You would need to write a class that overrides the authenticate method to check both the username and email fields against the password provided.
4. How do I add a profile picture to the User model?
Simply add an ImageField to your CustomUser model. Make sure you have installed the Pillow library and configured MEDIA_URL and MEDIA_ROOT in your settings.
5. Should I put everything in the Custom User model?
Not necessarily. To keep the users table fast, only put data that you query frequently. Less frequent data (like user preferences, social links, or physical addresses) should be moved to a separate Profile or Settings model linked via a OneToOneField.
