Python Django Custom User Model Guide

Written by

in

Building a robust authentication system is one of the first challenges you face when starting a new Django project. While Django’s default User model works well for many simple applications, real‑world projects often require additional fields, different login identifiers, or custom permission logic. That’s where a custom user model comes in. In this comprehensive guide we’ll walk through every step of creating, configuring, and maintaining a custom user model in Django, complete with code snippets, best‑practice tips, and common pitfalls to avoid.

Why Choose a Custom User Model?

Before diving into the implementation, it’s important to understand why you might need a custom user model instead of extending the built‑in one.

  • Additional profile data: Store phone numbers, avatars, or subscription status directly on the user record.
  • Alternative authentication fields: Use email, phone number, or a UUID as the primary login identifier.
  • Custom permissions or roles: Implement role‑based access control that differs from Django’s default groups.
  • Future‑proofing: Adding a custom model at project start saves you from costly migrations later.

Because Django reads the user model configuration at startup, you must define your custom model before you run the first migrate. Switching later is possible but requires careful data migration and can be error‑prone.

Step‑by‑Step: Creating a Custom User Model

1. Set Up a Dedicated App

It’s a best practice to keep authentication logic isolated in its own app, for example accounts.

django-admin startapp accounts

2. Define a Custom User Manager

The manager handles user creation and ensures the required fields are set correctly. Use BaseUserManager as the parent class.

from django.contrib.auth.base_user import BaseUserManager

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=None, **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)

3. Build the Custom User Model

Extend AbstractBaseUser and PermissionsMixin to inherit password handling and permission utilities.

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

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_joined = models.DateTimeField('date joined', default=timezone.now)
    is_active = models.BooleanField('active', default=True)
    is_staff = models.BooleanField('staff status', default=False)

    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. Tell Django About the New Model

Add the following line to your project’s settings.py before any app that imports django.contrib.auth:

AUTH_USER_MODEL = 'accounts.CustomUser'

5. Register the Model in the Admin Interface

Customizing the admin makes user management seamless for staff.

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

class CustomUserAdmin(BaseUserAdmin):
    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')}),
        ('Permissions', {'fields': ('is_staff', 'is_active', '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',)
    ordering = ('email',)

admin.site.register(CustomUser, CustomUserAdmin)

6. Create Custom Forms (Optional but Recommended)

Using dedicated forms ensures proper validation for the admin and any registration views.

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

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

Running Migrations

Now that the model is defined, create and apply migrations.

python manage.py makemigrations accounts
python manage.py migrate

If you’re starting a fresh project, the migration will create the accounts_customuser table. For existing projects, consider using a data migration to copy data from auth_user to the new table before dropping the old model.

Using the Custom User Model in Your Code

Authentication Backends

The default authentication backend works out of the box because it relies on AUTH_USER_MODEL. However, if you need to authenticate with a phone number or a third‑party token, create a custom backend:

from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model

User = get_user_model()

class EmailOrPhoneBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        try:
            user = User.objects.get(email=username)  # or phone_number=username
        except User.DoesNotExist:
            return None
        if user.check_password(password):
            return user
        return None

Then add it to AUTHENTICATION_BACKENDS in settings.py.

Referencing the User Model

Never import the model directly with from accounts.models import CustomUser in reusable apps. Use Django’s helper:

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

This approach respects the AUTH_USER_MODEL setting and keeps your code portable.

Testing Your Custom User Model

Write unit tests to guarantee that user creation, authentication, and admin integration behave as expected.

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

User = get_user_model()

class CustomUserModelTests(TestCase):
    def test_create_user_with_email(self):
        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):
        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 user model after migrations: If you forget to set AUTH_USER_MODEL before the first migration, you’ll need to create a new database or perform a complex data migration.
  • Hard‑coding the model name in foreign keys: Use settings.AUTH_USER_MODEL in ForeignKey definitions, e.g. author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE).
  • Missing manager methods: Forgetting to implement create_superuser will break the createsuperuser command.
  • Admin registration errors: Ensure the admin form classes reference the custom model; otherwise you’ll see “User has no attribute ‘username’”.
  • Third‑party packages: Some older packages still assume username exists. Check their documentation or fork them to use email instead.

Best Practices Checklist

  1. Define the custom model before running manage.py migrate.
  2. Always reference the user model via get_user_model() or settings.AUTH_USER_MODEL.
  3. Provide a dedicated

Comments

Leave a Reply

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