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.
| Approach | Best for | The catch |
|---|---|---|
Extend AbstractUser | A brand-new project, before any migrations have run | Must be configured via AUTH_USER_MODEL before your first migrate — switching later is painful |
Separate Profile model | A project that already has real users in its database | The 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'
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.