Introduction to Forms
Forms are the primary way users interact with web applications. They allow users to submit data like login credentials, search queries, comments, and file uploads. Understanding form handling is essential for building interactive web applications.
How Forms Work
HTML forms collect user input and send it to the server. When a form is submitted, the browser packages the form data and sends it using either GET (data in URL) or POST (data in request body). Flask receives this data through the request object and processes it in your view function.
Why it matters: Forms enable two-way communication between users and your application, transforming static pages into interactive experiences.
Form Submission Flow
The form submission lifecycle: user fills in data, submits via POST, server validates input, and returns an appropriate response with feedback.
GET vs POST Methods
GET Method
- Data appended to URL as query string
- Visible in browser address bar
- Can be bookmarked
- Limited data size (URL length)
- Cached by browsers
POST Method
- Data sent in request body
- Not visible in URL
- Cannot be bookmarked
- No size limitation
- Not cached
HTML Forms in Templates
Create HTML forms in your Jinja2 templates with proper action URLs and method attributes. Use Flask's url_for() to generate form action URLs dynamically.
Basic Form Structure
<!-- templates/login.html -->
<form action="{{ url_for('login') }}" method="POST">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
The action attribute specifies where to send data, method specifies GET or POST. Each input needs a name attribute to identify it on the server.
Common Form Input Types
| Input Type | Purpose | Example |
|---|---|---|
text |
Single-line text | <input type="text" name="name"> |
email |
Email with validation | <input type="email" name="email"> |
password |
Masked input | <input type="password" name="pwd"> |
number |
Numeric input | <input type="number" name="qty"> |
textarea |
Multi-line text | <textarea name="message"></textarea> |
select |
Dropdown list | <select name="country">...</select> |
Complete Registration Form
<!-- templates/register.html -->
<form action="{{ url_for('register') }}" method="POST">
<input type="text" name="username" placeholder="Username" required>
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<select name="country">
<option value="">Select Country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="pk">Pakistan</option>
</select>
<textarea name="bio" placeholder="Tell us about yourself"></textarea>
<button type="submit">Register</button>
</form>
Include various input types based on the data you need. Use the required attribute for client-side validation.
Handling Form Submissions
Flask receives form data through the request object. Use request.form for POST data and request.args for GET data. Handle both displaying the form and processing submissions in a single route.
Basic Form Handler
from flask import Flask, render_template, request, redirect, url_for
app = Flask(__name__)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
# Process login (simplified)
if username == 'admin' and password == 'secret':
return redirect(url_for('dashboard'))
else:
return render_template('login.html', error='Invalid credentials')
# GET request - show form
return render_template('login.html')
The route handles both GET (display form) and POST (process submission). Use request.form.get() for safe access with default None for missing keys.
Accessing Form Data
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
# Access individual fields
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
# Access with default value
newsletter = request.form.get('newsletter', 'no')
# Get all form data as dictionary
all_data = request.form.to_dict()
# Process registration...
return redirect(url_for('success'))
return render_template('register.html')
Use .get() with a default value to handle optional fields. The .to_dict() method converts all form data to a Python dictionary.
Handling GET Form (Search)
@app.route('/search')
def search():
query = request.args.get('q', '')
page = request.args.get('page', 1, type=int)
if query:
# Perform search
results = perform_search(query, page)
return render_template('results.html', results=results, query=query)
return render_template('search.html')
GET forms use request.args instead of request.form. The type parameter in .get() automatically converts the value.
Form Validation
Always validate form data on the server side. Client-side validation improves user experience, but server-side validation ensures security since client-side checks can be bypassed.
Basic Server-Side Validation
@app.route('/register', methods=['GET', 'POST'])
def register():
errors = []
if request.method == 'POST':
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
# Validation checks
if not username:
errors.append('Username is required')
elif len(username) < 3:
errors.append('Username must be at least 3 characters')
if not email or '@' not in email:
errors.append('Valid email is required')
if len(password) < 8:
errors.append('Password must be at least 8 characters')
if not errors:
# Save user and redirect
return redirect(url_for('success'))
return render_template('register.html', errors=errors)
Collect all errors before deciding to proceed. Pass errors to the template to display feedback to the user.
Displaying Errors in Template
<!-- templates/register.html -->
{% if errors %}
<div class="error-messages">
<ul>
{% for error in errors %}
<li class="text-danger">{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form action="{{ url_for('register') }}" method="POST">
<input type="text" name="username"
value="{{ request.form.get('username', '') }}">
<!-- Preserves user input on error -->
</form>
Loop through errors to display them prominently. Preserve form values using request.form.get() so users do not have to re-enter data after an error.
Email Validation Helper
import re
def is_valid_email(email):
"""Simple email validation"""
pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
return re.match(pattern, email) is not None
def validate_registration(form_data):
"""Validate registration form data"""
errors = {}
if not form_data.get('username'):
errors['username'] = 'Username is required'
if not is_valid_email(form_data.get('email', '')):
errors['email'] = 'Invalid email format'
return errors
Flash Messages
Flash messages provide one-time notifications to users after actions like form submissions. They persist across redirects and are automatically removed after being displayed once.
Setting Flash Messages
from flask import Flask, flash, redirect, url_for, render_template
app = Flask(__name__)
app.secret_key = 'your-secret-key-here' # Required for sessions
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username == 'admin' and password == 'secret':
flash('Login successful! Welcome back.', 'success')
return redirect(url_for('dashboard'))
else:
flash('Invalid username or password.', 'error')
return render_template('login.html')
The flash() function stores a message in the session. The second argument is a category (success, error, warning, info) for styling.
app.secret_key to be set. Use a secure random string in production.
Displaying Flash Messages
<!-- templates/base.html -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages">
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
<button class="close">×</button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
Place flash message display in your base template so they appear on all pages. Use with_categories=true to access the category for styling.
Flash Message Categories
# Success message
flash('Account created successfully!', 'success')
# Error message
flash('Please correct the errors below.', 'error')
# Warning message
flash('Your session will expire in 5 minutes.', 'warning')
# Info message
flash('New features are now available!', 'info')
Security Considerations
Form handling introduces security risks. Protect your application from common attacks like CSRF, XSS, and SQL injection with proper security measures.
CSRF Protection
Cross-Site Request Forgery (CSRF) attacks trick users into submitting forms on your site without their knowledge. Protect against this with CSRF tokens.
# Using Flask-WTF for CSRF protection
# pip install flask-wtf
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
app.secret_key = 'your-secret-key'
csrf = CSRFProtect(app)
Flask-WTF provides easy CSRF protection. Once enabled, all forms require a CSRF token.
Adding CSRF Token to Forms
<!-- With Flask-WTF installed -->
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- or simply -->
{{ form.csrf_token }}
<!-- rest of form -->
</form>
Include the CSRF token as a hidden field in every form. The server validates this token on submission.
Input Sanitization
from markupsafe import escape
@app.route('/comment', methods=['POST'])
def add_comment():
# Escape HTML to prevent XSS
comment = escape(request.form.get('comment', ''))
# Strip whitespace
username = request.form.get('username', '').strip()
# Limit length
bio = request.form.get('bio', '')[:500]
# Use parameterized queries for database (prevents SQL injection)
# cursor.execute("INSERT INTO users (name) VALUES (?)", (username,))
return redirect(url_for('comments'))
Practice Exercises
Apply your form handling knowledge with these hands-on exercises covering submissions, validation, and flash messages.
Form Handling Practice
Task: Create a contact form with name, email, and message fields. Display submitted data on success.
Show Solution
# app.py
@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')
return f'Thanks {name}! We received your message.'
return render_template('contact.html')
<!-- templates/contact.html -->
<form method="POST">
<input type="text" name="name" placeholder="Name" required>
<input type="email" name="email" placeholder="Email" required>
<textarea name="message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>
Task: Create a search form that uses GET method. Display the search query on the results page.
Show Solution
# app.py
@app.route('/search')
def search():
query = request.args.get('q', '')
if query:
return f'Search results for: {query}'
return render_template('search.html')
<!-- templates/search.html -->
<form method="GET" action="{{ url_for('search') }}">
<input type="text" name="q" placeholder="Search...">
<button type="submit">Search</button>
</form>
Task: Create a registration form with username, email, password validation. Show errors on the form.
Show Solution
# app.py
@app.route('/register', methods=['GET', 'POST'])
def register():
errors = []
if request.method == 'POST':
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
if len(username) < 3:
errors.append('Username must be 3+ characters')
if '@' not in email:
errors.append('Invalid email')
if len(password) < 8:
errors.append('Password must be 8+ characters')
if not errors:
return redirect(url_for('success'))
return render_template('register.html', errors=errors)
Task: Add success and error flash messages to a login form. Display them in the template.
Show Solution
# app.py
from flask import flash
app.secret_key = 'dev-secret-key'
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
if request.form.get('username') == 'admin':
flash('Welcome back, admin!', 'success')
return redirect(url_for('dashboard'))
flash('Invalid credentials', 'error')
return render_template('login.html')
<!-- templates/login.html -->
{% for cat, msg in get_flashed_messages(with_categories=true) %}
<div class="alert alert-{{ cat }}">{{ msg }}</div>
{% endfor %}
Task: When validation fails, preserve the user's input so they do not have to re-enter it.
Show Solution
<!-- templates/register.html -->
<form method="POST">
<input type="text" name="username"
value="{{ request.form.get('username', '') }}"
placeholder="Username">
<input type="email" name="email"
value="{{ request.form.get('email', '') }}"
placeholder="Email">
<!-- Password typically not preserved for security -->
<input type="password" name="password" placeholder="Password">
<button type="submit">Register</button>
</form>
Task: Create a two-step registration: Step 1 collects name/email, Step 2 collects password. Store step 1 data in session.
Show Solution
# app.py
from flask import session
@app.route('/register/step1', methods=['GET', 'POST'])
def register_step1():
if request.method == 'POST':
session['reg_name'] = request.form.get('name')
session['reg_email'] = request.form.get('email')
return redirect(url_for('register_step2'))
return render_template('register_step1.html')
@app.route('/register/step2', methods=['GET', 'POST'])
def register_step2():
if 'reg_name' not in session:
return redirect(url_for('register_step1'))
if request.method == 'POST':
password = request.form.get('password')
# Create user with session['reg_name'], session['reg_email'], password
session.pop('reg_name', None)
session.pop('reg_email', None)
flash('Registration complete!', 'success')
return redirect(url_for('login'))
return render_template('register_step2.html')
Task: Create a form where dropdown options come from a data source. Handle the selection on submit.
Show Solution
# app.py
categories = [
{'id': 1, 'name': 'Technology'},
{'id': 2, 'name': 'Science'},
{'id': 3, 'name': 'Arts'}
]
@app.route('/article/new', methods=['GET', 'POST'])
def new_article():
if request.method == 'POST':
title = request.form.get('title')
category_id = request.form.get('category', type=int)
category = next((c for c in categories if c['id'] == category_id), None)
flash(f'Article "{title}" created in {category["name"]}', 'success')
return redirect(url_for('articles'))
return render_template('new_article.html', categories=categories)
<!-- templates/new_article.html -->
<form method="POST">
<input type="text" name="title" placeholder="Article Title">
<select name="category">
{% for cat in categories %}
<option value="{{ cat.id }}">{{ cat.name }}</option>
{% endfor %}
</select>
<button type="submit">Create</button>
</form>
Task: Create a subscription form that validates email format using regex and displays inline errors.
Show Solution
# app.py
import re
def is_valid_email(email):
pattern = r'^[\w\.-]+@[\w\.-]+\.\w{2,}$'
return re.match(pattern, email) is not None
@app.route('/subscribe', methods=['GET', 'POST'])
def subscribe():
error = None
if request.method == 'POST':
email = request.form.get('email', '').strip()
if not email:
error = 'Email is required'
elif not is_valid_email(email):
error = 'Please enter a valid email address'
else:
flash('Subscribed successfully!', 'success')
return redirect(url_for('home'))
return render_template('subscribe.html', error=error)
Task: Install Flask-WTF and add CSRF protection to a login form. Handle CSRF errors gracefully.
Show Solution
# app.py
from flask_wtf.csrf import CSRFProtect, CSRFError
app = Flask(__name__)
app.secret_key = 'your-secret-key'
csrf = CSRFProtect(app)
@app.errorhandler(CSRFError)
def handle_csrf_error(e):
flash('Form expired. Please try again.', 'error')
return redirect(request.url)
<!-- templates/login.html -->
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="text" name="username" placeholder="Username">
<input type="password" name="password" placeholder="Password">
<button type="submit">Login</button>
</form>
Key Takeaways
Forms Enable Interaction
HTML forms let users submit data. Use GET for retrieval (search), POST for modifications (login, create)
Request Object Access
Use request.form for POST data and request.args for GET parameters. The .get() method provides safe access
Server-Side Validation
Always validate on the server. Client validation is for UX only - it can be bypassed
Flash Messages
Provide user feedback with flash(). Messages persist across redirects and display once
CSRF Protection
Protect forms from CSRF attacks with tokens. Flask-WTF makes this easy with CSRFProtect
Sanitize Input
Escape HTML, strip whitespace, limit lengths, and use parameterized queries for security
Knowledge Check
Quick Quiz
Test what you've learned about forms and validation