Django User Model: Custom User and User Profile

Django's default User model provides fundamental authentication functionality but often requires customization for real-world applications needing additional user attributes, alternative authentication methods, or specialized user types. Understanding custom user models and profile extensions enables building flexible authentication systems tailored to specific application requirements. This comprehensive guide explores Django's User model architecture including the default User model's fields and methods, creating custom user models extending AbstractUser for simple customizations, implementing completely custom authentication with AbstractBaseUser for maximum flexibility, adding user profiles through OneToOne relationships for supplementary information, managing multiple user types with model inheritance and type discrimination, integrating custom managers for specialized querysets, handling migrations when customizing users, and best practices for user model design ensuring scalability and maintainability. Mastering user model customization early in project development prevents complex migrations later while providing the flexibility needed for authentication requirements across diverse applications from simple blogs to complex multi-tenant SaaS platforms.
Understanding Default User Model
Django's default User model located in django.contrib.auth.models provides fields including username, password, email, first_name, last_name, is_staff, is_active, is_superuser, last_login, and date_joined. It handles authentication, permissions, and groups through built-in methods and relationships. While suitable for simple applications, it has limitations including mandatory username field, non-unique email, and fixed field set requiring customization for most production applications needing additional user attributes or alternative authentication schemes.
# Default User model fields and methods
from django.contrib.auth.models import User
# Creating users
user = User.objects.create_user(
username='john',
email='[email protected]',
password='secure_password',
first_name='John',
last_name='Doe'
)
# Accessing user fields
print(user.username) # 'john'
print(user.email) # '[email protected]'
print(user.get_full_name()) # 'John Doe'
print(user.is_authenticated) # True
print(user.is_staff) # False
# Changing password
user.set_password('new_password')
user.save()
# Checking password
if user.check_password('new_password'):
print('Password correct')
# User permissions
user.user_permissions.add(permission)
user.groups.add(group)
if user.has_perm('app.change_model'):
print('User has permission')
# Limitations of default User model:
# 1. Username required (can't use email-only authentication)
# 2. Email not unique by default
# 3. Can't add custom fields without profile
# 4. Fixed authentication methodCustom User with AbstractUser
AbstractUser provides the easiest customization path by extending Django's default User model with additional fields while retaining all built-in authentication functionality. This approach suits applications needing extra user attributes like phone numbers, birthdays, or addresses while keeping standard username and email authentication. AbstractUser includes all default User fields and methods allowing simple field additions without reimplementing authentication logic.
# models.py - Custom User extending AbstractUser
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
# Add custom fields
phone_number = models.CharField(max_length=15, blank=True)
date_of_birth = models.DateField(null=True, blank=True)
bio = models.TextField(max_length=500, blank=True)
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
website = models.URLField(blank=True)
location = models.CharField(max_length=100, blank=True)
# Override fields if needed
email = models.EmailField(unique=True) # Make email unique
def get_age(self):
if self.date_of_birth:
from datetime import date
today = date.today()
return today.year - self.date_of_birth.year
return None
def __str__(self):
return self.username
# settings.py
AUTH_USER_MODEL = 'accounts.CustomUser'
# admin.py - Register custom user
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
class CustomUserAdmin(UserAdmin):
model = CustomUser
list_display = ['username', 'email', 'phone_number', 'is_staff']
# Add custom fields to admin
fieldsets = UserAdmin.fieldsets + (
('Additional Info', {
'fields': ('phone_number', 'date_of_birth', 'bio', 'avatar', 'website', 'location')
}),
)
add_fieldsets = UserAdmin.add_fieldsets + (
('Additional Info', {
'fields': ('phone_number', 'date_of_birth', 'email')
}),
)
admin.site.register(CustomUser, CustomUserAdmin)
# forms.py - Custom registration form
from django.contrib.auth.forms import UserCreationForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = CustomUser
fields = ('username', 'email', 'phone_number', 'password1', 'password2')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['email'].required = True
# Usage in views
from .forms import CustomUserCreationForm
from django.views.generic import CreateView
class RegisterView(CreateView):
form_class = CustomUserCreationForm
template_name = 'register.html'
success_url = '/dashboard/'Custom User with AbstractBaseUser
AbstractBaseUser provides maximum flexibility for completely custom authentication systems requiring non-standard fields, alternative authentication methods like email or phone-only login, or specialized user types. This approach requires implementing custom managers, defining USERNAME_FIELD and REQUIRED_FIELDS, and manually adding permission support. Use AbstractBaseUser when you need full control over user model structure and authentication logic beyond what AbstractUser provides.
# models.py - Email-based authentication with AbstractBaseUser
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.db import models
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)
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(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
full_name = models.CharField(max_length=255)
phone_number = models.CharField(max_length=15, unique=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(auto_now_add=True)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['full_name']
def __str__(self):
return self.email
def get_full_name(self):
return self.full_name
def get_short_name(self):
return self.email
# settings.py
AUTH_USER_MODEL = 'accounts.CustomUser'
# admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import CustomUser
class CustomUserAdmin(BaseUserAdmin):
list_display = ('email', 'full_name', 'is_staff', 'is_active')
list_filter = ('is_staff', 'is_active')
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal Info', {'fields': ('full_name', 'phone_number')}),
('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', 'full_name', 'phone_number', 'password1', 'password2', 'is_staff', 'is_active')
}),
)
search_fields = ('email', 'full_name')
ordering = ('email',)
filter_horizontal = ('groups', 'user_permissions')
admin.site.register(CustomUser, CustomUserAdmin)User Profile with OneToOne Relationship
When using Django's default User model or when you can't modify the user model directly, create separate Profile models connected via OneToOne relationships. This approach keeps authentication data separate from additional profile information, supports multiple profile types for different user roles, and allows incremental profile data collection. Profiles are created automatically using signals when users register, accessed through user.profile relationships, and extended easily without modifying core user models.
# models.py - User Profile with OneToOne
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
bio = models.TextField(max_length=500, blank=True)
avatar = models.ImageField(upload_to='profiles/', blank=True)
phone_number = models.CharField(max_length=15, blank=True)
date_of_birth = models.DateField(null=True, blank=True)
website = models.URLField(blank=True)
location = models.CharField(max_length=100, blank=True)
twitter = models.CharField(max_length=50, blank=True)
github = models.CharField(max_length=50, blank=True)
def __str__(self):
return f'{self.user.username} Profile'
# Automatically create profile when user is created
@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()
# Accessing profile
user = User.objects.get(username='john')
profile = user.profile
print(profile.bio)
# Updating profile
user.profile.bio = 'New bio text'
user.profile.save()
# views.py - Profile update view
from django.views.generic import UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
class ProfileUpdateView(LoginRequiredMixin, UpdateView):
model = UserProfile
fields = ['bio', 'avatar', 'phone_number', 'website', 'location']
template_name = 'accounts/profile_form.html'
success_url = '/profile/'
def get_object(self, queryset=None):
return self.request.user.profile
# forms.py - Combined user and profile form
from django import forms
from django.contrib.auth.models import User
from .models import UserProfile
class UserUpdateForm(forms.ModelForm):
class Meta:
model = User
fields = ['username', 'email', 'first_name', 'last_name']
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ['bio', 'avatar', 'phone_number', 'website', 'location']
# Combined update view
class CombinedProfileUpdateView(LoginRequiredMixin, TemplateView):
template_name = 'accounts/profile_edit.html'
def get(self, request):
user_form = UserUpdateForm(instance=request.user)
profile_form = ProfileUpdateForm(instance=request.user.profile)
return render(request, self.template_name, {
'user_form': user_form,
'profile_form': profile_form
})
def post(self, request):
user_form = UserUpdateForm(request.POST, instance=request.user)
profile_form = ProfileUpdateForm(
request.POST,
request.FILES,
instance=request.user.profile
)
if user_form.is_valid() and profile_form.is_valid():
user_form.save()
profile_form.save()
messages.success(request, 'Profile updated successfully!')
return redirect('profile')
return render(request, self.template_name, {
'user_form': user_form,
'profile_form': profile_form
})User Model Approaches Comparison
| Approach | Use When | Pros | Cons |
|---|---|---|---|
| Default User | Simple projects | Quick setup, well documented | Limited customization |
| AbstractUser | Need extra fields | Easy customization, keeps auth | Still uses username |
| AbstractBaseUser | Full control needed | Complete flexibility | More complex setup |
| Profile Model | Can't modify User | Separate concerns, flexible | Extra queries, complexity |
Best Practices
- Start with custom user: Always create custom user model at project start even if not customizing immediately
- Use AbstractUser for simple cases: Extend AbstractUser when you need additional fields but keep standard authentication
- AbstractBaseUser for full control: Use when you need email-only or phone-only authentication or completely custom fields
- Signals for profile creation: Use post_save signals to automatically create profiles ensuring every user has one
- Related_name for clarity: Always set related_name on OneToOne fields for clear reverse relationships
- Test migrations thoroughly: Test custom user migrations extensively before production deployment
Conclusion
Customizing Django's User model is essential for most production applications requiring additional user attributes, alternative authentication methods, or specialized user types. AbstractUser provides the easiest customization path for applications needing extra fields while maintaining standard authentication. AbstractBaseUser offers complete flexibility for custom authentication systems using email, phone numbers, or other identifiers as primary credentials. User Profile models connected via OneToOne relationships provide flexible profile extensions when modifying the user model directly isn't feasible. The key decision is choosing the right approach based on project requirements: default User for simple projects, AbstractUser for additional fields, AbstractBaseUser for complete control, or Profile models for separation of concerns. Always implement custom user models at project start preventing complex migrations later. Understanding user model customization patterns enables building scalable authentication systems tailored to specific application needs supporting diverse use cases from basic blogs to complex multi-tenant platforms throughout Django 6.0 development.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


