Evolving Django’s auth.User
Folks will occasionally ask if I've got an opinion on some topic Django related. I'll reply that I've got an opinion on everything, so I've certainly got an opinion on that.
In Django terms most of the opinions I hold are quite conservative. Django has a grain. It wants you to do things a certain way. That way is proven by the test of time, and my view is, roughly, that the more you lean into it, the happier, and more successful, you are going to be.
“Conservative” here doesn't mean standing still. It is exciting times for Django. The last several years have added a whole host of a new features and improvements, and I see plenty of room to keep pushing the framework forward.
My recent contributions, in the form of Neapolitan and django-template-partials, say, aren't radical – rather they’re very much more of the same – they're more Django – so whilst new, they're quite orthodox too.
But I do have one heresy, at least: I don’t use a custom user model. Instead I prefer Django’s built-in auth.User
. What’s more, I think that custom user models – whilst monstrously cool in their implementation via swappable models – are a failed experiment: they add a complexity tax we all pay, a performance tax that we’re invited to fall into, and fail to address in practice the problems they were introduced to solve.
This is anathema indeed. The Django docs are explicit that a custom user model is recommended. Clearly I think that’s a mistake. More though (and to be argued herein) I think it’s actively doing harm. We should be deemphasising custom user models, and advancing auth.User
directly, as we move forward.
Like all heretics, I think my unorthodoxy is really the correct way, is in fact the more Django-like. I’m not saying this just to be contrary. So that’s our topic today.
I’m making this issue of The Stack Report free access as it ties into recent discussions in the community about Django’s roadmap going forward. In a recent roadmap workshop, updating django.contrib.auth
came out as the number one action item. I put my hand up as being interested. This is, in-part, a pulling-together of my thoughts in order to help the discussion there.
This is somewhat long. I apologise. I’m arguing directly against the received opinion, so by needs I have to spell it out.
Hopefully, needless to say, I’m not criticising anyone or anyone’s work here. I know the history of all of this, and I understand the reasons why discussions went the way they did. I do though think it’s time to review. It’s only with the intervening time that we can see how it’s fallen out. It could have gone otherwise. I’m not sure I’ll convince anyone. I’m not even sure I’m right. These are just how I see it. How I do things, even if others don’t. ❤️
§
The Complexity Tax
Django is hard to get started with. Custom user models make it harder.
It’s not easy for anyone to get going with their first web framework. There’s a learning curve, and some of that is essential to the subject. We can make the point time and time again that under the hood Django is doing exactly the same as every other framework out there, in almost exactly the same way but, we’re adding accidental complexity to that where other web frameworks don’t. We’re not setting up Django well for beginners.
This is typical: Django is considered "difficult to learn". Go read that piece. Same link: Our headline introductory tutorial is known as the "infamous polls tutorial".
You run startproject
as a beginner and the overwhelming first impression is "Wow, that’s a lot of files"1.
We’re not making it easy.
Due to this, there are various thoughts about presenting a much more trimmed down example, and then showing how a full Django app grows from that2.
The goal being a simpler on-ramp to building your first Django application.
§
At the same time, though, the Django Heroes that are active on the Discord and on the Forum, offering support to folks getting started, regularly report that users struggle — almost panic — when they encounter custom user models for the first time.
The usual evolution is that users have got going with the built-in auth.User
— they haven’t even considered it, and rightly so. They’ve built a decent amount of app, and then they’re reading the Django docs to level-up a bit. They come across this warning in the Customizing authentication docs:
If you’re starting a new project, it’s highly recommended to set up a custom user model, even if the default User model is sufficient for you. This model behaves identically to the default user model, but you’ll be able to customize it in the future if the need arises
"Highly recommended", "if the need arises" — Argh, I didn’t do any of these. And so panic.
What’s worse, just below that note, there’s a scary warning about how migrating to a custom user model mid-project is "significantly more difficult" that’s enough to put the heebie-jeebies up anyone.
Users think they’ve done wrong in not using a custom user model. They think the fix is beyond them. They’re worried they have to rebuild their whole project from scratch, and they come — or a small proportion of them, which itself isn’t small, comes — to the support channels looking for help.
Let me get ahead and put it down now, so as to be clear:
- At the absolute least that warning should be taken out of the docs. That “if the need arises”… — there is no need. The projects using
auth.User
are legion. They more than get by not ever changing that. Nothing of significance passes if you don’t use a custom user model when you start a new project. - That note about migrating mid-project can be toned down several levels. It’s a little bit involved, yes. But it’s not earth shattering. If the need arises, you can migrate to a custom user model with a little bit of effort.
We’re not doing anyone any favours leaving a warning that new users have no possibility of seeing before it’s well too late for them to consider. (That’s to leave aside whether it’s good advice, which of course, I don’t think it is.)
§
If we don’t back away from recommending custom user models, the thought is that the tutorial is doing it wrong.
Let’s contrast with Will Vincent’s Django for Beginners. Now in its 5th Edition, Django for Beginners is the canonical, up-to-date introduction to Django.
Django for Beginners does everything right here. It introduces authentication using auth.User
to show log-in, log-out, signup and so on. But then it needs to do it all again: after the first auth chapter, there’s a whole other one introducing the custom user model, and then a third that largely has to re-tread the ground of the first.
Now, there’s no other way if we’re going to have custom user models as the blessed approach—they need to be explained—and Will does it magnificently— but what a world we’re introducing people to. These are folks battling to get up and running with Django, and we’ve got them stuck in the weeds with auth. What a way we’ve found to rob their momentum!
Imagine if we didn’t need all that. Django gives you a User
model. It’s ready to go. You route these views. You add these templates. You’re done. Let’s get on with building our app! — That would be a much better story.
§
For the polls tutorial, the suggestion becomes, "Let’s add a custom user model to the default project template": alongside the existing project folder, add an additional app to hold a stub user model, and adjust the tutorial and so on to explain all that.
Beginners then wouldn’t find themselves without a user model.
But at that point I blink.
The solution to our overly complex tutorial, and default project template is to make it more complex? "Wow, you have a lot of files".
In any other circumstance we’d call that a code smell, an indication that we should think again.
I’ve long-thought, and sometimes argued, that we’ve internalised a suboptimal situation as being a feature here. I’ve described custom user models as being Django’s leaky battery. We should push the user model back to being Django’s responsibility and address that leak.
Django is nearly 20 years old. We’ve had custom user models for more than 10 of those. Do we stick with the status quo here for another 10? Another 20?
Custom user models are a complexity tax that most projects simply do not need. We should deemphasise them, and get rid of that.
§
The Performance Tax
The complexity angle wasn’t my primary reason for not liking custom users models, though. (It was a long time ago. I didn’t have that perspective.)
If you already know Django, using a custom user just is not that complex. Arguably it’s not complex at all. The standard example that almost everyone has is this:
# config/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
The link there is to the docs again. We extend AbstractUser
and this gives us an equivalent to auth.User
to be going on with.
My issue was always that pass
.
The primary job of the user model is to identify who is making the current request. In order to do that, a reference to the authenticated user is stored in the session and then the User record is fetched from the database on every single request.
The vast majority of Django projects certainly aren’t in the position where pulling a small amount of extra data from the database is a significant performance issue.
But the trouble is the user model becomes a dumping ground for miscellaneous user-related data from around your application.
We need some extra data for the user here. Oh, put a field on the user model.
In any given circumstance, putting that extra field on the user is almost always wrong—this isn’t auth related data—but it’s always going to be quicker than adding an appropriate profile model for that section of your app.
What do time-pressed developers do? They do the easy thing. They add the field to the user model. And then once it’s done once or twice, that’s the pattern: that’s just where we put such data. I’ve lost count of the projects I’ve come in on over the years where the user model was 10, 20, 30 fields and more.
This is madness. But it’s a pattern we encourage by saying that profile related data belongs on the User — by adding that pass
and saying Hey, add your fields here.
We spend any amount of time applying the advice from Django’s Database Access Optimization docs. There’s a whole section there, Don’t retrieve things you don’t need, that includes not fetching fields that you’re not going to use. Now, these optimisations are often marginal, but it’s hard to make the case that you’re going to apply them elsewhere when you’re fetching who-knows-what from the database on the critical path of effectively every single request.
The docs are so busy telling you that you should be using a custom user model that they pass over telling you that profile data belongs elsewhere.
Likely your team is clever enough not to do this. You won’t use your user model as a dumping ground.
But then this…
class User(AbstractUser):
pass
… what’s it for?
”Well, if I need to customise it…” — OK, but what customisation is that exactly? Profiles are different on every app, sure, but in all these years I never needed to customise the core authentication and identity role that the user model plays. Maybe there are projects that need such, I don’t know. But in the main it’s a knob that you're never meant to turn. So why have it?
Without discipline, those few lines are a foot-gun: they’re asking you to do it wrong, and to laden your user model with inappropriate app data. At best they’re a few lines of boilerplate — boilerplate that you’d never just let sit there anywhere else in your application. You’d delete them.
§
So what about profile data?
What was meant to be wrong with auth.User
in the first place?
Well, login by email was the main one, but let’s come back to that.
The other great issue — and this is the one people always bring up when I tell them I don’t use a custom user model — are the first_name
and last_name
profile fields that are defined on auth.User
.
First name/last name is just horribly anglocentric, and it doesn’t even really work then. In 2017 Russell Keith-Magee gave an amazing DjangoCon AU talk, called Red User, Blue User, MyUser, auth.User that just nails it on the head. If you haven’t seen it go watch now:
On Russell’s telling custom user models were the answer to how we’d get rid of those fields:
- Maybe you don’t need to collect name details at all.
- But if you do, a full name, and maybe a short name (for “What do we call you?”) is a much better pattern.
The idea was you’d define a custom user model and get rid of the inappropriate first_name
and last_name
pair.
But how did it turn out?
Quoting a recent Will Vincent tutorial on creating a Django Custom User Model:
There are two modern ways to create a custom user model in Django:
AbstractUser
andAbstractBaseUser
. In both cases, we can subclass them to extend existing functionality; however,AbstractBaseUser
requires much, much more work. Seriously, only mess with it if you know what you're doing. And if you did, you wouldn't be reading this tutorial, would you?
Most users — almost all users? — follow Will’s advice here, and the same advice that we already quoted from the Django docs, and subclass AbstractUser
:
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
The trouble is AbstractUser
itself is what defines the problematic first_name
and last_name
profile fields. (AbstractUser
is auth.User
’s superclass, where ≈all the work is done.)
The overwhelming majority of projects out there with a custom user model have exactly this starting point.
So, they have exactly the same profile related first_name
and last_name
fields that custom models were introduced to address. Nothing has changed. (Except, again, we have this extra boilerplate model, which gets in the way at best. A foot-gun asking to be played with.)
Oh, but you can change the superclass (to
BaseAbstractUser
) and then customise the fields.
Really? Can you? You’re a Django expert. Are really sure you could swap out the superclass here without making a mistake? Without having to dedicate a pretty decent amount of time?
I’ll tell you for certain I couldn’t.
And you’re an expert.
How far across the user base as a whole do you think that’s a realistic option? I’d suggest Tends to zero would be a pretty good first order approximation. It’s too hard, too time consuming, too risky. In general, teams just aren’t going to take it on.
And so, a decade later, the first_name
, last_name
fields haven’t gone anywhere. Custom user models, in practice, don’t address the problematic anglocentric naming fields that was one of their primary motivations.
If we actually wanted to address to anglocentric naming problem — if we wanted to shift the proportion of actual Django projects doing it right — then the better way would have been to say that no profile data should be kept on the user model.
We could then provide a canonical How-To in the docs showing a Profile model with example best-practice naming fields in place. Folks would copy and paste that on-mass, and we’d actually be moving the needle.
§
Interlude
So all of that is quite a lot — well done if you made it! — and maybe not quite as tight as I’d like, so let’s have a summary.
The complaint about custom user models is three-fold:
- They impose a complexity tax, especially for new users. Auth is a battery that Django should just provide. The need for customising auth is almost vanishingly niche. You shouldn’t have to think about it at all until you’re well on your way. You certainly shouldn’t get on your way and then find warnings suggesting that you’ve done it wrong.
- They impose a performance tax as teams laden the user model with app data that is fetched on every singe request. Teams that avoid this are left with a boilerplate model that they’re never going to do anything with.
- The issues with the anglocentric name fields are not resolved as most projects build off of
AbstractUser
even when using a custom user model.
My view is that we should, at the minimum, remove the recommendation to use custom user models, instead demonstrate best practices with a Profile How-To, and then work to actually address the (few) complaints with auth.User
, in order to address (what I’ve called) the leaky battery here.
It’s worth noting how small the gap is between what I’m arguing for here and the received position really is.
Go back to Russell’s DjangoCon AU talk. In the latter-half, when he’s discussing user models with one or more profile models attached… — well that’s just what I’m thinking about, that’s how I do it. The only difference is that I’d have exactly zero profile data on the user model itself. That’s the difference.
Given the discussion of whether you need profile data at all, and one or two other half-remarks in the talk, I wonder if profile fields on the user was just a byproduct of Django’s decision making process.
If you go back to the wiki document on updating contrib.auth
, the view I’m advocating is close to the Solution 5 there: to strip auth.User
down to little more than an identifier, to be used for authentication, and as the target of foreign keys from other models.
Turning away from criticism, that’s the way I see forward. So that’s what I want to round up by discussing. First, though, I just need to address one more set of responses that I get whenever I say that you should keep your profile data on a separate profile model.
§
Working with profile models
The are two issues that immediately come up when you mention using a profile model.
The first comment is often, ”Oh, but I need the profile data every request”.
I’d hope not but, the idea here is normally you have a bit in the corner of your UI that shows the logged-in user: their name/username, an avatar image and so on. That’s quite standard. The claim is that you need to fetch that data every request in order to render that part of the UI. (So it may as well be part of the user model is the conclusion.)
How often does that profile data change do you think? Once when the user first enters it. Maybe again because they made a typo. Maybe then, what, months or even years later?
What services do you use? GitHub, Mastodon, LinkedIn, …? How often do you update your profile? Is that anything like every request?
Rather than fetching that data every request, then, we should be leveraging Django’s in-built template fragment caching to render the little user avatar section once and then re-use that essentially forever, until the user makes an edit to their profile, when we can clear that cached fragment for next time.
The reality is profile data needs be fetched just once in a while in most normal usage.
Showing this pattern as part of a Profile How-To would be a real gateway into one of Django’s more powerful features.
The second issue is about creating the profile object for each user. I’ve got my user from the request. I go to access the profile, but it’s missing. What do I do there?
The standard approach is arguably to use signals. This is the approach Will Vincent shows in his tutorial on using a user profile model with Django.
With a signal you listen to post_save
and if you’ve got a newly created instance of your user model, you create a matching profile.
I’m on the record as avoiding signals so — following this kind of strategy — I’d do similar but create the profile in the user creation form, exactly next to the point where you create the user. It’s just the same, except the code is in the same place, so you don’t have to remember about the signal being triggered out-of-line.
Both of those approaches are totally fine. As a matter of fact I prefer to use the get_or_create()
method on my profile model’s manager when fetching a profile instance. The happy path there — where the profile exists — is exactly like a plain get()
and the or_create
fallback covers the first-access. That first access happens almost instantly in most cases: you either send the new user to a profile-edit
view, or render that avatar section of the UI, that pulls the profile model anyway. It’s the littlest tweak to use get_or_create()
, and I find it more than satisfactory. (Theoretically there’s a race-condition with get_or_create()
. "Theoretically". If you ever hit it, you’re obviously doing something right; I’m available for consulting work to help you resolve this in your project.)
Again, showing these approaches as part of a Profile How-To would let new users get set up right.
§
Evolving auth.User
Earlier we asked, what was meant to be wrong with auth.User
in the first place?
Those issues haven’t been addressed, and if we were to back away from the recommendation of custom user models, we’d need to finally take them on.
Making changes to a core Django model, whilst maintaining the API stability policy, is no mean feat. It’s something that I think we gave up on too early with auth.User
. I’ve been slowly experimenting with ideas for what might be a way forward. Thus far it’s but suggestions and proofs-of-concept, but I’m hoping it shows that the problems are not intractable.
The main complaint really, which we paused on when we mentioned it earlier, was login by email. Most people wanted nothing else.
Here I introduced my package django-unique-user-email
. It enables login-by-email with the default User model for your Django project by making auth.User.email
unique, providing an authentication backend, and an authentication form to pass the right credentials down.
django-unique-user-email
is officially at proof-of-concept stage, but it works, and it’s a pattern I’ve used in projects for years. The essence of it is a simple and reversible bolt-on migration that’s entirely compatible with the existing code. (The migration itself is optional: as long as you enforce uniqueness at the form level, authentication doesn’t actually require a constraint in the DB, but it’s much better to push the data integrity down to that level.)
In contrast to custom user models, adding a constraint looks positively lightweight.
For those profile fields, first_name
and last_name
I’m currently noodling on a similar idea to simply allow removing them, as a bolt-on option. (I’m hoping to have a proof-of-concept for around DjangoCon US this year.) At very least, it looks plausible to add a database level default to the existing fields, so there’s no need to provide a value for them, and then remove them from the model layer. (That’s even if we can’t just remove the columns entirely. And that’s even being done as an extension rather than in core.)
I want to be able to finally say that, not just some but, most, or even all, new Django projects are created without those first_name
and last_name
fields in play. If we could push this forward we can get there.
Then those admin fields, is_staff
, is_superuser
. It seems plausible to me that we could move those into a StaffMember
profile model in django.contrib.admin
— where the is_staff
check is just Does an instance exist for this user? I’m not wrapped up in this, but it would be much neater.
That would leave just the date_joined
/last_login
fields, which can stay, and then the credentials fields, password
, username
and email
. These could stay but some folks want login only by email, some only by username, some by both. (I like both, since you asked.)
We should make that an option. django-allauth
(for example) allows configuring login-just-by-email, but then it needs to populate the username
field behind the scenes with some unique value, because that’s not optional.
We could maybe pull these field off of the user model into separate credentials models . By itself that may not be worth it: rather a couple of bolt-on migrations to tweak the field how you need them would be the way to go.
However, auth has long-gone beyond just email/username and password. I single sign-on from any number of providers. I have TOTP 2fa from my password manager. I have a passkey powered by touch on my laptop, and face scanning on my phone, and to be honest who knows where all that is going to go?
If we were to separate credentials from the user model itself, we could create a pluggable interface, in the Django way, to add credential implementations as needed, without being limited to what already existed. (In our actual case, to what was contemporary when Django was first born.)
Now, there’s a lot there. It won’t happen quickly. But that general path seems to me to be more Django.
§
Action points
At the least I’d like us to remove the highly recommended line about custom user models. Even if you think I’m mad, you must see that having that there is harmful for beginners.
At the same time we should tone down the warning about migrating to a custom user model mid-project. I’ve argued there’s no reason to do it, but even so, it’s not that hard. We can say it’s involved, without giving people heart attacks.
Adding a Profile How-To to the docs would be an easy win too.
- Show a best-practice model — with decent name fields.
- Even if we can’t (yet) remove the existing fields, tell folks they’re outdated (at best) and say it’s OK to just ignore them.
- Show creating the profile instance — with a signal, inline, or using
get_or_create()
- Give an introduction to template fragment caching.
And then, can we advance auth.User
? I think we can. django-unique-user-email
is almost no code at all. Surely Django can support login-by-email out of the box? The other points are still more speculative, but surely it’s not unaddressable? I’m here for the long-haul with Django. I want to fix these issues, not just leave them.
I need help on this last. I can make suggestions. I can play with ideas. But if we’re going to adopt them in core then it needs buy-in, and the hard bits will need thinking through.
-
And it's not even a beginner reaction if we’re honest. Amber Brown remarked exactly that during their 2019 DjangoCon US keynote. ↩
-
I linked the Using Django as a Microframework talk above. Paolo Melchiorre wrote up an evolution of that after the DjangoCon US Sprints last year. There have been various related takes.
In the questions after my DjangoCon Europe talk in Vigo this year, Daniele Procida raised the explicit prospect of using a much compact Hello, world! as the starting point, and growing the application from there.
For the last few years I’ve advocated The Single Folder Project Layout for Django that eliminates the need for the immediate separate app folder.
And so on. There are irons in the fire, even if they take time to come to heat. ↩