Forms & Validation in Flask

Forms are a fundamental part of web applications, allowing users to submit data to your application. This guide covers how to create, validate, and process forms in Flask applications.

Important
Always validate form data on both the client and server side. Client-side validation provides immediate feedback, but server-side validation is essential for security.

Introduction to Flask-WTF

Flask-WTF is a Flask extension that integrates WTForms, making it easy to create and validate forms in Flask applications.

1. Installation

Install Flask-WTF using pip:

pip install Flask-WTF

2. Basic Configuration

Configure Flask-WTF in your application:

# In your app.py or __init__.py file
from flask import Flask
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'  # Required for CSRF protection
csrf = CSRFProtect(app)  # Enable CSRF protection globally

3. Creating a Simple Form

Define a form class in a separate file (e.g., forms.py):

# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email, Length

class ContactForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired(), Length(min=2, max=50)])
    email = StringField('Email', validators=[DataRequired(), Email()])
    message = TextAreaField('Message', validators=[DataRequired(), Length(min=10, max=500)])
    submit = SubmitField('Send Message')

Displaying Forms in Templates

Once you've defined your forms, you need to display them in your templates. Here's how to render forms in Jinja2 templates:

1. Form Template

Create a template to render your form:

<!-- templates/contact.html -->
{% extends 'base.html' %}

{% block content %}
    <h1>Contact Us</h1>
    <form method="POST" action="{{ url_for('contact') }}">
        {{ form.csrf_token }}
        
        <div class="form-group">
            {{ form.name.label }}
            {{ form.name(class="form-control") }}
            {% if form.name.errors %}
                <div class="text-danger">
                    {% for error in form.name.errors %}
                        {{ error }}
                    {% endfor %}
                </div>
            {% endif %}
        </div>
        
        <div class="form-group">
            {{ form.email.label }}
            {{ form.email(class="form-control") }}
            {% if form.email.errors %}
                <div class="text-danger">
                    {% for error in form.email.errors %}
                        {{ error }}
                    {% endfor %}
                </div>
            {% endif %}
        </div>
        
        <div class="form-group">
            {{ form.message.label }}
            {{ form.message(class="form-control") }}
            {% if form.message.errors %}
                <div class="text-danger">
                    {% for error in form.message.errors %}
                        {{ error }}
                    {% endfor %}
                </div>
            {% endif %}
        </div>
        
        {{ form.submit(class="btn btn-primary") }}
    </form>
{% endblock %}

2. Route Function

Create a route function to handle the form:

# routes.py
from flask import render_template, flash, redirect, url_for
from . import app
from .forms import ContactForm

@app.route('/contact', methods=['GET', 'POST'])
def contact():
    form = ContactForm()
    
    if form.validate_on_submit():
        # Process form data
        name = form.name.data
        email = form.email.data
        message = form.message.data
        
        # Do something with the data (e.g., save to database, send email)
        # ...
        
        flash('Your message has been sent!', 'success')
        return redirect(url_for('contact'))
    
    return render_template('contact.html', form=form)

Custom Validation

WTForms provides several built-in validators, but sometimes you need to create custom validation rules.

1. Custom Validators

Create custom validators for specific validation needs:

# Custom validator function
from wtforms.validators import ValidationError

def validate_username(form, field):
    if field.data.lower() in ['admin', 'root', 'superuser']:
        raise ValidationError('This username is reserved.')

# Using custom validator in a form
class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[
        DataRequired(),
        Length(min=3, max=20),
        validate_username
    ])
    # other fields...

2. Validation within Form Class

Add validation methods to your form class for complex validation rules:

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired(), Length(min=3, max=20)])
    password = PasswordField('Password', validators=[DataRequired(), Length(min=8)])
    confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
    
    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user:
            raise ValidationError('Username already taken. Please choose a different one.')

File Uploads

Flask-WTF makes it easy to handle file uploads in your forms.

1. Creating a File Upload Form

# forms.py
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed
from wtforms import SubmitField

class UploadForm(FlaskForm):
    photo = FileField('Upload Photo', validators=[
        FileRequired(),
        FileAllowed(['jpg', 'png', 'jpeg'], 'Images only!')
    ])
    submit = SubmitField('Upload')

2. Processing File Uploads

# routes.py
import os
from werkzeug.utils import secure_filename
from flask import current_app

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()
    
    if form.validate_on_submit():
        f = form.photo.data
        filename = secure_filename(f.filename)
        upload_path = os.path.join(current_app.root_path, 'static', 'uploads')
        
        # Ensure the upload directory exists
        os.makedirs(upload_path, exist_ok=True)
        
        # Save the file
        f.save(os.path.join(upload_path, filename))
        
        flash('File uploaded successfully!', 'success')
        return redirect(url_for('upload'))
    
    return render_template('upload.html', form=form)

AJAX Forms

You can use JavaScript to submit forms asynchronously without page reloads for a better user experience.

1. HTML Form with AJAX

<!-- templates/ajax_form.html -->
{% extends 'base.html' %}

{% block content %}
    <h1>Contact Us</h1>
    <form id="contact-form" method="POST" action="{{ url_for('api.contact') }}">
        {{ form.csrf_token }}
        
        <div class="form-group">
            {{ form.name.label }}
            {{ form.name(class="form-control") }}
            <div class="error-message" id="name-error"></div>
        </div>
        
        <div class="form-group">
            {{ form.email.label }}
            {{ form.email(class="form-control") }}
            <div class="error-message" id="email-error"></div>
        </div>
        
        <div class="form-group">
            {{ form.message.label }}
            {{ form.message(class="form-control") }}
            <div class="error-message" id="message-error"></div>
        </div>
        
        <button type="submit" class="btn btn-primary">Send Message</button>
    </form>
    
    <div id="form-success" class="alert alert-success mt-3" style="display: none;">
        Your message has been sent!
    </div>
{% endblock %}

{% block scripts %}
    {{ super() }}
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const form = document.getElementById('contact-form');
            
            form.addEventListener('submit', function(e) {
                e.preventDefault();
                
                // Reset error messages
                document.querySelectorAll('.error-message').forEach(el => {
                    el.textContent = '';
                });
                
                // Get form data
                const formData = new FormData(form);
                
                // Submit form via AJAX
                fetch(form.action, {
                    method: 'POST',
                    body: formData,
                    headers: {
                        'X-Requested-With': 'XMLHttpRequest'
                    }
                })
                .then(response => response.json())
                .then(data => {
                    if (data.success) {
                        // Show success message
                        document.getElementById('form-success').style.display = 'block';
                        form.reset();
                    } else {
                        // Display validation errors
                        if (data.errors) {
                            Object.keys(data.errors).forEach(field => {
                                const errorEl = document.getElementById(`${field}-error`);
                                if (errorEl) {
                                    errorEl.textContent = data.errors[field].join(', ');
                                }
                            });
                        }
                    }
                })
                .catch(error => {
                    console.error('Error:', error);
                });
            });
        });
    </script>
{% endblock %}

2. API Route for AJAX Form

# routes/api.py
from flask import Blueprint, request, jsonify
from .forms import ContactForm

api = Blueprint('api', __name__)

@api.route('/api/contact', methods=['POST'])
def contact():
    form = ContactForm()
    
    if request.is_xhr:  # Check if it's an AJAX request
        if form.validate_on_submit():
            # Process form data
            # ...
            
            return jsonify({
                'success': True,
                'message': 'Your message has been sent!'
            })
        else:
            return jsonify({
                'success': False,
                'errors': form.errors
            })
    
    # Handle non-AJAX request (fallback)
    if form.validate_on_submit():
        # Process form data
        # ...
        
        flash('Your message has been sent!', 'success')
        return redirect(url_for('contact'))
    
    return render_template('contact.html', form=form)

Security Considerations

Risk Prevention
Cross-Site Request Forgery (CSRF) Always include the CSRF token in your forms (This is a dummy token for demonstration purposes)
Cross-Site Scripting (XSS) Validate and sanitize user input before displaying it
SQL Injection Use parameterized queries or ORM (e.g., SQLAlchemy) for database operations
Malicious File Uploads Validate file extensions, content types, and scan for malware
Mass Assignment Explicitly specify which form fields to accept when processing forms
Security Warning
Never trust data coming from the client. Always validate and sanitize user input on the server side, even if you have client-side validation.

Best Practices

  • Organize forms in separate modules: Keep your forms organized in dedicated modules (e.g., forms.py)
  • Use macros for form rendering: Create reusable macros for rendering form fields to avoid repetition
  • Provide clear error messages: Help users understand what went wrong and how to fix it
  • Use appropriate field types: WTForms provides many field types for different data (e.g., EmailField, IntegerField)
  • Add client-side validation: Enhance user experience with immediate feedback, but always rely on server-side validation
  • Use form inheritance: Create base forms with common fields and inherit from them
  • Test your forms: Write unit tests for your form validation logic
  • Handle form errors gracefully: Display validation errors clearly and preserve user input