Module 10.3

Form Handling

Forms let users interact with your web application. Learn to create HTML forms, handle form submissions, validate user input, and provide feedback with flash messages. Build interactive, secure web applications.

50 min
Intermediate
Hands-on
What You'll Learn
  • Create HTML forms in templates
  • Handle GET and POST form submissions
  • Validate and sanitize user input
  • Use flash messages for feedback
  • Implement CSRF protection
Contents
01

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.

Key Concept

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
User Fills
HTML Form
Submit
POST Request
Validate
Check Data
Response
Success/Error

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
Use for: Search forms, filters, pagination
POST Method
  • Data sent in request body
  • Not visible in URL
  • Cannot be bookmarked
  • No size limitation
  • Not cached
Use for: Login, registration, data modification
02

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.

03

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.

04

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
05

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.

Important: Flash messages require 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">&times;</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')
06

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'))
Security Best Practices: Always validate on the server, escape user input before display, use parameterized database queries, and never trust client-side validation alone.
07

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

1 Which HTTP method should be used for form submissions that modify data?
2 How do you access POST form data in Flask?
3 What is the purpose of flash() in Flask?
4 What attack does CSRF protection prevent?
5 Why is server-side validation important even with client-side validation?
6 What does request.form.get('field', '') do if 'field' is not submitted?
Answer all questions to check your score