Complete Guide: Creating Custom User Models in Django
Table of Contents
- Introduction
- Why Custom User Models?
- Planning Your Custom User
- Step-by-Step Implementation
- Advanced Features
- Forms and Authentication
- Views and Templates
- Admin Integration
- Signals and User Profiles
- Best Practices
- Common Pitfalls
- Testing
- Migration Strategies
Introduction
Django's built-in User model is excellent for many applications, but real-world projects often require customization. This comprehensive guide will teach you how to create custom user models in Django from scratch, covering everything from basic implementation to advanced features.
Why Custom User Models?
Benefits of Custom User Models:
- Email as Username: Use email instead of username for authentication
- Additional Fields: Add custom fields like full_name, phone, avatar, etc.
- Better Control: Complete control over user authentication logic
- Future-Proof: Easier to modify user model later
- Business Logic: Integrate business-specific user requirements
When to Use Custom User Models:
- ✅ Start of project (highly recommended)
- ✅ Email-based authentication needed
- ✅ Additional user fields required
- ✅ Custom authentication logic
- ❌ Mid-project (complex migrations required)
Planning Your Custom User
Before implementing, decide on:
- Authentication field: email, phone, username
- Required fields: What information is mandatory
- Optional fields: Additional user data
- Permissions: Role-based access control
- Profile separation: Separate profile model or all-in-one
Step-by-Step Implementation
Step 1: Create Accounts App
# Create the accounts app
python manage.py startapp accounts
# Add to INSTALLED_APPS in settings.py
Step 2: Custom User Manager
Create a custom user manager in accounts/models.py:
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.db.models.signals import post_save
from django.dispatch import receiver
class CustomUserManager(BaseUserManager):
"""
Custom user manager where email is the unique identifier.
This manager handles user creation with email as the primary field
instead of username. It provides methods for creating regular users
and superusers.
"""
def create_user(self, email, password=None, **extra_fields):
"""
Create and return a regular user with an email and password.
Args:
email (str): User's email address
password (str): User's password
**extra_fields: Additional fields for the user
Returns:
User: The created user instance
Raises:
ValueError: If email is not provided
"""
if not email:
raise ValueError('The Email field must be set')
# Normalize email to ensure consistency
email = self.normalize_email(email)
# Create user instance
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):
"""
Create and return a superuser with admin privileges.
Args:
email (str): Superuser's email address
password (str): Superuser's password
**extra_fields: Additional fields for the superuser
Returns:
User: The created superuser instance
Raises:
ValueError: If is_staff or is_superuser is not True
"""
# Set default values for superuser
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_active', True)
# Validate superuser requirements
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)
Step 3: Custom User Model
Continue in accounts/models.py:
class User(AbstractBaseUser, PermissionsMixin):
"""
Custom user model with email as the unique identifier.
This model extends AbstractBaseUser and PermissionsMixin to provide
a complete user system with email-based authentication.
"""
# Core fields
email = models.EmailField(
unique=True,
db_index=True, # Index for faster queries
help_text='Required. Enter a valid email address.'
)
full_name = models.CharField(
max_length=255,
blank=True,
help_text='User\'s full name (optional)'
)
# Status fields
is_active = models.BooleanField(
default=True,
help_text='Designates whether this user should be treated as active.'
)
is_staff = models.BooleanField(
default=False,
help_text='Designates whether the user can log into the admin site.'
)
# Timestamps
date_joined = models.DateTimeField(auto_now_add=True)
last_modified = models.DateTimeField(auto_now=True)
# Manager
objects = CustomUserManager()
# Authentication settings
USERNAME_FIELD = 'email' # Use email for authentication
REQUIRED_FIELDS = [] # Required fields when creating superuser (excluding USERNAME_FIELD and password)
class Meta:
verbose_name = 'user'
verbose_name_plural = 'users'
db_table = 'accounts_user' # Custom table name
indexes = [
models.Index(fields=['email']),
models.Index(fields=['is_active']),
]
def __str__(self):
"""String representation of the user."""
return self.email
def get_short_name(self):
"""
Return the short name for the user.
Returns:
str: First name or part of email if full_name not available
"""
if self.full_name:
return self.full_name.split()[0]
return self.email.split('@')[0]
def get_full_name(self):
"""
Return the full name for the user.
Returns:
str: Full name or email if full_name not available
"""
return self.full_name or self.email
@property
def is_premium(self):
"""Check if user has premium status (example business logic)."""
# This is an example - implement based on your business logic
return hasattr(self, 'profile') and self.profile.is_premium_member
Step 4: Configure Settings
Add to your settings.py:
# Custom user model configuration
AUTH_USER_MODEL = 'accounts.User'
# Add accounts app to INSTALLED_APPS
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Your apps
'accounts', # Add this
# ... other apps
]
# Optional: Email backend configuration for development
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
Step 5: Create and Run Migrations
# Create migrations for the custom user model
python manage.py makemigrations accounts
# Apply migrations
python manage.py migrate
# Create superuser with email
python manage.py createsuperuser
Advanced Features
User Profile with Signals
Create a separate profile model for additional user data:
class Profile(models.Model):
"""
User profile model for additional user information.
This model is automatically created when a user is registered
and contains non-authentication related user data.
"""
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='profile'
)
# Personal information
bio = models.TextField(
max_length=500,
blank=True,
help_text='Short biography (max 500 characters)'
)
avatar = models.ImageField(
upload_to='avatars/',
blank=True,
null=True,
help_text='Profile picture'
)
phone = models.CharField(
max_length=20,
blank=True,
help_text='Phone number'
)
birth_date = models.DateField(
null=True,
blank=True,
help_text='Date of birth'
)
# Address information
address = models.TextField(blank=True)
city = models.CharField(max_length=100, blank=True)
country = models.CharField(max_length=100, blank=True)
# Dashboard statistics
saved_tutorials_count = models.PositiveIntegerField(default=0)
active_courses_count = models.PositiveIntegerField(default=0)
orders_count = models.PositiveIntegerField(default=0)
# Preferences
newsletter_subscribed = models.BooleanField(
default=False,
help_text='Subscribe to newsletter'
)
email_notifications = models.BooleanField(
default=True,
help_text='Receive email notifications'
)
is_premium_member = models.BooleanField(
default=False,
help_text='Premium membership status'
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'profile'
verbose_name_plural = 'profiles'
def __str__(self):
return f"{self.user.email}'s profile"
def get_display_name(self):
"""Return display name for the user."""
return self.user.get_full_name()
# Signal handlers for automatic profile creation
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
"""Create profile when user is created."""
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
"""Save profile when user is saved."""
if hasattr(instance, 'profile'):
instance.profile.save()
Forms and Authentication
Registration Form
Create accounts/forms.py:
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import AuthenticationForm
from django.core.exceptions import ValidationError
from django.contrib.auth.password_validation import validate_password
User = get_user_model()
class UserRegistrationForm(forms.ModelForm):
"""
Form for user registration with email and password.
This form handles user registration with custom validation
and styling for better user experience.
"""
email = forms.EmailField(
widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'john@example.com',
'required': True,
}),
help_text='We\'ll never share your email with anyone else.'
)
full_name = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'John Doe',
}),
help_text='Optional: Your full name for personalization.'
)
password1 = forms.CharField(
label='Password',
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': '••••••••',
}),
help_text='At least 8 characters with a mix of letters, numbers & symbols.'
)
password2 = forms.CharField(
label='Confirm Password',
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': '••••••••',
}),
help_text='Enter the same password as before, for verification.'
)
agree_terms = forms.BooleanField(
required=True,
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input',
}),
help_text='You must agree to our terms and conditions.'
)
class Meta:
model = User
fields = ['email', 'full_name']
def clean_email(self):
"""Validate that email is unique."""
email = self.cleaned_data.get('email')
if User.objects.filter(email=email).exists():
raise ValidationError('A user with this email already exists.')
return email
def clean_password1(self):
"""Validate password strength."""
password = self.cleaned_data.get('password1')
if password:
try:
validate_password(password)
except ValidationError as error:
raise ValidationError(error)
return password
def clean(self):
"""Validate that passwords match."""
cleaned_data = super().clean()
password1 = cleaned_data.get('password1')
password2 = cleaned_data.get('password2')
if password1 and password2 and password1 != password2:
raise ValidationError('Passwords do not match.')
return cleaned_data
def save(self, commit=True):
"""Save user with hashed password."""
user = super().save(commit=False)
user.set_password(self.cleaned_data['password1'])
if commit:
user.save()
return user
class UserLoginForm(AuthenticationForm):
"""Custom login form with email instead of username."""
username = forms.EmailField(
label='Email',
widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'your@email.com',
'autofocus': True,
})
)
password = forms.CharField(
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': 'Password',
})
)
remember_me = forms.BooleanField(
required=False,
initial=True,
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input',
}),
help_text='Keep me signed in on this device.'
)
class ProfileUpdateForm(forms.ModelForm):
"""Form for updating user profile information."""
full_name = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter your full name',
})
)
bio = forms.CharField(
max_length=500,
required=False,
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Tell us about yourself...',
})
)
phone = forms.CharField(
max_length=20,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '+1 (555) 123-4567',
})
)
newsletter_subscribed = forms.BooleanField(
required=False,
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input',
}),
help_text='Receive our newsletter with updates and tips.'
)
class Meta:
model = User
fields = ['full_name']
def __init__(self, *args, **kwargs):
"""Initialize form with user profile data."""
super().__init__(*args, **kwargs)
if self.instance and hasattr(self.instance, 'profile'):
profile = self.instance.profile
self.fields['bio'].initial = profile.bio
self.fields['phone'].initial = profile.phone
self.fields['newsletter_subscribed'].initial = profile.newsletter_subscribed
def save(self, commit=True):
"""Save both user and profile data."""
user = super().save(commit=commit)
if commit and hasattr(user, 'profile'):
profile = user.profile
profile.bio = self.cleaned_data.get('bio', '')
profile.phone = self.cleaned_data.get('phone', '')
profile.newsletter_subscribed = self.cleaned_data.get('newsletter_subscribed', False)
profile.save()
return user
Views and Templates
Class-Based Views
Create accounts/views.py:
from django.shortcuts import render, redirect
from django.contrib.auth import login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import LoginView
from django.contrib import messages
from django.urls import reverse_lazy
from django.views.generic import CreateView, UpdateView
from django.utils.decorators import method_decorator
from django.contrib.auth.mixins import LoginRequiredMixin
from .forms import UserRegistrationForm, UserLoginForm, ProfileUpdateForm
class RegisterView(CreateView):
"""
User registration view using class-based approach.
This view handles user registration with proper form validation,
automatic login after registration, and success messaging.
"""
form_class = UserRegistrationForm
template_name = 'accounts/register.html'
success_url = reverse_lazy('accounts:dashboard')
def dispatch(self, request, *args, **kwargs):
"""Redirect authenticated users to dashboard."""
if request.user.is_authenticated:
return redirect('accounts:dashboard')
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
"""Handle successful form submission."""
user = form.save()
login(self.request, user)
messages.success(
self.request,
f'Welcome {user.get_short_name()}! Your account has been created successfully.'
)
return redirect(self.success_url)
def form_invalid(self, form):
"""Handle form validation errors."""
messages.error(
self.request,
'Please correct the errors below.'
)
return super().form_invalid(form)
class CustomLoginView(LoginView):
"""
Custom login view with email authentication and enhanced features.
"""
form_class = UserLoginForm
template_name = 'accounts/login.html'
redirect_authenticated_user = True
def get_success_url(self):
"""Determine where to redirect after successful login."""
return reverse_lazy('accounts:dashboard')
def form_valid(self, form):
"""Handle successful login with remember me functionality."""
remember_me = form.cleaned_data.get('remember_me')
if not remember_me:
# Session expires when browser closes
self.request.session.set_expiry(0)
else:
# Session expires after 30 days
self.request.session.set_expiry(30 * 24 * 60 * 60)
messages.success(
self.request,
f'Welcome back, {form.get_user().get_short_name()}!'
)
return super().form_valid(form)
@method_decorator(login_required, name='dispatch')
class ProfileUpdateView(UpdateView):
"""
View for updating user profile information.
"""
form_class = ProfileUpdateForm
template_name = 'accounts/profile_update.html'
success_url = reverse_lazy('accounts:profile')
def get_object(self):
"""Return the current user."""
return self.request.user
def form_valid(self, form):
"""Handle successful profile update."""
messages.success(
self.request,
'Your profile has been updated successfully!'
)
return super().form_valid(form)
def logout_view(request):
"""Handle user logout."""
user_name = request.user.get_short_name() if request.user.is_authenticated else "User"
logout(request)
messages.info(request, f'Goodbye {user_name}! You have been logged out.')
return redirect('core:home')
@login_required
def dashboard_view(request):
"""
User dashboard showing profile overview and statistics.
"""
user = request.user
profile = user.profile
# Example: Get user-related data
context = {
'user': user,
'profile': profile,
# Add your dashboard data here
'total_orders': 0, # Replace with actual data
'active_courses': 0, # Replace with actual data
'saved_items': 0, # Replace with actual data
}
return render(request, 'accounts/dashboard.html', context)
URL Configuration
Create accounts/urls.py:
from django.urls import path
from .views import (
RegisterView,
CustomLoginView,
ProfileUpdateView,
logout_view,
dashboard_view
)
app_name = 'accounts'
urlpatterns = [
path('register/', RegisterView.as_view(), name='register'),
path('login/', CustomLoginView.as_view(), name='login'),
path('logout/', logout_view, name='logout'),
path('dashboard/', dashboard_view, name='dashboard'),
path('profile/', ProfileUpdateView.as_view(), name='profile'),
]
Add to main urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accounts.urls')),
# ... other URLs
]
Admin Integration
Create accounts/admin.py:
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.contrib.auth import get_user_model
from .models import Profile
User = get_user_model()
class CustomUserCreationForm(UserCreationForm):
"""Custom user creation form for admin."""
class Meta:
model = User
fields = ('email',)
class CustomUserChangeForm(UserChangeForm):
"""Custom user change form for admin."""
class Meta:
model = User
fields = ('email', 'full_name', 'is_active', 'is_staff', 'is_superuser')
class ProfileInline(admin.StackedInline):
"""Inline admin for user profile."""
model = Profile
can_delete = False
verbose_name_plural = 'Profile'
fields = (
'bio', 'avatar', 'phone', 'birth_date',
'newsletter_subscribed', 'email_notifications',
'is_premium_member'
)
@admin.register(User)
class CustomUserAdmin(BaseUserAdmin):
"""Custom user admin with email-based authentication."""
form = CustomUserChangeForm
add_form = CustomUserCreationForm
inlines = [ProfileInline]
# Fields to display in the user list
list_display = (
'email', 'full_name', 'is_active',
'is_staff', 'date_joined', 'last_login'
)
# Fields to filter by
list_filter = (
'is_active', 'is_staff', 'is_superuser',
'date_joined', 'last_login'
)
# Fields to search by
search_fields = ('email', 'full_name')
# Ordering
ordering = ('-date_joined',)
# Fields for adding a user
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2'),
}),
)
# Fields for editing a user
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {'fields': ('full_name',)}),
('Permissions', {
'fields': (
'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions'
),
}),
('Important dates', {'fields': ('last_login', 'date_joined')}),
)
# Read-only fields
readonly_fields = ('date_joined', 'last_login')
@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
"""Admin for user profiles."""
list_display = (
'user', 'created_at', 'newsletter_subscribed',
'is_premium_member', 'updated_at'
)
list_filter = (
'newsletter_subscribed', 'is_premium_member',
'created_at', 'updated_at'
)
search_fields = ('user__email', 'user__full_name')
readonly_fields = ('created_at', 'updated_at')
fieldsets = (
('User', {'fields': ('user',)}),
('Personal Information', {
'fields': ('bio', 'avatar', 'phone', 'birth_date')
}),
('Address', {
'fields': ('address', 'city', 'country'),
'classes': ('collapse',)
}),
('Statistics', {
'fields': (
'saved_tutorials_count', 'active_courses_count',
'orders_count'
),
'classes': ('collapse',)
}),
('Preferences', {
'fields': (
'newsletter_subscribed', 'email_notifications',
'is_premium_member'
)
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
Best Practices
1. Security Considerations
# In your models.py
class User(AbstractBaseUser, PermissionsMixin):
# ... other fields ...
# Add security-related methods
def get_session_auth_hash(self):
"""Return hash for session invalidation on password change."""
return super().get_session_auth_hash()
def check_password(self, raw_password):
"""Check password with additional security measures."""
# Add custom password checking logic if needed
return super().check_password(raw_password)
# In settings.py - Password validation
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 8,
}
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
2. Performance Optimization
# In your views.py
from django.db import select_related, prefetch_related
def dashboard_view(request):
# Optimize database queries
user = User.objects.select_related('profile').get(id=request.user.id)
# Use prefetch_related for reverse foreign keys
orders = user.orders.prefetch_related('items').filter(status='completed')
return render(request, 'dashboard.html', {
'user': user,
'orders': orders
})
3. Testing Your Custom User
Create accounts/tests.py:
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
User = get_user_model()
class CustomUserModelTests(TestCase):
"""Test cases for custom user model."""
def test_create_user(self):
"""Test creating a regular user."""
user = User.objects.create_user(
email='test@example.com',
password='testpass123',
full_name='Test User'
)
self.assertEqual(user.email, 'test@example.com')
self.assertEqual(user.full_name, 'Test User')
self.assertTrue(user.is_active)
self.assertFalse(user.is_staff)
self.assertFalse(user.is_superuser)
def test_create_superuser(self):
"""Test creating a superuser."""
admin_user = User.objects.create_superuser(
email='admin@example.com',
password='testpass123'
)
self.assertEqual(admin_user.email, 'admin@example.com')
self.assertTrue(admin_user.is_active)
self.assertTrue(admin_user.is_staff)
self.assertTrue(admin_user.is_superuser)
def test_user_without_email_raises_error(self):
"""Test that creating user without email raises ValueError."""
with self.assertRaises(ValueError):
User.objects.create_user(
email='',
password='testpass123'
)
def test_user_profile_created(self):
"""Test that profile is automatically created."""
user = User.objects.create_user(
email='test@example.com',
password='testpass123'
)
self.assertTrue(hasattr(user, 'profile'))
self.assertIsNotNone(user.profile)
class CustomUserFormsTests(TestCase):
"""Test cases for custom user forms."""
def test_valid_registration_form(self):
"""Test valid user registration form."""
form_data = {
'email': 'test@example.com',
'full_name': 'Test User',
'password1': 'complexpass123',
'password2': 'complexpass123',
'agree_terms': True
}
from .forms import UserRegistrationForm
form = UserRegistrationForm(data=form_data)
self.assertTrue(form.is_valid())
def test_password_mismatch(self):
"""Test password mismatch validation."""
form_data = {
'email': 'test@example.com',
'password1': 'complexpass123',
'password2': 'differentpass123',
'agree_terms': True
}
from .forms import UserRegistrationForm
form = UserRegistrationForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('Passwords do not match', str(form.errors))
Common Pitfalls
1. Migration Issues
❌ Wrong: Changing to custom user mid-project ✅ Right: Implement custom user from project start
2. Foreign Key References
❌ Wrong:
from django.contrib.auth.models import User # Wrong!
user = models.ForeignKey(User, on_delete=models.CASCADE)
✅ Right:
from django.conf import settings
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
3. Missing Required Fields
❌ Wrong: Not implementing USERNAME_FIELD properly ✅ Right: Properly set USERNAME_FIELD and REQUIRED_FIELDS
Migration Strategies
For Existing Projects (Advanced)
⚠️ Warning: Migrating existing projects is complex and risky!
# 1. Backup your database first!
# 2. Create custom user model
# 3. Create migration
python manage.py makemigrations --empty accounts
# 4. Edit migration file manually (complex process)
# 5. Run migration
python manage.py migrate
Data Migration Example
# In your migration file
from django.db import migrations
from django.contrib.auth import get_user_model
def migrate_users(apps, schema_editor):
# Complex migration logic here
pass
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.RunPython(migrate_users),
]
Conclusion
Creating custom user models in Django provides flexibility and future-proofing for your applications. Key takeaways:
- Start Early: Implement custom users at project start
- Plan Ahead: Think about all required fields and relationships
- Use Signals: Automatically create related models (profiles)
- Test Thoroughly: Comprehensive testing is crucial
- Follow Conventions: Use Django's built-in patterns and best practices
Next Steps
- Implement email verification
- Add social authentication
- Create role-based permissions
- Add password reset functionality
- Implement account deactivation/deletion
This guide provides a solid foundation for implementing custom user models in Django. Adapt the examples to fit your specific requirements and always test thoroughly in a development environment first.
Created by analyzing Django best practices and real-world implementations. Last updated: January 2026