Custom User Model

The built-in User model you've been using for authentication and registration covers a huge number of projects perfectly well — a username, an email, a password hash, and a handful of flags is genuinely enough for plenty of apps. But sooner or later you'll want to attach information to an account that doesn't belong on the built-in model at all: a short bio, an avatar image, a preferred timezone, or — in TaskFlow's case — something like "this user's favorite project." This lesson covers the two accepted ways to add that information, and how to decide between them.

Why the Built-in User Model Isn't Always Enough

Django's User model is deliberately minimal. It handles the mechanics of authentication well, but it was never meant to hold every piece of profile data your app might need. You have two respectable options once you hit that wall: replace the User model itself with an extended version, or keep the original User model untouched and attach a second model to it with a one-to-one link. They solve the same underlying problem in very different ways, and picking the wrong one for your situation causes real pain later.

ApproachBest forThe catch
Extend AbstractUserA brand-new project, before any migrations have runMust be configured via AUTH_USER_MODEL before your first migrate — switching later is painful
Separate Profile modelA project that already has real users in its databaseThe extra data lives on a second model, so you always access it through user.profile rather than directly on user

Approach 1: Extending AbstractUser

If you're starting a new project and know up front that you'll want extra fields on every account, subclassing AbstractUser is the recommended route. It fully replaces Django's default User model with your own version that carries everything the original had — username, password, email, permissions — plus whatever additional fields you add:

Example: A Custom User Model for TaskFlow

# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to='avatars/', blank=True)

That model alone doesn't do anything until Django is told to actually use it in place of its own default. That's done with one setting, and it is one of the most important lines you'll ever add to a Django project:

# settings.py
AUTH_USER_MODEL = 'accounts.CustomUser'
Note: AUTH_USER_MODEL must be set before you run your project's first migrate. Every other model that references User — including Django's own permission and admin-log tables — is built around whichever model this setting points to at migration time. Changing it after a project already has real user data is a genuinely difficult migration to pull off safely, so this is a decision worth making on day one, not day ninety.

Approach 2: A Separate Profile Model

If TaskFlow already had real registered users and switching the user model wasn't realistic, the safer path is to leave User completely alone and attach a second model to it with a OneToOneField — the same relationship type covered in ManyToMany & OneToOne, just applied specifically to the built-in User model:

Example: A Profile Model Linked to User

# accounts/models.py
from django.contrib.auth.models import User
from django.db import models

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to='avatars/', blank=True)

Each User row can have at most one matching Profile row, and the OneToOneField is what enforces that one-to-one pairing. Nothing about the original User model changes — you're simply parking the extra fields next door and linking back to the account they belong to.

Auto-Creating a Profile Whenever a User Registers

The Profile approach has one gap the AbstractUser approach doesn't: nothing automatically creates a Profile row just because a new User was created. The standard fix is a Django signal — a hook that runs a function automatically whenever a particular event happens elsewhere in the framework. Here, the event is "a User was just saved for the first time":

Example: Creating a Profile via a post_save Signal

# accounts/signals.py
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
from .models import Profile

@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

The created flag is what keeps this from firing on every single save — it's only True the first time a given User row is created, so an existing user logging in later never triggers a duplicate Profile. Signals have a lot more depth than this one example — the full picture, including where to register them and other events besides post_save, is covered in Signals.

Reading the Profile Back from a User Instance

Once a Profile exists, Django's OneToOneField gives you a reverse accessor named after the model in lowercase — so from any logged-in user you reach their profile data as request.user.profile:

# In a view, after the user is authenticated
avatar_url = request.user.profile.avatar.url

That single attribute lookup — user to profile to avatar — is the entire payoff of the OneToOneField approach. It reads almost as if avatar lived directly on User, even though it's actually stored on a separate table.

Note: Both approaches above are correct, real Django patterns — but this website's own project doesn't run a live Django server or database, so neither model definition executes on this page. Try them in your own TaskFlow project.
Next up: Having the right fields on a user account is only half the story — Authorization & Permissions covers controlling what an authenticated user is actually allowed to do.