In Django form validation, why should a ValidationError carry a code and params rather than a pre-formatted message?
answer
- machines versus humans
- stable across translations
- interpolation happens later
- error_messages keyed by code
- lists and dicts of errors
basics
~10 sA Django ValidationError's code lets tests, views and error_messages overrides identify the error without parsing text, and params let the message be translated and reworded with placeholders filled in at render time.
solid answer
~40 sDjango's documentation recommends `ValidationError(_('Only %(max)s guests per room.'), code='too_many_guests', params={'max': 4})`. The **code** is a stable identifier: tests assert it with `has_error()`, views branch on it through `errors.as_data()`, JSON clients receive it, and it survives translation and rewording. The **params** are interpolated only when the message is rendered, so translators can move or drop `%(max)s`, and the message stays overridable. Codes also drive `error_messages`: when a validator run by a field raises with code `min_value`, Django swaps in the field's `error_messages['min_value']` if one is set. A pre-formatted f-string loses all of that. You can also raise a list of errors, or, from `Form.clean()` only, a dict keyed by field names.
code
python · 23 linesfrom django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
MAX_GUESTS = {'single': 1, 'double': 2, 'family': 4}
class BookingForm(forms.Form):
room_type = forms.ChoiceField(choices=[(k, k.title()) for k in MAX_GUESTS])
guests = forms.IntegerField(
min_value=1,
error_messages={'min_value': _('Book at least one guest.')},
)
def clean(self):
cleaned = super().clean()
room, guests = cleaned.get('room_type'), cleaned.get('guests')
if room and guests and guests > MAX_GUESTS[room]:
self.add_error('guests', ValidationError(
_('A %(room)s room holds at most %(max)s guests.'),
code='too_many_guests',
params={'room': room, 'max': MAX_GUESTS[room]},
))
return cleanedgo deeper
Recall the constructor: message, code and params, and that you should always give a code.
Explain how params are interpolated at render time, how error_messages replaces a validator's message by code, and where list and dict errors are allowed.
Show you design reusable validators and fields with stable codes, test by code, and keep messages translatable and overridable.
Treat error codes as part of the product's contract with front ends and translators, and set conventions for naming and stability.
## Anatomy of a ValidationError `django.core.exceptions.ValidationError(message, code=None, params=None)` is the one exception every layer of Django form validation raises. It carries three things: - **`message`**: human-readable text, ideally marked for translation, with `%(name)s` placeholders. - **`code`**: a short machine-readable identifier such as `'too_many_guests'`. - **`params`**: a dictionary whose values fill the placeholders when the message is displayed. The message can also be a **list** of errors or a **dict** mapping field names to errors, which is how several problems travel in one exception. ## The documented guidelines Django's form-validation documentation spells out four rules for flexible messages: 1. Give every error a descriptive `code`, so programs can recognise it independently of the wording. 2. Do not interpolate values into the message yourself; use placeholders and `params`. 3. Use named placeholders (`%(max)s`) with a dict, not positional `%s` with a tuple, so translators can reorder or omit them. 4. Wrap the text in `gettext` so it can be translated. Putting it together for a hotel booking form: ```python from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ raise ValidationError( _('A %(room)s room holds at most %(max)s guests.'), code='too_many_guests', params={'room': 'double', 'max': 2}, ) ``` ## What the code buys you | Consumer | Uses the code to | |---|---| | Tests | `form.has_error('guests', code='too_many_guests')` survives rewording | | Views | branch on `form.errors.as_data()` without parsing text | | Script clients | `get_json_data()` sends `code` next to `message` | | Field `error_messages` | replace a validator's message by code | The last row is easy to miss. Inside `Field.run_validators()`, if a validator raises an error whose `code` is a key in the field's `error_messages`, Django replaces the message. So `forms.IntegerField(min_value=1, error_messages={'min_value': 'Book at least one guest.'})` rewrites `MinValueValidator`'s text, because that validator raises with code `'min_value'`. The swap happens in the field's validator step (built-in field errors such as `required` and `invalid` also read `error_messages`); an error you raise yourself in `clean_guests()` is not rewritten, so give it the final text. ## Multiple errors and where dicts are allowed - Raise `ValidationError([error1, error2])` to report several problems for one field at once. - Raise `ValidationError({'check_out': error, 'guests': other})` to target fields, but **only from `Form.clean()`**. Django routes per-field errors through `add_error(fieldname, error)`, which raises `TypeError` when it receives a dict together with a field name, so a dict raised from `clean_guests()` crashes the request instead of showing a message. - Inside `clean()`, `self.add_error()` is often clearer than a dict because it does not stop the method. ## Reading an error back A `ValidationError` exposes its contents in shapes that match how it was built: - `error.messages`: a flat list of rendered strings, with `params` interpolated. - `error.message_dict`: a field-to-messages mapping, available only for dict-shaped errors; on any other error it raises `AttributeError`. - `error.error_list`: the individual `ValidationError` objects, each with its own `code` and `params`. Interpolation happens when the error is iterated or rendered, which is why the untouched `params` stay available to code that inspects the objects. ## When a plain message is acceptable The documentation concedes that at the very end of the chain, in your own form's `clean()`, a message you will never need to override can be formatted directly. Reusable fields, validators and forms shared between apps should always follow the full pattern. ## What interviewers listen for That codes are for machines and messages for people, that params keep translation and overrides working, the `error_messages` swap by code, and the dict-only-in-`clean()` rule.
- In Django, why does raising ValidationError with a dict from clean_guests() fail?Django catches errors from per-field cleaning and calls `add_error('guests', error)`. `add_error()` refuses a dict-shaped error when a field name is given and raises `TypeError`, which escapes as a server error. Dicts keyed by field are only valid from `Form.clean()`, where the field argument is `None`.
- Should a Django form use gettext or gettext_lazy for ValidationError messages?Messages built inside a method at validation time can use `gettext`, because the request's language is active then. Messages defined at import time, such as `error_messages` in a field declaration, need `gettext_lazy` so translation happens when the message is displayed rather than when the module loads.
saying these in an interview costs you the question
- Formatting values into the message with an f-string is the recommended style.
- error_messages on a field only affects the required and invalid errors.
- A dict of field errors can be raised from any clean_<field>() method.
- The error code changes when the message is translated.
- ValidationError can only carry one message at a time.