Forms: User Input and Validation
Welcome to Forms! Think of forms as conversations with your users - they ask for information, validate it, and process it safely. Forms are how users interact with your web applications.
HTML Forms Basics
Let’s start with a simple HTML form:
<!-- templates/contact.html -->
<!DOCTYPE html>
<html>
<head>
<title>Contact Us</title>
</head>
<body>
<h1>Contact Us</h1>
<form action="/contact" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required><br>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required><br>
<label for="message">Message:</label>
<textarea id="message" name="message" required></textarea><br>
<input type="submit" value="Send Message">
</form>
</body>
</html>
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/contact', methods=['GET', 'POST'])
def contact():
if request.method == 'POST':
name = request.form.get('name')
email = request.form.get('email')
message = request.form.get('message')
# Process the form data
print(f"Message from {name} ({email}): {message}")
return "Thank you for your message!"
return render_template('contact.html')
if __name__ == '__main__':
app.run(debug=True)
Flask-WTF for Better Forms
Flask-WTF makes forms much easier and more secure:
pip install flask-wtf
from flask import Flask, render_template, flash, redirect, url_for
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email, Length
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'
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')
@app.route('/contact', methods=['GET', 'POST'])
def contact():
form = ContactForm()
if form.validate_on_submit():
# Form is valid, process the data
name = form.name.data
email = form.email.data
message = form.message.data
# Here you would typically save to database or send email
print(f"Message from {name} ({email}): {message}")
flash('Your message has been sent!', 'success')
return redirect(url_for('contact'))
return render_template('contact.html', form=form)
if __name__ == '__main__':
app.run(debug=True)
<!-- templates/contact.html -->
{% extends "base.html" %}
{% block content %}
<h1>Contact Us</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.name.label }}
{{ form.name(class="form-control") }}
{% if form.name.errors %}
<div class="error">
{% for error in form.name.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-control") }}
{% if form.email.errors %}
<div class="error">
{% for error in form.email.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-group">
{{ form.message.label }}
{{ form.message(class="form-control") }}
{% if form.message.errors %}
<div class="error">
{% for error in form.message.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
{% endblock %}
Form Fields and Validators
Common Field Types
from wtforms import (
StringField, TextAreaField, PasswordField,
IntegerField, FloatField, BooleanField,
SelectField, SelectMultipleField, RadioField,
FileField, DateField, DateTimeField
)
from wtforms.validators import (
DataRequired, Email, EqualTo, Length,
NumberRange, URL, Regexp, Optional
)
class UserForm(FlaskForm):
# Text fields
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=20)])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
confirm_password = PasswordField('Confirm Password',
validators=[DataRequired(), EqualTo('password')])
# Text area
bio = TextAreaField('Biography', validators=[Length(max=500)])
# Numbers
age = IntegerField('Age', validators=[NumberRange(min=13, max=120)])
height = FloatField('Height (cm)', validators=[NumberRange(min=50, max=250)])
# Choices
gender = SelectField('Gender', choices=[
('', 'Select gender'),
('male', 'Male'),
('female', 'Female'),
('other', 'Other')
])
interests = SelectMultipleField('Interests', choices=[
('sports', 'Sports'),
('music', 'Music'),
('reading', 'Reading'),
('travel', 'Travel')
])
# Boolean
newsletter = BooleanField('Subscribe to newsletter')
# Date
birth_date = DateField('Birth Date', validators=[DataRequired()])
submit = SubmitField('Register')
Custom Validators
from wtforms.validators import ValidationError
import re
def phone_number_validator(form, field):
"""Validate phone number format."""
pattern = re.compile(r'^\+?1?[-.\s]?\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})$')
if not pattern.match(field.data):
raise ValidationError('Invalid phone number format.')
def strong_password_validator(form, field):
"""Validate strong password requirements."""
password = field.data
if len(password) < 8:
raise ValidationError('Password must be at least 8 characters long.')
if not re.search(r'[A-Z]', password):
raise ValidationError('Password must contain at least one uppercase letter.')
if not re.search(r'[a-z]', password):
raise ValidationError('Password must contain at least one lowercase letter.')
if not re.search(r'[0-9]', password):
raise ValidationError('Password must contain at least one number.')
class RegistrationForm(FlaskForm):
phone = StringField('Phone', validators=[DataRequired(), phone_number_validator])
password = PasswordField('Password', validators=[DataRequired(), strong_password_validator])
submit = SubmitField('Register')
File Uploads
Handle file uploads securely:
import os
from werkzeug.utils import secure_filename
from flask import current_app
class UploadForm(FlaskForm):
file = FileField('File', validators=[DataRequired()])
submit = SubmitField('Upload')
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif'}
@app.route('/upload', methods=['GET', 'POST'])
def upload():
form = UploadForm()
if form.validate_on_submit():
file = form.file.data
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)
flash('File uploaded successfully!', 'success')
return redirect(url_for('upload'))
else:
flash('Invalid file type.', 'error')
return render_template('upload.html', form=form)
Form Processing Patterns
Create, Read, Update, Delete (CRUD)
# Mock database
users = []
class UserForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
submit = SubmitField()
@app.route('/users')
def list_users():
return render_template('users/list.html', users=users)
@app.route('/users/create', methods=['GET', 'POST'])
def create_user():
form = UserForm()
if form.validate_on_submit():
user = {
'id': len(users) + 1,
'name': form.name.data,
'email': form.email.data
}
users.append(user)
flash('User created successfully!', 'success')
return redirect(url_for('list_users'))
return render_template('users/create.html', form=form)
@app.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
def edit_user(user_id):
user = next((u for u in users if u['id'] == user_id), None)
if not user:
abort(404)
form = UserForm(obj=user) # Pre-populate form
if form.validate_on_submit():
user['name'] = form.name.data
user['email'] = form.email.data
flash('User updated successfully!', 'success')
return redirect(url_for('list_users'))
return render_template('users/edit.html', form=form, user=user)
@app.route('/users/<int:user_id>/delete', methods=['POST'])
def delete_user(user_id):
global users
users = [u for u in users if u['id'] != user_id]
flash('User deleted successfully!', 'success')
return redirect(url_for('list_users'))
CSRF Protection
Flask-WTF automatically protects against CSRF attacks:
# In your template
<form method="POST">
{{ form.hidden_tag() }} <!-- CSRF token -->
<!-- Your form fields -->
</form>
# In your app configuration
app.config['WTF_CSRF_ENABLED'] = True
app.config['WTF_CSRF_SECRET_KEY'] = 'your-csrf-secret-key'
Form Rendering with Bootstrap
Make forms look professional:
<!-- templates/base.html -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- templates/user_form.html -->
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="mb-4">{{ title }}</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.name.label(class="form-label") }}
{{ form.name(class="form-control" + (" is-invalid" if form.name.errors else "")) }}
{% if form.name.errors %}
<div class="invalid-feedback">
{% for error in form.name.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }}
{% if form.email.errors %}
<div class="invalid-feedback">
{% for error in form.email.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
</div>
</div>
</div>
{% endblock %}
Practical Examples
Example 1: Registration Form
from flask import Flask, render_template, redirect, url_for, flash
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import DataRequired, Email, EqualTo, Length
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
confirm_password = PasswordField('Confirm Password',
validators=[DataRequired(), EqualTo('password')])
accept_terms = BooleanField('I accept the terms and conditions',
validators=[DataRequired()])
submit = SubmitField('Sign Up')
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# Here you would create the user account
flash(f'Account created for {form.username.data}!', 'success')
return redirect(url_for('home'))
return render_template('register.html', title='Register', form=form)
@app.route('/')
def home():
return render_template('home.html')
if __name__ == '__main__':
app.run(debug=True)
Example 2: Contact Form with Email
from flask import Flask, render_template, flash, redirect, url_for
from flask_wtf import FlaskForm
from flask_mail import Mail, Message
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = 'your-email@gmail.com'
app.config['MAIL_PASSWORD'] = 'your-password'
mail = Mail(app)
class ContactForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
subject = StringField('Subject', validators=[DataRequired()])
message = TextAreaField('Message', validators=[DataRequired()])
submit = SubmitField('Send')
@app.route('/contact', methods=['GET', 'POST'])
def contact():
form = ContactForm()
if form.validate_on_submit():
msg = Message(
subject=f"Contact Form: {form.subject.data}",
sender=form.email.data,
recipients=['your-email@gmail.com'],
body=f"""
From: {form.name.data} <{form.email.data}>
{form.message.data}
"""
)
mail.send(msg)
flash('Your message has been sent!', 'success')
return redirect(url_for('contact'))
return render_template('contact.html', form=form)
if __name__ == '__main__':
app.run(debug=True)
Example 3: Search Form
from flask import Flask, render_template, request
from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, SubmitField
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
class SearchForm(FlaskForm):
query = StringField('Search')
category = SelectField('Category', choices=[
('all', 'All Categories'),
('books', 'Books'),
('electronics', 'Electronics'),
('clothing', 'Clothing')
])
sort_by = SelectField('Sort by', choices=[
('relevance', 'Relevance'),
('price_low', 'Price: Low to High'),
('price_high', 'Price: High to Low'),
('newest', 'Newest First')
])
submit = SubmitField('Search')
# Mock search results
products = [
{'id': 1, 'name': 'Python Book', 'category': 'books', 'price': 29.99},
{'id': 2, 'name': 'Laptop', 'category': 'electronics', 'price': 999.99},
{'id': 3, 'name': 'T-Shirt', 'category': 'clothing', 'price': 19.99},
]
@app.route('/search', methods=['GET', 'POST'])
def search():
form = SearchForm()
results = []
if form.validate_on_submit() or request.method == 'GET':
query = form.query.data or request.args.get('q', '')
category = form.category.data or request.args.get('category', 'all')
# Filter results
results = products
if query:
results = [p for p in results if query.lower() in p['name'].lower()]
if category != 'all':
results = [p for p in results if p['category'] == category]
return render_template('search.html', form=form, results=results)
if __name__ == '__main__':
app.run(debug=True)
Best Practices
1. Always Validate Input
# Good
class SafeForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=100)])
# Bad - No validation
name = request.form.get('name') # Could be anything!
2. Use CSRF Protection
# Always include in templates
<form method="POST">
{{ form.hidden_tag() }}
<!-- form fields -->
</form>
3. Handle File Uploads Securely
from werkzeug.utils import secure_filename
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg'}
if file and allowed_file(secure_filename(file.filename)):
# Safe to save
pass
4. Use Flash Messages
# In route
flash('Success message!', 'success')
flash('Error message!', 'error')
# In template
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
5. Redirect After POST
# POST-REDIRECT-GET pattern
if form.validate_on_submit():
# Process form
flash('Success!', 'success')
return redirect(url_for('some_route')) # Redirect prevents double submission
Practice Exercises
Exercise 1: User Registration
Create a registration system with:
- Username, email, password fields
- Password confirmation
- Terms acceptance checkbox
- Email validation
- Password strength requirements
Exercise 2: Blog Comment System
Build a comment form with:
- Name and email fields
- Comment text area
- CAPTCHA or simple math verification
- Comment moderation (approve/reject)
- Nested replies
Exercise 3: Product Review Form
Create a product review system with:
- Star rating (1-5)
- Review title and text
- Photo upload
- Review filtering and sorting
- Average rating calculation
Exercise 4: Survey Application
Build a multi-page survey with:
- Different question types (text, radio, checkbox, select)
- Progress indicator
- Save progress functionality
- Results summary page
- Export responses to CSV
Summary
Forms handle user input and validation in Flask applications:
Basic Form Handling:
@app.route('/submit', methods=['GET', 'POST'])
def submit():
if request.method == 'POST':
data = request.form.get('field_name')
# Process data
return render_template('form.html')
Flask-WTF Forms:
class MyForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
submit = SubmitField('Submit')
@app.route('/form', methods=['GET', 'POST'])
def my_form():
form = MyForm()
if form.validate_on_submit():
# Process valid form
return redirect(url_for('success'))
return render_template('form.html', form=form)
Key Concepts:
- Form validation with WTForms
- CSRF protection
- File uploads
- Flash messages for feedback
- POST-REDIRECT-GET pattern
Next: Databases - storing and retrieving data with SQLAlchemy! 💾