Skip to main content

Creating Forms with Flask-WTF

Creating Forms with Flask-WTF

Flask-WTF is a Flask extension that simplifies the creation, validation, and processing of web forms by integrating the WTForms library with Flask. It provides tools for generating HTML forms, handling user input, validating data, and protecting against security threats like Cross-Site Request Forgery (CSRF). Flask-WTF is particularly useful in data-driven applications, such as dashboards or machine learning (ML) interfaces using Pandas, where forms collect user inputs for processing or visualization. This guide covers creating forms with Flask-WTF, including setup, form creation, validation, rendering, best practices, and practical examples, with a focus on data-driven use cases.


01. Overview of Flask-WTF

Flask-WTF extends WTForms to provide seamless form handling in Flask applications. It automates tasks like form rendering, data validation, and CSRF protection, making it easier to build secure and user-friendly forms.

  • Purpose: Simplify form creation, validation, and processing while ensuring security.
  • Key Features: Form classes, built-in validators, CSRF protection, and Jinja2 integration.
  • Use Cases: Collecting user inputs for ML predictions, filtering Pandas DataFrames, or submitting configuration data.

1.1 Key Components

  • Form Classes: Defined using FlaskForm to specify fields and validators.
  • Fields: Input types like StringField, IntegerField, or FileField.
  • Validators: Rules like DataRequired or Length to enforce input constraints.
  • CSRF Protection: Automatically included to prevent cross-site request forgery.

02. Setting Up Flask-WTF

2.1 Installation

Install Flask-WTF using pip:

pip install flask-wtf

2.2 Project Structure

project/
├── app.py
├── static/
│   ├── css/
│   │   └── style.css
├── templates/
│   ├── base.html
│   ├── form.html
│   └── result.html
└── uploads/

2.3 Configuring Flask-WTF

Flask-WTF requires a secret key for CSRF protection and session management.

Example: Basic Flask-WTF Setup

File: app.py

from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired

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

class NameForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired()])
    submit = SubmitField('Submit')

@app.route('/form', methods=['GET', 'POST'])
def form():
    form = NameForm()
    if form.validate_on_submit():
        name = form.name.data
        return render_template('result.html', name=name)
    return render_template('form.html', form=form)

if __name__ == '__main__':
    app.run(debug=True)

File: templates/base.html

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Flask-WTF App{% endblock %}</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    <div class="container mt-3">
        {% block content %}{% endblock %}
    </div>
</body>
</html>

File: static/css/style.css

body {
    font-family: Arial, sans-serif;
}
.form-group {
    margin-bottom: 15px;
}
.error {
    color: red;
}

File: templates/form.html

{% extends 'base.html' %}

{% block title %}Name Form{% endblock %}

{% block content %}
    <h1>Enter Your Name</h1>
    <form method="post">
        {{ form.hidden_tag() }}
        <div class="form-group">
            {{ form.name.label }}
            {{ form.name(class="form-control") }}
            {% if form.name.errors %}
                {% for error in form.name.errors %}
                    <span class="error">{{ error | escape }}</span>
                {% endfor %}
            {% endif %}
        </div>
        {{ form.submit(class="btn btn-primary") }}
    </form>
{% endblock %}

File: templates/result.html

{% extends 'base.html' %}

{% block title %}Result{% endblock %}

{% block content %}
    <h1>Hello, {{ name | title | escape }}!</h1>
    <a href="{{ url_for('form') }}" class="btn btn-secondary">Back</a>
{% endblock %}

Output (/form):

  • GET: Displays a Bootstrap-styled form.
  • POST (valid name): Redirects to a result page with the name.
  • POST (empty name): Shows a validation error.

Explanation:

  • app.config['SECRET_KEY']: Enables CSRF protection.
  • FlaskForm: Defines the form with a required StringField.
  • form.hidden_tag(): Includes the CSRF token.
  • form.validate_on_submit(): Checks if the form is valid and submitted.
  • form.name.errors: Displays validation errors.

03. Creating Forms with Flask-WTF

3.1 Common Field Types

WTForms provides various field types for different input needs:

Field Description Example
StringField Text input. StringField('Name')
IntegerField Integer input. IntegerField('Age')
FloatField Floating-point input. FloatField('Salary')
FileField File upload. FileField('File')
SubmitField Submit button. SubmitField('Submit')

3.2 Common Validators

Validators enforce input constraints:

Validator Description Example
DataRequired Ensures the field is not empty. DataRequired()
Length Restricts string length. Length(min=2, max=50)
NumberRange Restricts numeric range. NumberRange(min=0, max=100)
Email Validates email format. Email()

3.3 Rendering Forms

Forms are rendered in Jinja2 templates with Bootstrap classes for styling and error handling.

Example: Advanced Form with Multiple Fields

File: app.py

from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField, FloatField, SubmitField
from wtforms.validators import DataRequired, NumberRange, Length

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'

class PredictionForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired(), Length(min=2, max=50)])
    age = IntegerField('Age', validators=[DataRequired(), NumberRange(min=18, max=100)])
    salary = FloatField('Salary', validators=[DataRequired(), NumberRange(min=0)])
    submit = SubmitField('Predict')

@app.route('/predict', methods=['GET', 'POST'])
def predict():
    form = PredictionForm()
    if form.validate_on_submit():
        name = form.name.data
        age = form.age.data
        salary = form.salary.data
        prediction = 0.75  # Mock ML prediction
        return render_template('result.html', name=name, age=age, salary=salary, prediction=prediction)
    return render_template('predict.html', form=form)

if __name__ == '__main__':
    app.run(debug=True)

File: templates/predict.html

{% extends 'base.html' %}

{% block title %}Prediction Form{% endblock %}

{% block content %}
    <h1>ML Prediction Form</h1>
    <form method="post">
        {{ form.hidden_tag() }}
        <div class="form-group">
            {{ form.name.label }}
            {{ form.name(class="form-control") }}
            {% if form.name.errors %}
                {% for error in form.name.errors %}
                    <span class="error">{{ error | escape }}</span>
                {% endfor %}
            {% endif %}
        </div>
        <div class="form-group">
            {{ form.age.label }}
            {{ form.age(class="form-control") }}
            {% if form.age.errors %}
                {% for error in form.age.errors %}
                    <span class="error">{{ error | escape }}</span>
                {% endfor %}
            {% endif %}
        </div>
        <div class="form-group">
            {{ form.salary.label }}
            {{ form.salary(class="form-control") }}
            {% if form.salary.errors %}
                {% for error in form.salary.errors %}
                    <span class="error">{{ error | escape }}</span>
                {% endfor %}
            {% endif %}
        </div>
        {{ form.submit(class="btn btn-primary") }}
    </form>
{% endblock %}

File: templates/result.html

{% extends 'base.html' %}

{% block title %}Prediction Result{% endblock %}

{% block content %}
    <h1>Prediction Result</h1>
    <p>Name: {{ name | title | escape }}</p>
    <p>Age: {{ age | escape }}</p>
    <p>Salary: ${{ salary | float | round(2) }}</p>
    <p>Prediction: {{ prediction | float | round(2) }}</p>
    <a href="{{ url_for('predict') }}" class="btn btn-secondary">Try Again</a>
{% endblock %}

Output (/predict):

  • GET: Displays a form with name, age, and salary fields.
  • POST (valid): Shows the submitted data and a mock prediction.
  • POST (invalid): Displays validation errors (e.g., "This field is required").

Explanation:

  • Multiple fields: StringField, IntegerField, and FloatField with validators.
  • Validation: Ensures required fields, valid ranges, and string lengths.
  • Error handling: Displays errors per field.

04. Handling File Uploads with Flask-WTF

Flask-WTF supports file uploads using FileField, often combined with validators like FileAllowed from flask_wtf.file.

Example: CSV File Upload for Data Processing

File: app.py

from flask import Flask, render_template
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import SubmitField
from wtforms.validators import DataRequired
import pandas as pd
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['UPLOAD_FOLDER'] = 'uploads'

class UploadForm(FlaskForm):
    file = FileField('CSV File', validators=[DataRequired(), FileAllowed(['csv'], 'CSV files only')])
    submit = SubmitField('Upload')

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()
    if form.validate_on_submit():
        file = form.file.data
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
        file.save(file_path)
        df = pd.read_csv(file_path)
        return render_template('upload.html', form=form, data=df.to_dict(orient='records'))
    return render_template('upload.html', form=form)

if __name__ == '__main__':
    os.makedirs('uploads', exist_ok=True)
    app.run(debug=True)

File: templates/upload.html

{% extends 'base.html' %}

{% block title %}Upload CSV{% endblock %}

{% block content %}
    <h1>Upload CSV File</h1>
    <form method="post" enctype="multipart/form-data">
        {{ form.hidden_tag() }}
        <div class="form-group">
            {{ form.file.label }}
            {{ form.file(class="form-control") }}
            {% if form.file.errors %}
                {% for error in form.file.errors %}
                    <span class="error">{{ error | escape }}</span>
                {% endfor %}
            {% endif %}
        </div>
        {{ form.submit(class="btn btn-primary") }}
    </form>
    {% if data %}
        <h2>Data Preview</h2>
        <table class="table table-striped mt-3">
            <thead>
                <tr>
                    {% for key in data[0].keys() %}
                        <th>{{ key | title }}</th>
                    {% endfor %}
                </tr>
            </thead>
            <tbody>
                {% for row in data %}
                    <tr>
                        {% for value in row.values() %}
                            <td>{{ value | escape }}</td>
                        {% endfor %}
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    {% endif %}
{% endblock %}

Output (/upload):

  • GET: Displays a file upload form.
  • POST (valid CSV): Saves the file, displays a Pandas DataFrame table.
  • POST (invalid file): Shows an error (e.g., "CSV files only").

Explanation:

  • FileField: Handles file uploads.
  • FileAllowed(['csv']): Restricts uploads to CSV files.
  • Pandas: Reads and displays the uploaded CSV.

05. Forms in Data-Driven Applications

Flask-WTF is ideal for data-driven applications, enabling forms to filter data, collect ML inputs, or process uploaded datasets.

Example: ML Dashboard with Form

File: app.py

from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import FloatField, SubmitField
from wtforms.validators import DataRequired, NumberRange
import pandas as pd

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'

@app.template_filter('probability')
def probability_filter(value):
    try:
        return f"{float(value) * 100:.1f}%"
    except (ValueError, TypeError):
        return value

class PredictionForm(FlaskForm):
    feature1 = FloatField('Feature 1', validators=[DataRequired(), NumberRange(min=0, max=1)])
    feature2 = FloatField('Feature 2', validators=[DataRequired(), NumberRange(min=0, max=1)])
    submit = SubmitField('Predict')

@app.route('/dashboard', methods=['GET', 'POST'])
def dashboard():
    form = PredictionForm()
    df = pd.DataFrame({
        'Name': ['Alice', 'Bob', 'Charlie'],
        'Age': [25, 30, 35],
        'Salary': [50000, 60000, 55000]
    })
    prediction = None
    if form.validate_on_submit():
        feature1 = form.feature1.data
        feature2 = form.feature2.data
        prediction = (feature1 + feature2) / 2  # Mock ML prediction
    return render_template('dashboard.html', form=form, data=df.to_dict(orient='records'), prediction=prediction)

if __name__ == '__main__':
    app.run(debug=True)

File: templates/dashboard.html

{% extends 'base.html' %}

{% block title %}ML Dashboard{% endblock %}

{% block content %}
    <h1>ML Prediction Dashboard</h1>
    <table class="table table-striped">
        <thead>
            <tr>
                <th>Name</th>
                <th>Age</th>
                <th>Salary</th>
            </tr>
        </thead>
        <tbody>
            {% for row in data %}
                <tr>
                    <td>{{ row.Name | title }}</td>
                    <td>{{ row.Age }}</td>
                    <td>${{ row.Salary | float | round(2) }}</td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
    <h2>Make a Prediction</h2>
    <form method="post">
        {{ form.hidden_tag() }}
        <div class="form-group">
            {{ form.feature1.label }}
            {{ form.feature1(class="form-control") }}
            {% if form.feature1.errors %}
                {% for error in form.feature1.errors %}
                    <span class="error">{{ error | escape }}</span>
                {% endfor %}
            {% endif %}
        </div>
        <div class="form-group">
            {{ form.feature2.label }}
            {{ form.feature2(class="form-control") }}
            {% if form.feature2.errors %}
                {% for error in form.feature2.errors %}
                    <span class="error">{{ error | escape }}</span>
                {% endfor %}
            {% endif %}
        </div>
        {{ form.submit(class="btn btn-primary") }}
    </form>
    {% if prediction is not none %}
        <p>Prediction Probability: {{ prediction | probability }}</p>
    {% endif %}
{% endblock %}

Output (/dashboard):

  • GET: Shows a Pandas DataFrame table and a prediction form.
  • POST (feature1=0.8, feature2=0.6): Displays the table and Prediction Probability: 70.0%.

Explanation:

  • PredictionForm: Collects ML input features with validation.
  • Pandas: Displays a static dataset alongside the form.
  • | probability: Formats the prediction output.

06. Best Practices for Flask-WTF Forms

6.1 Recommended Practices

  • Use form.hidden_tag(): Always include the CSRF token in forms.
  • Validate Inputs: Use appropriate validators (e.g., DataRequired, NumberRange) to enforce constraints.
  • Handle Errors: Display field-specific errors in templates for user feedback.
  • Secure Outputs: Use | escape for user inputs in templates.
  • Style Forms: Apply Bootstrap or custom CSS for a polished look.
  • Reuse Forms: Define form classes for reuse across routes or projects.

6.2 Security Considerations

  • CSRF Protection: Ensure SECRET_KEY is set and form.hidden_tag() is included.
  • File Uploads: Use FileAllowed and validate file sizes to prevent abuse.
  • Sanitize Inputs: Use libraries like bleach for additional input cleaning if needed.

Example: Insecure Form Handling

File: templates/insecure.html

{% extends 'base.html' %}

{% block content %}
    <h1>Insecure Form</h1>
    <form method="post">
        <!-- Missing CSRF token -->
        <div class="form-group">
            {{ form.name.label }}
            {{ form.name(class="form-control") }}
        </div>
        {{ form.submit(class="btn btn-primary") }}
    </form>
{% endblock %}

Issue: Missing {{ form.hidden_tag() }} disables CSRF protection, risking attacks.

Correct:

{% extends 'base.html' %}

{% block content %}
    <h1>Secure Form</h1>
    <form method="post">
        {{ form.hidden_tag() }}
        <div class="form-group">
            {{ form.name.label }}
            {{ form.name(class="form-control") }}
        </div>
        {{ form.submit(class="btn btn-primary") }}
    </form>
{% endblock %}

Output: Form includes CSRF token, ensuring security.

Explanation:

  • Insecure: Omitting form.hidden_tag() exposes the form to CSRF attacks.
  • Correct: Including the CSRF token ensures protection.

6.3 Practices to Avoid

  • Avoid Skipping Validation: Always use validators to enforce input rules.
  • Avoid Hardcoding CSRF: Rely on form.hidden_tag() instead of manual tokens.
  • Avoid Unstyled Forms: Apply CSS for better user experience.

07. Conclusion

Flask-WTF simplifies the creation of secure, validated, and user-friendly forms in Flask, making it an essential tool for data-driven applications. Key takeaways:

  • Define forms with FlaskForm, using fields and validators for robust input handling.
  • Render forms in Jinja2 templates with Bootstrap for styling and error display.
  • Support data-driven tasks like ML input collection or CSV uploads with Pandas integration.
  • Follow best practices like CSRF protection, input validation, and secure output rendering.

By mastering Flask-WTF, you can build secure, efficient, and polished forms that enhance user interaction in Flask applications, particularly for dynamic, data-driven contexts!

Comments