The user model, and customising it early
Django ships with a complete user model and authentication system — you already used it when you created a
superuser. But the single most consequential auth decision you make is whether to customise the user model
at the very start of the project, and it is one beginners almost always get wrong by not deciding at all.
This lesson covers Django's built-in User, and why you should replace it with your own on day one even if
you think you will not need to.
The built-in User
django.contrib.auth provides a User model out of the box, with username, password (hashed —
verified, stored as pbkdf2_sha256$…, never plaintext), email, first_name, last_name,
is_active, is_staff, is_superuser, and the machinery for groups and permissions. You create users with
User.objects.create_user(username, password) (which hashes the password) and superusers with
createsuperuser. For many projects this is genuinely enough.
The temptation is therefore to just use it. The problem is what happens when — months in, with real data — you discover you need to add a field to the user, or log in by email instead of username, or otherwise change it. At that point, switching to a custom user model is painful and risky, because so much (the admin, foreign keys, migrations) already points at the built-in one.
The rule: create a custom user model before your first migration
Django's own documentation is unusually blunt about this: "It is highly recommended to set up a custom user model, even if the default User model is sufficient for you." The reason is entirely about timing. Swapping the user model on a fresh project (no migrations applied yet) is trivial; swapping it on a project with data is a genuine ordeal. So you pay a tiny cost now to avoid a large cost later that you cannot predict whether you will incur.
Do this at the very start, before the first migrate:
# accounts/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
# add fields now or later — the point is that YOU own this model
phone = models.CharField(max_length=15, blank=True)
# settings.py
AUTH_USER_MODEL = "accounts.User" # tell Django to use YOUR user model
AbstractUser gives you everything the built-in User has (username, password, permissions, the lot) as
a base class you subclass — so you lose nothing and gain a model you can extend whenever you need. Setting
AUTH_USER_MODEL makes Django use it everywhere. Done before the first migration, this is a few lines and
no pain.
AbstractUser versus AbstractBaseUser
Two base classes, and picking the right one matters:
AbstractUser— the full built-in user (username, email, names, permissions) as a subclassable base. Use this almost always: you keep all the standard behaviour and just add fields. It is the low-risk, high-reward choice.AbstractBaseUser— a minimal base with only the password/auth core; you build the rest yourself. Use this only when you need to fundamentally change how identity works — for example, logging in by email with no username at all, or a radically different user structure. It is much more work and only worth it whenAbstractUsergenuinely cannot fit.
For Nidaan, AbstractUser with a couple of extra fields (a phone number, perhaps a role) is exactly right.
Reach for AbstractBaseUser only if you have a specific, strong reason — and you will know when you do.
Referring to the user model correctly
Once you have a custom user model, never import User directly from django.contrib.auth.models in
your own code — that imports the built-in one, not yours. Use the indirections Django provides:
# in models.py, for a ForeignKey to the user:
from django.conf import settings
class Appointment(models.Model):
booked_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
# in views/other code, to get the user model class:
from django.contrib.auth import get_user_model
User = get_user_model()
settings.AUTH_USER_MODEL (a string, for ForeignKeys and migrations) and get_user_model() (the actual
class, for code) both resolve to your user model. Hard-coding from django.contrib.auth.models import User is a bug the moment you have a custom model — it silently points at the wrong thing. Make these two
indirections a habit and your code works whether or not the user model is customised.
Extending with a profile — the alternative, and its limits
If you are already deep into a project with the built-in User and cannot easily switch, the fallback is a
OneToOneField profile (from the relationships lesson):
class StaffProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile")
phone = models.CharField(max_length=15)
This attaches extra fields without changing the user model. It works, but it is a workaround with real
costs: every access to a user's phone becomes user.profile.phone (an extra query or a select_related),
and the data is split across two tables. It is the right tool only when you are stuck with the built-in
model and cannot migrate. For a new project, the custom user model is simpler and better — which is exactly
why you set it up before the first migration.
Check your work
What the built-in User provides. Username, hashed password (verified pbkdf2_sha256), email, names,
is_staff/is_superuser, and groups/permissions — often sufficient on its own.
The one rule. Create a custom user model before the first migration, even if the default seems enough — switching later, with data, is painful; switching on a fresh project is trivial.
How to do it. Subclass AbstractUser in your own app and set AUTH_USER_MODEL = "app.User" in settings
before migrating.
AbstractUser versus AbstractBaseUser. AbstractUser (full user as a base, add fields) almost
always; AbstractBaseUser (minimal, build identity yourself) only to fundamentally change how login works.
Referring to the user model. settings.AUTH_USER_MODEL for ForeignKeys/migrations, get_user_model()
for the class in code — never from django.contrib.auth.models import User, which points at the built-in
one.
The profile fallback. A OneToOneField profile extends the built-in user without switching — a
workaround for projects already stuck with it, with the cost of split data and extra queries.
Practice
- On a fresh project, create an
accountsapp with aUser(AbstractUser)and setAUTH_USER_MODELbefore the firstmigrate; confirmcreatesuperuseruses your model. - Add a
phonefield to your custom user and migrate; confirm it appears in the admin. - In a model, add a
ForeignKeyto the user usingsettings.AUTH_USER_MODEL; confirm the migration references your model. - In code, get the user class with
get_user_model(); contrast with importingUserdirectly and reason about why the latter breaks with a custom model. - On a project that already migrated with the built-in user, attempt to switch to a custom model and read the errors — feel why the timing rule exists.
- Add a
StaffProfileviaOneToOneFieldand accessuser.profile.phone; note the extra query versus a field on a custom user.
Official documentation
- Django — Customizing authentication (custom user model) — The "do it at the start" guidance.
- Django —
AbstractUserandAbstractBaseUser— Choosing a base class. - Django —
get_user_model()andAUTH_USER_MODEL— Referring to the user model correctly.
Next: login, logout and password reset.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship