I could not find the the correct solution or path to custom form validation in Django, so I will quickly write up my findings (for me to check later on and maybe they help you too). Keep on reading↡ if you want to follow my path of discovery, jump to the working answer or to the working snippet right away EDIT: or to even better improved snippet.

flask-wtforms-style

Initially i wanted to impose the validation flask-wtforms-style, since this is how i learned form validation first. Turns out i wasn’t quite able to do that. The idea was to put all my validators in a validators.py file in my app. Import the used (would say all) validators into my forms.py and impose them onto form fields where needed. This did not work for me!

validators.py

1from django.core.exceptions import ValidationError
2
3
4def end_smaller_than_start(time_end, time_start):
5    if time_start >= time_end:
6        raise ValidationError(
7            f"End time {time_end} should be after start time {time_start}"
8        )

forms.py

 1from django import forms
 2
 3from django.utils import timezone
 4
 5from .validators import end_smaller_than_start
 6
 7
 8class FillPapelitoForm(forms.Form):
 9    equipment_type = forms.CharField(max_length=150, required=True)
10    time_start = forms.TimeField(required=True)
11    time_end = forms.TimeField(required=True, validators=[end_smaller_than_start])

models.py
I did not put any specific constraints into my models.py, but for the sake of completeness:

1from django.db import models
2from django.utils import timezone
3
4# Create your models here.
5class PapelitoReport(models.Model):
6    equipment_type = models.CharField(max_length=150)
7    time_start = models.TimeField()
8    time_end = models.TimeField()

Note: I left out some models, fields that are not important in proving my point here.

While this did in some way validate I couldn’t get the code to do what I wanted in the validator. What I wanted was to make sure that time_end was bigger, i.e. after, time_start, since I want the duration of the usage for the equipment. So my logical approach would be to check in the validator:

if time_end smaller than time_start:
    throw an error

However for some reason I couldn’t make out the validator validated the other way round. That is, I had <= (smaller or equal than) in my validator but it only validated in my expected way if I put >= (bigger or equal than) in the validator. some quick examples:

*first example*
time_start: 08:00
time_end: 16:00
--> error!

*second example*
time_start: 08:00
time_end: 07:00
--> **no** error

Since I wanted my code to clearly represent what it does and what I want from it, I had to come up with some other way to custom validate those two values. I tried and tried and searched and searched and finally came up with the:

working answer

Based on this answer on Stack Overflow by User Igor I was able to validate my values in the way I wanted. The answer states that one should not describe fields in forms.py, but nonetheless some special ones can be described in forms.py. The answer states the following solution in
forms.py

 1from models import Student
 2
 3class StudentForm(forms.ModelForm):
 4    (...)
 5    (fields are defined here)
 6    (I left them out since the answer uses a different approach to define fields)
 7    (...)
 8    
 9    # Now you could describe all validation methods
10    def clean_first_name(self):
11        first_name = self.cleaned_data['first_name']
12        if not first_name.isalpha():
13            return ValidationError('First name must contain only letters')
14        return first_name

What this does is it it checks the cleaned_data1 returned on form submission, from the dictionary it grabs the values needed, validates them and returns a descriptive ValidationError. If no error is found (the question was asking for how to validate that the field first_name contains only alphanumerical values), then the same, unchanged value is returned.

This did work more or less. There was just a small error that prevented this from working for me. And I think also for the original question poster (there is a comment below Igor’s solution stating: It still isn't validating(...)). In line 13 of the code part above Igor returns a ValidationError. This did not work for me. I changed the return to raise and … it worked fine!

So now on to the

working snippet

for my case based on the answer above:

forms.py

 1from django import forms
 2
 3from django.core import validators
 4
 5class FillPapelitoForm(forms.Form):
 6    equipment_type = forms.CharField(max_length=150, required=True)
 7    time_start = forms.TimeField(required=True)
 8    time_end = forms.TimeField(required=True)
 9
10    def clean_time_end(self):
11        time_end = self.cleaned_data["time_end"]
12        time_start = self.cleaned_data["time_start"]
13        if time_end <= time_start:
14            raise forms.ValidationError(
15                f"Error: End Time {time_end} must be after Start time {time_start}"
16            )
17        return time_end

Here the validator does what is in the code and what I want it to check (that if end is smaller than start, there should be an error) and raises the error (instead of returning it).

note: I submitted an edit to the Stack Overflow answer, which is at submission of this post still in review.

Edit and addition

Thanks to the kind comment from Dan over at fosstodon I further improved my snippet. It seems like my version above ran a validation before each field was successfully validated in the “basic” way (e.g. the field was checked to be not empty). That’s why one should implement the validation inside of the django-provided clean function (make sure to not forget the call on line 21 to fetch the cleaned_data). The validation then is moved inside an if block that checks if both required values actually are present in the cleaned_data dictionary, that is in the variables we stored their values in (as can be seen on line 25).

 1from django import forms
 2
 3from django.core.exceptions import ValidationError
 4
 5
 6class FillPapelitoForm(forms.Form):
 7    equipment_type = forms.CharField(max_length=150, required=True)
 8    time_start = forms.TimeField(required=True)
 9    time_end = forms.TimeField(required=True)
10
11    def clean(self):
12        cleaned_data = super().clean()
13        time_end = cleaned_data.get("time_end")
14        time_start = cleaned_data("time_start")
15
16        if time_start and time_end:
17            if time_end <= time_start:
18                raise ValidationError(
19                    f"Error: End Time {time_end} must be after Start time {time_start}"
20                )

Dan refers in his comment to this section of the official Django documentation: cleaning and validating fields that depend on each other

The official snippet looks like this:

 1from django import forms
 2from django.core.exceptions import ValidationError
 3
 4
 5class ContactForm(forms.Form):
 6    # Everything as before.
 7    ...
 8
 9    def clean(self):
10        cleaned_data = super().clean()
11        cc_myself = cleaned_data.get("cc_myself")
12        subject = cleaned_data.get("subject")
13
14        if cc_myself and subject:
15            # Only do something if both fields are valid so far.
16            if "help" not in subject:
17                raise ValidationError(
18                    "Did not send for 'help' in the subject despite " "CC'ing yourself."
19                )

Sidenote: a .get call needs () parenthesis and will not work with [] brackets.


  1. cleaned_data is a dictionary-like object that stores the cleaned form data after validation. In Django, when a user submits a form, the data is automatically validated by the framework. During this process, Django checks if the data entered in the form is valid and in the correct format. If it’s valid, Django stores the cleaned data in the cleaned_data dictionary.
    The cleaned_data is passed to the view function as an argument and can be used to create or update models, generate reports or perform any other operations required. The cleaned_data dictionary is also used to re-populate the form fields in case there is any validation error, so the user doesn’t have to re-enter the data again. ↩︎