How It Works Features Pricing Blog Error Guides
Log In Start Free Trial
Django · Python

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

Traceback (most recent call last): File "/app/contacts/views.py", line 18, in submit_contact form.save() File "/app/contacts/forms.py", line 25, in save self.instance.full_clean() File "/venv/lib/python3.11/site-packages/django/db/models/base.py", line 1465, in full_clean self._perform_field_validation(exclude) File "/venv/lib/python3.11/site-packages/django/db/models/base.py", line 1507, in _perform_field_validation setattr(self, f.attname, f.clean(raw_value, self)) File "/venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py", line 755, in clean self.run_validators(value) File "/venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py", line 710, in run_validators raise ValidationError(errors) django.core.exceptions.ValidationError: ['Enter a valid email address.']

Here's what each line means:

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.

Before (broken)
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})
After (fixed)
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:

  1. Notice the error alert or see it in your monitoring tool
  2. Open the error dashboard and read the stack trace
  3. Identify the file and line number from the stack trace
  4. Open your IDE and navigate to the file
  5. Read the surrounding code to understand context
  6. Reproduce the error locally
  7. Identify the root cause
  8. Write the fix
  9. Run the test suite locally
  10. Fix any failing tests
  11. Write new tests covering the edge case
  12. Run the full test suite again
  13. Create a new git branch
  14. Commit and push your changes
  15. Open a pull request
  16. Wait for code review
  17. Merge and deploy to production
  18. 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:

  1. Captures the stack trace and request context
  2. Pulls the relevant source files from your GitHub repo
  3. Analyzes the error and understands the code context
  4. Generates a minimal, verified fix
  5. Runs your existing test suite
  6. Pushes through your CI/CD pipeline
  7. 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)

  1. Run the full test suite locally to confirm validation errors are displayed.
  2. Open a pull request with the view and template fixes.
  3. Wait for CI checks to pass on the PR.
  4. Have a teammate review and approve the PR.
  5. 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.