Files
jhauga 27149859a4 Add skill to create web forms
Add skill to create web forms
2026-02-09 19:22:54 -05:00

12 KiB

Python Flask Forms Reference

Source: https://testdriven.io/courses/learn-flask/forms/

This reference covers how to work with forms in Flask, including handling GET and POST requests, using WTForms for form creation and validation, implementing CSRF protection, and managing file uploads.


Overview

Flask provides tools for handling web forms through:

  • The request object for accessing submitted form data
  • Flask-WTF and WTForms for declarative form creation, validation, and CSRF protection
  • Jinja2 templates for rendering form HTML
  • Flash messages for user feedback

Basic Form Handling in Flask

Handling GET and POST Requests

from flask import Flask, render_template, request, redirect, url_for, flash

app = Flask(__name__)
app.secret_key = 'your-secret-key'

@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!', 'success')
            return redirect(url_for('dashboard'))
        else:
            flash('Invalid credentials.', 'error')

    return render_template('login.html')

The request.form Object

The request.form is an ImmutableMultiDict that contains parsed form data from POST and PUT requests.

Method Description
request.form['key'] Access a value; raises 400 Bad Request if missing
request.form.get('key') Access a value; returns None if missing
request.form.get('key', 'default') Access a value with a fallback default
request.form.getlist('key') Returns a list of all values for a key (for multi-select fields)

The request.method Attribute

Used to distinguish between GET (displaying the form) and POST (processing the submission):

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        # Process the form submission
        pass
    # GET: Display the form
    return render_template('register.html')

Flask-WTF and WTForms

Installation

pip install Flask-WTF

Flask-WTF is a Flask extension that integrates WTForms. It provides:

  • CSRF protection out of the box
  • Integration with Flask's request object
  • Jinja2 template helpers
  • File upload support

Configuration

from flask import Flask

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'  # Required for CSRF
app.config['WTF_CSRF_ENABLED'] = True          # Enabled by default

Defining Forms with WTForms

Basic Form Class

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[
        DataRequired(),
        Length(min=3, max=25)
    ])
    email = StringField('Email', validators=[
        DataRequired(),
        Email()
    ])
    password = PasswordField('Password', validators=[
        DataRequired(),
        Length(min=6)
    ])
    confirm_password = PasswordField('Confirm Password', validators=[
        DataRequired(),
        EqualTo('password', message='Passwords must match.')
    ])
    submit = SubmitField('Register')

Common Field Types

Field Type Description
StringField Single-line text input
PasswordField Password input (masked characters)
TextAreaField Multi-line text input
IntegerField Integer input with built-in type coercion
FloatField Float input with built-in type coercion
BooleanField Checkbox (True/False)
SelectField Dropdown select menu
SelectMultipleField Multiple-select dropdown
RadioField Radio button group
FileField File upload input
HiddenField Hidden input field
SubmitField Submit button
DateField Date picker input

Common Validators

Validator Description
DataRequired() Field must not be empty
Email() Must be a valid email format
Length(min, max) String length must fall within range
EqualTo('field') Must match another field's value
NumberRange(min, max) Numeric value must fall within range
Regexp(regex) Must match the provided regular expression
URL() Must be a valid URL
Optional() Field is allowed to be empty
InputRequired() Raw input data must be present
AnyOf(values) Must be one of the provided values
NoneOf(values) Must not be any of the provided values

Using Forms in Routes

Route with WTForms

@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()

    if form.validate_on_submit():
        # form.validate_on_submit() checks:
        # 1. Is the request method POST?
        # 2. Does the form pass all validation?
        # 3. Is the CSRF token valid?

        username = form.username.data
        email = form.email.data
        password = form.password.data

        # Process the data (e.g., save to database)
        flash(f'Account created for {username}!', 'success')
        return redirect(url_for('login'))

    return render_template('register.html', form=form)

validate_on_submit() Method

This method combines two checks:

  1. request.method == 'POST' -- ensures the form was actually submitted
  2. form.validate() -- runs all validators on the form fields and checks the CSRF token

Returns True only if both conditions are met.


Rendering Forms in Templates

Basic Template Rendering

