Python Django Custom User Model Guide

Written by

in

Building a robust authentication system is often the first step when launching a new Django project, and the default User model may not always fit your unique business requirements. In this comprehensive guide we’ll walk you through everything you need to know about creating a Python Django custom user model—from the initial decision‑making process to the final testing stage. By the end of this article you’ll have a production‑ready custom user model that scales with your app, improves security, and boosts SEO relevance for keywords like “custom user model guide” and “Django authentication”.

Why Choose a Custom User Model?

Before you dive into code, it’s worth understanding the real benefits of replacing Django’s built‑in User model:

  • Flexibility: Add fields such as phone_number, date_of_birth, or profile_image without creating separate profile tables.
  • Future‑proofing: Avoid costly migrations later—once you switch, changing back is painful.
  • Cleaner authentication flow: Use Email or phone as the primary login identifier instead of a username.
  • Better SEO alignment: Tailor URLs and user‑generated content to include relevant keywords, improving search engine visibility.

When to Implement the Custom User Model

The Django documentation is crystal clear: create your custom user model at the start of the project. If you wait until later, you’ll face complex data migrations and third‑party app compatibility issues. Here’s a quick decision matrix:

  1. **New project** – Implement custom model immediately.
  2. **Existing project without user data** – You can safely migrate, but plan for a maintenance window.
  3. **Existing project with live user data** – Consider a phased rollout or a separate authentication micro‑service.

Step‑by‑Step Guide to Building the Model

1. Create a Dedicated App

Isolating authentication logic keeps your project tidy. Run:

python manage.py startapp accounts

Then add 'accounts' to INSTALLED_APPS in settings.py.

2. Define the Custom User Model

In accounts/models.py extend AbstractBaseUser and PermissionsMixin. This gives you password handling and permission utilities out of the box.

from django.contrib.auth.models import (
    AbstractBaseUser, 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)

        if extra_fields.get('is_staff') is not True or \
           extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_staff=True and 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('first name', max_length=30, blank=True)
    last_name = models.CharField('last name', max_length=30, blank=True)
    date_of_birth = models.DateField(null=True, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    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

3. Update Django Settings

Tell Django to use your new model:

# settings.py
AUTH_USER_MODEL = 'accounts.CustomUser'

Also, configure authentication backends if you need email‑based login:

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
]

4. Create Custom Forms

Replace the default UserCreationForm and UserChangeForm with versions that understand your 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:
        model = CustomUser
        fields = ('email', 'first_name', 'last_name')

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

5. Register the Model in the Admin Site

Without proper admin registration, you’ll lose the ability to manage users from the Django admin.

# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser

@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
    add_form = CustomUserCreationForm
    form = CustomUserChangeForm
    model = CustomUser
    list_display = ('email', 'first_name', 'last_name', 'is_staff')
    list_filter = ('is_staff', 'is_active')
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Personal info', {'fields': ('first_name', 'last_name', 'date_of_birth')}),
        ('Permissions', {'fields': ('is_staff', 'is_active', 'groups', 'user_permissions')}),
    )
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'password1', 'password2', 'is_staff', 'is_active')
        }),
    )
    search_fields = ('email',)
    ordering = ('email',)

6. Run Migrations

Finally, create and apply migrations:

python manage.py makemigrations accounts
python manage.py migrate

At this point, your project is using the custom user model for all authentication flows.

Best Practices & Common Pitfalls

Never Change AUTH_USER_MODEL After Migrations

Switching the user model mid‑project forces you to rewrite every foreign key that points to auth.User. If you must, use Django’s swappable_dependency and a data migration script.

Always Use get_user_model()

Hard‑coding CustomUser in other apps can break third‑party packages. Import the user model dynamically:

from django.contrib.auth import get_user_model
User = get_user_model()

Leverage AbstractUser for Minor Tweaks

If you only need to add a few fields and still want the default username field, subclass AbstractUser instead of AbstractBaseUser. This reduces boilerplate.

Secure Password Handling

  • Never store raw passwords—always use set_password() and check_password().
  • Enable AUTH_PASSWORD_VALIDATORS in settings.py for strength enforcement.
  • Consider adding two‑factor authentication (2FA) for high‑risk accounts.

Testing Your Custom User Model

Automated tests guarantee that future changes won’t break authentication. Here’s a quick example using Django’s test framework:

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

class CustomUserModelTest(TestCase):
    def setUp(self):
        self.User = get_user_model()
        self.user = self.User.objects.create_user(
            email='test@example.com',
            password='StrongPass!123',
            first_name='Test',
            last_name='User'
        )

    def test_user_creation(self):
        self.assertEqual(self.user.email, 'test@example.com')
        self.assertTrue(self.user.check_password('StrongPass!123'))
        self.assertFalse(self.user.is_staff)

    def test_superuser_creation(self):
        admin = self.User.objects.create_superuser(
            email='admin@example.com',
            password='AdminPass!456'
        )
        self.assertTrue(admin.is_staff)
        self.assertTrue(admin.is_superuser)

Run python manage.py test accounts to ensure everything works as expected.

FAQ – Quick Answers for Common Questions

Comments

Leave a Reply

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