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.
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 |
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