Fix ValidationError: ['Enter a valid email address.'] in Django
This error means a form field's clean method or validator rejected the input data. Fix it by checking your form's clean methods return cleaned data correctly, ensuring custom validators raise ValidationError with user-friendly messages, and verifying the form is re-rendered with errors so users can see what went wrong.
Reading the Stack Trace
Here's what each line means:
- File "/app/contacts/views.py", line 18, in submit_contact: The view calls form.save() without checking form.is_valid() first, which skips form-level validation.
- File "/app/contacts/forms.py", line 25, in save: The form's save method calls full_clean on the model instance, triggering field-level validation that raises the error.
- raise ValidationError(errors): Django's field validator rejected the value because it does not match the expected email format.
Common Causes
1. Calling save() without is_valid()
The view saves the form without first calling is_valid(), so validation errors become unhandled exceptions instead of form errors.
def submit_contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
form.save() # Crashes if validation fails
return redirect('thanks')
2. Custom clean method not returning cleaned_data
A custom clean method validates data but forgets to return the cleaned value, causing the field to become None.
class ContactForm(forms.ModelForm):
def clean_phone(self):
phone = self.cleaned_data['phone']
if not phone.startswith('+'):
raise forms.ValidationError('Phone must start with country code.')
# Missing: return phone
3. ValidationError not displayed in template
The template does not render form errors, so the user sees a blank form after a validation failure with no feedback.
<!-- contact.html -->
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<!-- No {{ form.errors }} or {{ form.non_field_errors }} -->
<button type="submit">Send</button>
</form>
The Fix
Always call form.is_valid() before form.save(). When validation fails, re-render the form with errors so users see what went wrong. Django's form rendering automatically shows per-field error messages when the form has errors.
def submit_contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
form.save() # No is_valid() check
return redirect('thanks')
else:
form = ContactForm()
return render(request, 'contact.html', {'form': form})
def submit_contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
form.save()
return redirect('thanks')
# Form is re-rendered with errors
else:
form = ContactForm()
return render(request, 'contact.html', {'form': form})
# In template:
# <form method="post">
# {% csrf_token %}
# {{ form.non_field_errors }}
# {{ form.as_p }}
# <button type="submit">Send</button>
# </form>
Testing the Fix
import pytest
from django.test import TestCase, Client
class TestContactForm(TestCase):
def setUp(self):
self.client = Client()
def test_invalid_email_shows_error(self):
response = self.client.post('/contact/', {
'name': 'Alice',
'email': 'not-an-email',
'message': 'Hello',
})
assert response.status_code == 200
self.assertContains(response, 'Enter a valid email')
def test_valid_form_redirects(self):
response = self.client.post('/contact/', {
'name': 'Alice',
'email': 'alice@example.com',
'message': 'Hello',
})
assert response.status_code == 302
def test_empty_form_shows_required_errors(self):
response = self.client.post('/contact/', {})
assert response.status_code == 200
self.assertContains(response, 'This field is required')
Run your tests:
pytest
Pushing Through CI/CD
git checkout -b fix/form-validation-error-handling,git add contacts/views.py contacts/forms.py templates/contact.html,git commit -m "fix: check is_valid() before save and display form errors",git push origin fix/form-validation-error-handling
Your CI config should look something like this:
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- run: pip install -r requirements.txt
- run: pytest --tb=short -q
The Full Manual Process: 18 Steps
Here's every step you just went through to fix this one bug:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- Monitor production to confirm the error is resolved
Total time: 30-60 minutes. For one bug.
Or Let bugstack Fix It in Under 2 minutes
Every step above? bugstack does it automatically.
Step 1: Install the SDK
pip install bugstack
Step 2: Initialize
import bugstack
bugstack.init(api_key=os.environ["BUGSTACK_API_KEY"])
Step 3: There is no step 3.
bugstack handles everything from here:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- Deploys to production (or opens a PR for review)
Time from error to fix deployed: Under 2 minutes.
Human involvement: zero.
Try bugstack Free →No credit card. 5-minute setup. Cancel anytime.
Deploying the Fix (Manual Path)
- Run the full test suite locally to confirm validation errors are displayed.
- Open a pull request with the view and template fixes.
- Wait for CI checks to pass on the PR.
- Have a teammate review and approve the PR.
- Merge to main and verify form validation works correctly in staging.
Frequently Asked Questions
BugStack runs the fix through your existing test suite, generates tests for valid, invalid, and edge-case inputs, and validates that error messages display correctly before marking it safe to deploy.
BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.
Yes. Pass a message parameter to ValidationError, or set error_messages on the form field. For model fields, use the error_messages argument in the field definition.
Override the form's clean() method to access all cleaned_data fields and raise ValidationError for cross-field validation. Errors raised here appear in form.non_field_errors.