Python Django Custom User Model Guide

Written by

in

When you start building a Django application, the default User model often feels limiting. Whether you need to store extra profile information, switch to email‑based authentication, or integrate third‑party services, a custom user model is the clean, future‑proof solution. This guide walks you through every step of creating, configuring, and maintaining a Python Django custom user model, so you can avoid common pitfalls and keep your project scalable.

Why Replace Django’s Default User Model?

Out‑of‑the‑box Django provides a robust auth.User model, but it assumes a specific data structure:

  • Username is required and must be unique.
  • Only a limited set of fields (first name, last name, email, etc.) are available.
  • Changing the model after the first migration is extremely painful.

For most real‑world projects, you’ll eventually need one or more of the following:

  1. Email‑only login – eliminating usernames entirely.
  2. Additional profile fields such as phone number, avatar, or subscription status.
  3. Multi‑tenant support where the same email can exist in different organizations.
  4. Integration with external identity providers (OAuth, SSO, etc.) that require custom flags.

Implementing a custom user model from day one prevents costly refactors later and aligns your code with Django’s official recommendations.

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

1. Plan Your Model Structure

Before writing any code, decide which fields are essential. A typical modern user model includes:

  • email – unique, used for authentication.
  • first_name and last_name – optional but handy.
  • is_staff and is_active – for admin control.
  • Any domain‑specific fields (e.g., date_of_birth, profile_picture).

2. Create a New App (Optional but Recommended)

python manage.py startapp accounts

Keeping authentication logic in its own accounts app isolates concerns and makes future reuse easier.

3. Define the Custom User Model

In accounts/models.py, subclass AbstractBaseUser and PermissionsMixin. Use BaseUserManager to handle user creation.

from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.models import PermissionsMixin
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('first name', max_length=30, blank=True)
    last_name = models.CharField('last name', max_length=30, blank=True)
    is_staff = models.BooleanField('staff status', default=False)
    is_active = models.BooleanField('active', default=True)
    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

4. Tell Django About the New Model

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

AUTH_USER_MODEL = 'accounts.CustomUser'

This single setting instructs every Django component—admin, authentication backends, and the ORM—to use your custom model.

5. Update the Admin Interface

Without a proper admin configuration, the default admin will break. Create accounts/admin.py:

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
from django.utils.translation import gettext_lazy as _

@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
    list_display = ('email', 'first_name', 'last_name', 'is_staff')
    list_filter = ('is_staff', 'is_active', 'is_superuser')
    search_fields = ('email', 'first_name', 'last_name')
    ordering = ('email',)

    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        (_('Personal info'), {'fields': ('first_name', 'last_name')}),
        (_('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'),
        }),
    )
    filter_horizontal = ('groups', 'user_permissions')

6. Run Migrations

python manage.py makemigrations accounts
python manage.py migrate

Because the custom model is defined before any migrations that reference auth.User, Django creates the correct database schema without conflicts.

7. Adjust Forms and Authentication Backends (Optional)

If you use Django’s built‑in authentication views, replace the default forms with ones that expect an email field:

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

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

Then point your URLs to the custom views or use django-allauth for a ready‑made solution that works seamlessly with a custom user model.

Best Practices & Common Pitfalls

Never Change AUTH_USER_MODEL After Migration

Switching the user model after the first migration requires data migrations, manual SQL, or third‑party tools. Treat the decision as a permanent architectural choice.

Use get_user_model() Everywhere

Hard‑coding CustomUser in imports couples your code to a specific implementation. Instead, import the active model dynamically:

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

Keep the Model Lightweight

Store only authentication‑related fields in the custom user model. For richer profile data, create a separate Profile model linked via a OneToOneField. This separation preserves performance and keeps the auth system simple.

Test Authentication Thoroughly

  • Verify that createsuperuser works with the new fields.
  • Check password reset, email verification, and login flows.
  • Run integration tests that simulate real‑world login attempts.

Consider Third‑Party Packages

If you need advanced features like social login, multi‑factor authentication, or JWT tokens, libraries such as django‑allauth, simplejwt, or django‑extensions already support custom user models. Integrating them early saves time.

Advanced Topics

Using UUIDs as Primary Keys

For added security and easier data migration, replace the default integer id with a UUID:

import uuid
from django.db import models

class CustomUser(AbstractBaseUser, PermissionsMixin):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # ... other fields remain the same

Multi‑Tenant Authentication

When a single database serves multiple customers, include a tenant foreign key and adjust the manager’s get_by_natural_key method to filter by tenant ID.

Custom Permissions

Beyond <

Comments

Leave a Reply

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