Django's Field Choices Don't Constrain Your Data2020-01-22
This post is a PSA on the somewhat unintuitive way
Field.choices works in Django.
Take this Django model definition:
If we open up a shell to manipulate them, we can easily create a
Book with a given status choice:
choices list constrains the value of
status during model validation in Python:
This is great for
ModelForms and other cases using validation.
Users can’t select invalid choices and get messaging about what’s wrong.
Unfortunately, it’s still easy for us, as developers, to write this invalid data to the database:
It’s also possible to update all our instances to an invalid status in one line:
So, what gives?
Why does Django let us declare the set of
choices we want the field to take, but then let us easily circumvent that?
Well, Django’s model validation is designed mostly for forms. It trusts that other code paths in your application “know what they’re doing.”
If we want to prevent this, the most general solution is to get the database itself to reject bad data. Not only will this make your Django code more robust, but any other applications using the database will use the constraints too.
We can add such constraints using
CheckConstraint class, added in Django 2.2.
For our model, we need define and name a single filter
Q object represents a single expression we’d pass into
Constraints can have any amount of logic on the fields in the current model.
This includes all kinds of lookups, comparisons between fields, and database functions.
makemigrations, we get a migration that looks like this:
If we try to apply this while the database contains invalid data, it will fail:
If we clean that data up manually and try again, it will pass:
From that point on, the database won’t allow us to insert invalid rows, or update the valid rows to be invalid:
Currently Django doesn’t have a way of showing these
IntegrityErrors to users in model validation.
Nothing will catch and turn them into
ValidationErrors which can carry user-facing messages.
As per the documentation:
In general constraints are not checked during
full_clean(), and do not raise
There’s an open ticket #30581 to improve this.
In our case, since we are still using
choices, this is okay.
Validation already won’t allow users to select invalid statuses.
For more complex constraints, we might want to duplicate the logic in Python with a custom validator.
Check constraints are really neat. Having the data constrained at the lowest level possible gives us the strongest guarantees of its quality.
I hope this post helps you consider using them,
Want better tests? Check out my book Speed Up Your Django Tests which teaches you to write faster, more accurate tests.
One summary email a week, no spam, I pinky promise.
- Working Around Memory Leaks in Your Django Application
- How to Add Database Modifications Beyond Migrations to Your Django Project
- "Create Table As Select" in Django
© 2021 All rights reserved.