Python Django Custom User Model Guide

Written by

in

Building a robust authentication system is one of the first challenges you’ll face when developing a Django application. While Django’s default User model works for many simple projects, real‑world apps often require extra fields, alternative login mechanisms, or custom validation rules. That’s where a custom user model shines. In this comprehensive guide we’ll walk you through every step of creating, configuring, and maintaining a custom user model in Python Django, ensuring your project stays scalable, secure, and SEO‑friendly.

Why Use a Custom User Model?

Before diving into the code, it’s worth understanding the benefits that a custom user model brings to a Django project:

  • Flexibility: Add fields such as phone_number, profile_picture, or date_of_birth without resorting to messy one‑to‑one extensions.
  • Future‑proofing: Changing the user model after the first migration is extremely difficult. Starting with a custom model from day one prevents painful refactors later.
  • Alternative authentication: Use email, phone number, or a third‑party identifier as the primary login credential.
  • Better integration: Align the user schema with your business logic, analytics, and GDPR compliance requirements.

Step‑by‑Step Guide to Creating a Custom User Model

1. Plan Your Model Structure

Identify the data you need for each user. Common additions include:

email (as username)
first_name, last_name
is_active, is_staff, is_superuser
date_joined
profile_picture
phone_number

Make a quick sketch or a table to avoid missing fields later.

2. Set Up a New Django Project (If Needed)

If you’re starting fresh, run the following commands:

django-admin startproject myproject
cd myproject
python -m venv venv
source venv/bin/activate
pip install django
django-admin startapp accounts

We’ll keep the custom user model inside the accounts app.

3. Define the Custom User Model

Create accounts/models.py and subclass AbstractBaseUser together with PermissionsMixin. This gives you full control over authentication while still supporting Django’s permission framework.

from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin, BaseUserManager
from django.db import models
from django.utils import timezone

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('The Email field must be set')
        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, **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('email address', unique=True)
    first_name = models.CharField(max_length=30, blank=True)
    last_name = models.CharField(max_length=30, blank=True)
    phone_number = models.CharField(max_length=20, blank=True)
    profile_picture = models.ImageField(upload_to='profiles/', blank=True, null=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    date_joined = models.DateTimeField(default=timezone.now)

    objects = CustomUserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []  # Email & password are required by default

    class Meta:
        verbose_name = 'user'
        verbose_name_plural = 'users'

    def __str__(self):
        return self.email

4. Register the Model in settings.py

Tell Django to use your new model for authentication:

# myproject/settings.py
INSTALLED_APPS = [
    # …
    'accounts',
    # …
]

AUTH_USER_MODEL = 'accounts.CustomUser'

5. Create and Apply Migrations

Run the migration commands. Because the custom model replaces the built‑in User, you must do this before creating any other apps that reference the default model.

python manage.py makemigrations accounts
python manage.py migrate

6. Update Admin Interface

Without admin registration, you won’t be able to manage users from the Django admin. Add a custom UserAdmin class to handle the new fields.

# accounts/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', 'first_name', 'last_name', 'is_staff')
    list_filter = ('is_staff', 'is_active', 'is_superuser')
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Personal info', {'fields': ('first_name', 'last_name', 'phone_number', 'profile_picture')}),
        ('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', 'is_staff', 'is_active')}
        ),
    )
    search_fields = ('email', 'first_name', 'last_name')
    ordering = ('email',)

admin.site.register(CustomUser, CustomUserAdmin)

7. Adjust Forms and Authentication Views

If you rely on Django’s built‑in auth views (e.g., PasswordResetView), they already work with AUTH_USER_MODEL. However, custom registration forms need to reference the new model:

# accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser

class CustomUserCreationForm(UserCreationForm):
    class Meta(UserCreationForm.Meta):
        model = CustomUser
        fields = ('email', 'first_name', 'last_name', 'phone_number')

class CustomUserChangeForm(UserChangeForm):
    class Meta:
        model = CustomUser
        fields = ('email', 'first_name', 'last_name', 'phone_number', 'profile_picture')

8. Wire Up URLs and Views

Below is a minimal example using Django’s generic class‑based views for registration and profile editing.

# accounts/views.py
from django.urls import reverse_lazy
from django.views import generic
from .forms import CustomUserCreationForm, CustomUserChangeForm

class SignUpView(generic.CreateView):
    form_class = CustomUserCreationForm
    success_url = reverse_lazy('login')
    template_name = 'registration/signup.html'

class ProfileUpdateView(generic.UpdateView):
    form_class = CustomUserChangeForm
    success_url = reverse_lazy('profile')
    template_name = 'registration/profile.html'

    def get_object(self):
        return self.request.user

Then add the routes:

# accounts/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('signup/', views.SignUpView.as_view(), name='signup'),
    path('profile/', views.ProfileUpdateView.as_view(), name='profile'),
]

Best Practices for Maintaining a Custom User Model

Never Change AUTH_USER_MODEL After Migration

Switching the user model after you have data is a massive undertaking. If you anticipate a future change, consider using a proxy model or adding a OneToOneField profile instead of replacing the core model.

Keep Business Logic Out of the Model

Models should store data and provide simple helpers. Complex validation, email sending, or third‑party API calls belong in services, signals, or form clean methods. This keeps migrations fast and the codebase maintainable.

Use Signals Sparingly

While Django signals (e.g., post_save) can automate profile creation, they add hidden coupling. If you need a guaranteed side effect, prefer overriding save() or using explicit service calls.

Write Unit Tests Early

Testing authentication flows prevents regressions. A minimal test suite might look like:

# accounts/tests.py
from django.test import TestCase
from django.urls import reverse
from .models import CustomUser

class UserModelTests(TestCase):
    def test_create_user(self):
        user = CustomUser.objects.create_user(email='test@example.com', password='pass123')
        self.assertEqual(user.email, 'test@example.com')
        self.assertTrue(user.check_password('pass123'))

    def test_login_with_email(self):
        CustomUser.objects.create_user(email='login@example.com', password='secret')
        response = self.client.post(reverse('login'), {
            'username': 'login@example.com',
            'password': 'secret'
        })
        self.assertRedirects(response, '/')

Leverage Django’s Built‑In Password Validators

In settings.py enable validators to enforce strong passwords:

AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', '

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *