How to Switch to a Custom Django User Model Mid-Project

The Django documentation recommends always starting your project with a custom user model (even if it's identical to Django's to begin with), to make it easier to customize later if you need to. But what are you supposed to do if you didn't see this when starting a project, or if you inherited a project without a custom user model and you need to add one?
At Caktus, when Django first added support for a custom user model, we were still using South for migrations. Hard to believe! Nearly six years ago, I wrote a post about migrating to a custom user model that is, of course, largely obsolete now that Django has built-in support for database migrations. As such, I thought it would be helpful to put together a new post for anyone who needs to add a custom user model to their existing project on Django 2.0+.
Background
As of the time of this post, [ticket
25313](https://code.djangoproject.com/ticket/25313) is open in the
Django ticket tracker for adding further documentation about this issue. This ticket includes some high-level steps to follow when moving to a custom user model, and I recommend familiarizing yourself with this first. As noted in the documentation under Changing to a custom user model mid-project, "Changing AUTH_USER_MODEL after you’ve created database tables is significantly more difficult since it affects foreign keys and many-to-many relationships, for example."
The instructions I put together below vary somewhat from the high-level instructions in ticket #25313, I think (hope) in positive and less destructive ways. That said, there's a reason this ticket has been open for more than four years — it’s hard. So, as mentioned in the ticket:
Proceed with caution, and make sure you have a database backup (and a working process for restoring it) before changing your production database.
Overview
Steps 1 and 2 below are the same as they were in 2013 (circa Django
1.5), and everything after that differs since we're now using Django's
built-in migrations (instead of South). At a high level, our strategy is
to create a model in one of our own apps that has all the same fields as
auth.User
and uses the same underlying database table. Then, we fake
the initial migration for our custom user model, test the changes
thoroughly, and deploy everything up until this point to production.
Once complete, you'll have a custom user model in your project, as
recommended in the Django documentation, which you can continue to tweak
to your liking.
Contrary to some other methods (including my 2013
post
), I chose this time to update the existing auth_user
table to help
ensure existing foreign key references stay intact. The downside is that
it currently requires a little manual fiddling in the database. Still,
if you're using a database with referential integrity checking (which
you should be), you'll sleep easier at night knowing you didn't mess
up a data migration affecting all the users in your database.
If you (and a few others) can confirm that something like the below works for you, then perhaps some iteration of this process may make it into the Django documentation at some point.
Migration Process
Here's my approach for switching to a custom user model mid-project:
Assumptions:
- You have an existing project without a custom user model.
- You're using Django's migrations, and all migrations are up-to-date (and have been applied to the production database).
- You have an existing set of users that you need to keep, and any
number of models that point to Django's built-in
User
model.
First, assess any third party apps to make sure they either don't have any references to the Django's
User
model, or if they do, that they use Django's generic methods for referencing the user model.Next, do the same thing for your own project. Go through the code looking for any references you might have to the
User
model, and replace them with the same generic references. In short, you can use theget_user_model()
method to get the model directly, or if you need to create a ForeignKey or other database relationship to the user model, usesettings.AUTH_USER_MODEL
(which is simply a string corresponding to theappname.ModelName
path to the user model).Note that
get_user_model()
cannot be called at the module level in anymodels.py
file (and by extension any file that amodels.py
imports), since you'll end up with a circular import. Generally, it's easier to keep calls toget_user_model()
inside a method whenever possible (so it's called at run time rather than load time), and usesettings.AUTH_USER_MODEL
in all other cases. This isn't always possible (e.g., when creating a ModelForm), but the less you use it at the module level, the fewer circular imports you'll have to stumble your way through.Start a new
users
app (or give it another name of your choice, such asaccounts
). If preferred, you can use an existing app, but it must be an app without any pre-existing migration history because as noted in the Django documentation, "due to limitations of Django’s dynamic dependency feature for swappable models, the model referenced byAUTH_USER_MODEL
must be created in the first migration of its app (usually called0001_initial
); otherwise, you'll have dependency issues."python manage.py startapp users
Add a new
User
model tousers/models.py
, with adb_table
that will make it use the same database table as the existingauth.User
model. For simplicity when updating content types later (and if you'd like your many-to-many table naming in the underlying database schema to match the name of your user model), you should call itUser
as I've done here. You can rename it later if you like.from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): class Meta: db_table = 'auth_user'
As a convenience, if you'd like to inspect the user model via the admin as you go, add an entry for it to
users/admin.py
:from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import User admin.site.register(User, UserAdmin)
In
settings.py
, addusers
toINSTALLED_APPS
and setAUTH_USER_MODEL = 'users.User'
:INSTALLED_APPS = [ # ... 'users', ] AUTH_USER_MODEL = 'users.User'
Create an initial migration for your new
User
model:python manage.py makemigrations
You should end up with a new migration file
users/migrations/0001_initial.py
.Since the
auth_user
table already exists, normally in this situation we would fake this migration with the commandpython manage.py migrate users --fake-initial
. If you try to run that now, however, you'll get anInconsistentMigrationHistory
error, because Django performs a sanity check before faking the migration that prevents it from being applied. In particular, it does not allow this migration to be faked because other migrations that depend on it, i.e., any migrations that include references tosettings.AUTH_USER_MODEL
, have already been run. I'm not entirely sure why Django places this restriction on faking migrations, since the whole point is to tell it that the migration has, in fact, already been applied (if you know why, please comment below). Instead, you can accomplish the same result by adding the initial migration for your newusers
app to the migration history by hand:echo "INSERT INTO django_migrations (app, name, applied) VALUES ('users', '0001_initial', CURRENT_TIMESTAMP);" | python manage.py dbshell
If you're using an app name other than
users
, replaceusers
in the line above with the name of the Django app that holds your user model.At the same time, let's update the
django_content_types
table with the newapp_label
for our user model, so existing references to this content type will remain intact. As with the prior database change, this change must be made before runningmigrate
. The reason for this is thatmigrate
will create any non-existent content types, which will then prevent you from updating the old content type with the new app label (with a "duplicate key value violates unique constraint" error).echo "UPDATE django_content_type SET app_label = 'users' WHERE app_label = 'auth' and model = 'user';" | python manage.py dbshell
Again, if you called your app something other than
users
, be sure to updateSET app_label = 'users'
in the above with your chosen app name.Note that this SQL is for Postgres, and may vary somewhat for other database backends.
At this point, you should stop and deploy everything to a staging environment, as attempting to run migrate before manually tweaking your migration history will fail. If your automated deployment process runs
migrate
(which it likely does), you will need to update that process to run these two SQL statements beforemigrate
(in particular becausemigrate
will create any non-existent content types for you, thereby preventing you from updating the existing content type in the database without further fiddling). Test this process thoroughly (perhaps even multiple times) in a staging environment to make sure you have everything automated correctly.After testing and fixing any errors, everything up until this point should be deployed to production (and/or any other environments where you need to keep the existing user database), after ensuring that you have a good backup and a process for restoring it in the event anything goes wrong.
Now, you should be able to make changes to your
users.User
model and runmakemigrations
/migrate
as needed. For example, as a first step, you may wish to rename theauth_user
table to something in yourusers
app's namespace. You can do so by removingdb_table
from yourUser
model, so it looks like this:class User(AbstractUser): pass
You'll also need to create and run a new migration to make this change in the database:
python manage.py makemigrations --name rename_user_table python manage.py migrate
Success?
That should be it. You should now be able to make other changes (and
create migrations for those changes) to your custom User
model. The
types of changes you can make and how to go about making those changes
is outside the scope of this post, so I recommend carefully reading
through the Django documentation on substituting a custom User
model.
In the event you opt to switch from AbstractUser
to
AbstractBaseUser
, be sure to create data migrations for any of the
fields provided by AbstractUser that you want to keep before deleting
those columns from your database. For more on this topic, check out our
post about DjangoCon
2017,
where we link to a talk by Julia M Looney titled "Getting the most out
of Django’s User Model." My colleague Dmitriy also has a great post
with some other suggestions for picking up old
projects.
Once again, please test this carefully in a staging environment before attempting it on production, and make sure you have a working database backup. Good luck, and please comment below with any success or failure stories, or ideas on how to improve upon this process!