Python Django Custom User Model Guide

Written by

in

Creating a robust authentication system is often the first step when building a Django application. While Django’s default User model works for many projects, real‑world apps frequently need extra fields, alternative login methods, or stricter validation. That’s where a custom user model shines. In this comprehensive guide we’ll walk you through why you should replace the default model, how to design and implement a custom user model, and the best practices that keep your code clean, secure, and future‑proof.

Why Use a Custom User Model?

  • Flexibility: Add fields such as date_of_birth, profile_picture, or is_premium without creating a separate profile model.
  • Unified authentication: Use email, phone number, or a third‑party identifier as the primary login credential.
  • Scalability: Avoid the pain of migrating data later; switching after the first migration can be complex and risky.
  • Compliance: Meet GDPR or industry‑specific requirements by storing only the data you truly need.

When to Create a Custom User Model

Ideally, you should decide on a custom user model before running the first migration. If you’ve already created tables with the default model, you can still migrate, but the process is more involved and may require data export/import.

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

1. Plan Your Fields and Authentication Method

Start by listing the attributes your users need. Common choices include:

email (unique)
first_name
last_name
date_of_birth
is_staff
is_active
is_premium

If you want users to log in with their email instead of a username, you’ll need to set USERNAME_FIELD = 'email' later in the model.

2. Create a New App (Optional but Recommended)

Keeping authentication logic isolated makes maintenance easier.

python manage.py startapp accounts

3. Define the Custom User Model

In accounts/models.py, subclass AbstractBaseUser and PermissionsMixin. The latter provides group and permission handling out of the box.

from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.db import models
from django.utils import timezone
from django.core.mail import send_mail
from .managers import CustomUserManager

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('date of birth', null=True, blank=True)
    is_staff = models.BooleanField(
        'staff status',
        default=False,
        help_text='Designates whether the user can log into this admin site.'
    )
    is_active = models.BooleanField(
        'active',
        default=True,
        help_text='Designates whether this user should be treated as active.'
    )
    date_joined = models.DateTimeField('date joined', 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

    def get_full_name(self):
        return f"{self.first_name} {self.last_name}".strip()

    def get_short_name(self):
        return self.first_name or self.email

    def email_user(self, subject, message, from_email=None, **kwargs):
        send_mail(subject, message, from_email, [self.email], **kwargs)

4. Implement a Custom User Manager

The manager handles user creation, especially for the createsuperuser command.

from django.contrib.auth.base_user import BaseUserManager

class CustomUserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **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_user(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    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:
            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)

5. Tell Django to Use the New Model

Add the following line to your project’s settings.py:

AUTH_USER_MODEL = 'accounts.CustomUser'

Make sure the accounts app is listed before django.contrib.auth in INSTALLED_APPS to avoid circular import issues.

6. Create and Apply Migrations

python manage.py makemigrations accounts
python manage.py migrate

If you’re starting a new project, Django will create the auth_user table based on your custom model, and everything will line up automatically.

7. Update Admin Interface

Register the custom user model with the admin site so you can manage users easily.

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
from .forms import CustomUserCreationForm, CustomUserChangeForm

class CustomUserAdmin(UserAdmin):
    add_form = CustomUserCreationForm
    form = CustomUserChangeForm
    model = CustomUser
    list_display = ('email', 'first_name', 'last_name', 'is_staff', 'is_active')
    list_filter = ('is_staff', 'is_active')
    ordering = ('email',)
    search_fields = ('email', 'first_name', 'last_name')
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Personal info', {'fields': ('first_name', 'last_name', 'date_of_birth')}),
        ('Permissions', {'fields': ('is_staff', 'is_active', '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')}
        ),
    )

admin.site.register(CustomUser, CustomUserAdmin)

8. Create Custom Forms for Admin and Registration

These forms ensure proper validation for the new fields.

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', 'date_of_birth')

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

Testing Your Custom User Model

Write a few simple tests to verify that user creation, authentication, and admin integration work as expected.

from django.test import TestCase
from django.contrib.auth import get_user_model

class UserModelTests(TestCase):
    def test_create_user_with_email(self):
        User = get_user_model()
        user = User.objects.create_user(email='test@example.com', password='test123')
        self.assertEqual(user.email, 'test@example.com')
        self.assertTrue(user.check_password('test123'))
        self.assertFalse(user.is_staff)

    def test_create_superuser(self):
        User = get_user_model()
        admin = User.objects.create_superuser(email='admin@example.com', password='admin123')
        self.assertTrue(admin.is_staff)
        self.assertTrue(admin.is_superuser)

Common Pitfalls & How to Avoid Them

  • Changing the model after migrations: If you need to add a field later, use makemigrations as usual, but avoid altering USERNAME_FIELD after the initial migration.
  • Forgot to update AUTH_USER_MODEL: Django will still reference the default auth.User, leading to obscure errors.
  • Using django.contrib.auth.forms.AuthenticationForm without customization: It expects a username field. Replace it with a custom form that authenticates via email.
  • Hard‑coding user lookups: Always use get_user_model() instead of importing the model directly.

Best Practices for a Clean, Maintainable Custom User Model

  1. Define the model early: Decide on the user schema before any data exists.
  2. Keep authentication logic separate: Use a dedicated accounts app for models, managers, forms, and views.
  3. Leverage Django’s built‑in mixins: PermissionsMixin adds groups and permission handling without extra code.
  4. Comments

    Leave a Reply

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