<form method="POST" action="{{ url_for('register') }}">
    {{ form.hidden_tag() }}

    <div>
        {{ form.username.label }}
        {{ form.username(class="form-control", placeholder="Enter username") }}
        {% for error in form.username.errors %}
            <span class="error">{{ error }}</span>
        {% endfor %}
    </div>

    <div>
        {{ form.email.label }}
        {{ form.email(class="form-control", placeholder="Enter email") }}
        {% for error in form.email.errors %}
            <span class="error">{{ error }}</span>
        {% endfor %}
    </div>

    <div>
        {{ form.password.label }}
        {{ form.password(class="form-control") }}
        {% for error in form.password.errors %}
            <span class="error">{{ error }}</span>
        {% endfor %}
    </div>

    <div>
        {{ form.confirm_password.label }}
        {{ form.confirm_password(class="form-control") }}
        {% for error in form.confirm_password.errors %}
            <span class="error">{{ error }}</span>
        {% endfor %}
    </div>

    {{ form.submit(class="btn btn-primary") }}
</form>

Key Template Elements

Element Purpose
{{ form.hidden_tag() }} Renders the hidden CSRF token field
{{ form.field.label }} Renders the <label> element for the field
{{ form.field() }} Renders the <input> element for the field
{{ form.field(class="...") }} Renders the input with additional HTML attributes
{{ form.field.errors }} List of validation error messages for the field
{{ form.field.data }} The current value of the field

CSRF Protection

How CSRF Protection Works

Flask-WTF automatically includes CSRF protection:

  1. A unique token is generated per session and embedded as a hidden form field.
  2. When the form is submitted, Flask-WTF verifies the token matches the session token.
  3. If the token is missing or invalid, the request is rejected with a 400 Bad Request.

Including the CSRF Token

In your template, always include one of these:

<!-- Option 1: Hidden tag (includes CSRF + all hidden fields) -->
{{ form.hidden_tag() }}

<!-- Option 2: CSRF token only -->
{{ form.csrf_token }}

CSRF for AJAX Requests

For JavaScript-based form submissions:

<meta name="csrf-token" content="{{ csrf_token() }}">
fetch('/api/submit', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
    },
    body: JSON.stringify(data)
});

Custom Validators

Inline Custom Validator

Define a method on the form class with the naming pattern validate_<fieldname>:

from wtforms import ValidationError

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired(), Email()])

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

    def validate_email(self, field):
        # Check if email already exists in database
        if User.query.filter_by(email=field.data).first():
            raise ValidationError('That email is already registered.')

Reusable Custom Validator

from wtforms import ValidationError

def validate_no_special_chars(form, field):
    if not field.data.isalnum():
        raise ValidationError('Field must contain only letters and numbers.')

class MyForm(FlaskForm):
    username = StringField('Username', validators=[
        DataRequired(),
        validate_no_special_chars
    ])

File Uploads

Form with File Upload

from flask_wtf.file import FileField, FileAllowed, FileRequired

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

Handling File Uploads in Routes

import os
from werkzeug.utils import secure_filename

UPLOAD_FOLDER = 'static/uploads'

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()

    if form.validate_on_submit():
        file = form.photo.data
        filename = secure_filename(file.filename)
        filepath = os.path.join(UPLOAD_FOLDER, filename)
        file.save(filepath)
        flash('File uploaded successfully!', 'success')
        return redirect(url_for('upload'))

    return render_template('upload.html', form=form)

Multipart Form Encoding

File upload forms must use enctype="multipart/form-data":

<form method="POST" enctype="multipart/form-data">
    {{ form.hidden_tag() }}
    {{ form.photo.label }}
    {{ form.photo() }}
    {{ form.submit() }}
</form>

Flash Messages

Setting Flash Messages

from flask import flash

flash('Operation successful!', 'success')
flash('An error occurred.', 'error')
flash('Please check your input.', 'warning')

Displaying Flash Messages in Templates

{% 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 %}

Key Takeaways

  1. Use Flask-WTF for form handling -- it provides CSRF protection, validation, and clean form definitions.
  2. validate_on_submit() is the primary method for checking both submission and validation in one call.
  3. Always include {{ form.hidden_tag() }} in templates to enable CSRF protection.
  4. Use WTForms validators for clean, declarative server-side validation.
  5. Custom validators can be defined inline on the form class or as reusable functions.
  6. Flash messages provide user feedback for form actions.
  7. File uploads require enctype="multipart/form-data" and should use secure_filename() for safety.
  8. Set a SECRET_KEY in your Flask config -- it is required for CSRF tokens and session management.