Flask - WTF
Flask - Forms with Flask-WTF
Section titled “Flask - Forms with Flask-WTF”Web applications rely heavily on forms to collect user input. While HTML provides the <form> tag and various input elements (<input>, <textarea>, <select>, etc.), handling form data processing, validation, and rendering securely and efficiently on the server-side can be complex.
Manually handling forms involves several challenges:
- Repetitive Definition: Form structures often need to be defined in HTML for presentation and again in server-side code for processing and validation.
- Dynamic Rendering: Creating forms dynamically based on data or context can be cumbersome with plain HTML.
- Validation: Implementing robust input validation requires significant custom code.
- Security: Protecting against common web vulnerabilities like Cross-Site Request Forgery (CSRF) requires careful implementation.
Flask-WTF is a Flask extension that integrates the powerful WTForms library, simplifying form handling significantly. It allows you to define your form structure and validation rules in Python, render the form in your HTML templates, and process the submitted data securely.
Key benefits of using Flask-WTF include:
- Defining forms as Python classes.
- Rendering HTML forms from these Python classes.
- Validating submitted form data based on defined rules.
- Built-in CSRF protection.
First, you need to install the Flask-WTF extension:
pip install Flask-WTFNote: Flask-WTF depends on WTForms, which will be installed automatically.
To create a form, you define a class that inherits from FlaskForm provided by Flask-WTF. Form fields are defined as class variables, using field types imported from the wtforms library.
Here are some commonly used WTForms field types:
| Sr.No | Field Type | Description & HTML Representation |
|---|---|---|
| 1 | StringField | Renders as <input type='text'>. For general text input. |
| 2 | TextAreaField | Renders as <textarea>. For multi-line text input. |
| 3 | PasswordField | Renders as <input type='password'>. Hides input. |
| 4 | BooleanField | Renders as <input type='checkbox'>. For true/false values. |
| 5 | IntegerField | Renders as <input type='text'> (or number). Validates integer input. |
| 6 | DecimalField | Renders as <input type='text'> (or number). Validates decimal input. |
| 7 | DateField | Renders as <input type='text'> (or date). Validates date input. |
| 8 | RadioField | Renders as a list of <input type='radio'>. For selecting one option. |
| 9 | SelectField | Renders as <select>. For selecting from a dropdown list. |
| 10 | SubmitField | Renders as <input type='submit'>. For submitting the form. |
For example, a simple contact form with a name field can be defined like this:
from flask_wtf import FlaskFormfrom wtforms import StringField, SubmitFieldfrom wtforms.validators import DataRequired
class ContactForm(FlaskForm): name = StringField('Name Of Student', validators=[DataRequired()]) submit = SubmitField('Submit')Notice the validators=[DataRequired()] argument. This ensures the field is not submitted empty. WTForms provides various built-in validators.
Flask-WTF automatically adds a hidden field containing a CSRF token to your form. You need to render this field in your template using form.hidden_tag() or form.csrf_token. This token is essential for protecting your application against Cross-Site Request Forgery attacks, where malicious websites might try to trick users into submitting unintended data to your application.
When rendered in an HTML template (explained below), the name field would generate HTML similar to this (along with the hidden CSRF token field):
<label for="name">Name Of Student</label><input id="name" name="name" required type="text" value="">To use this form in your Flask application, you instantiate it in your view function and pass it to the template.
from flask import Flask, render_template, request, flashfrom forms import ContactForm # Assuming forms.py contains your form class
app = Flask(__name__)# A secret key is required for CSRF protection and session management# Use a strong, random, and secret value in production!app.config['SECRET_KEY'] = 'a-very-secret-key'
@app.route('/contact', methods=['GET', 'POST'])def contact(): form = ContactForm() if form.validate_on_submit(): # This block executes on POST request if validation passes name = form.name.data # Process the data (e.g., save to database, send email) flash(f'Message received from {name}!', 'success') # Redirect or render a success page return render_template('success.html') # On GET request or if validation fails on POST, render the form template return render_template('contact.html', form=form)
if __name__ == '__main__': app.run(debug=True)The form.validate_on_submit() method is a convenient helper. It returns True if the request method is POST and all field validations pass; otherwise, it returns False.
WTForms includes a validators module with various common validation rules. You pass a list of validators to the field definition.
| Sr.No | Common Validators | Description |
|---|---|---|
| 1 | DataRequired | Checks if the field contains data. Use instead of InputRequired for most cases. |
| 2 | InputRequired | Checks if input was provided for the field. Use for optional fields where empty value is different from missing value. |
| 3 | Validates that the input looks like an email address. | |
| 4 | EqualTo | Compares the input with the value of another field (e.g., password confirmation). |
| 5 | IPAddress | Validates an IPv4 or IPv6 address. |
| 6 | Length | Validates that the input string’s length is within a specified range. |
| 7 | NumberRange | Validates that a number is within a specified range. |
| 8 | URL | Validates a URL. |
Example with multiple validators:
from wtforms.validators import DataRequired, Email, Length
email = StringField('Email', validators=[ DataRequired(), Email(message='Invalid email address.'), Length(max=120)])If validation fails for a field when form.validate_on_submit() or form.validate() is called, an errors attribute (a list of error messages) is attached to the field object. You can display these errors in your template.
{# Example showing how to display errors for the 'name' field #}{{ form.name.label }}<br>{{ form.name(size=30) }}{% if form.name.errors %} <ul class="errors"> {% for error in form.name.errors %} <li>{{ error }}</li> {% endfor %} </ul>{% endif %}Let’s build a more complete example. Define the following form in forms.py:
from flask_wtf import FlaskFormfrom wtforms import StringField, TextAreaField, IntegerField, SelectField, RadioField, SubmitFieldfrom wtforms.validators import DataRequired, Email, NumberRange
class ContactForm(FlaskForm): name = StringField('Name Of Student', validators=[ DataRequired(message="Please enter your name.") ]) gender = RadioField('Gender', choices=[('M', 'Male'), ('F', 'Female')], validators=[ DataRequired(message="Please select your gender.") ]) address = TextAreaField('Address', validators=[ DataRequired(message="Please enter your address.") ]) email = StringField('Email', validators=[ DataRequired(message="Please enter your email address."), Email(message="Please enter a valid email address.") ]) age = IntegerField('Age', validators=[ DataRequired(message="Please enter your age."), NumberRange(min=1, max=120, message="Age must be between 1 and 120.") ]) language = SelectField('Favorite Language', choices=[ ('', '-- Select Language --'), # Add a placeholder ('py', 'Python'), ('js', 'JavaScript'), ('cpp', 'C++') ], validators=[ DataRequired(message="Please select a language.") ]) submit = SubmitField('Send')Here is the updated Flask application script (app.py):
from flask import Flask, render_template, request, flash, redirect, url_forfrom forms import ContactForm
app = Flask(__name__)# IMPORTANT: Use a real secret key, ideally from environment variablesapp.config['SECRET_KEY'] = 'keep-this-a-secret-please'
@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 # ... process other fields ... print(f"Received data: Name={name}, Email={email}") # Example processing flash('Form submitted successfully!', 'success') return render_template('success.html') # Show a success page # Else (GET request or validation failed), render the form again. # Errors will be available in the form object for the template to display. if request.method == 'POST' and not form.validate(): flash('Please correct the errors in the form.', 'danger')
return render_template('contact.html', form=form)
# Add a simple success route/template@app.route('/success')def success(): return render_template('success.html')
if __name__ == '__main__': app.run(debug=True)And the template (templates/contact.html) to render the form and display errors:
<!doctype html><html><head> <title>Contact Form</title> <style> .error { color: red; font-size: 0.8em; } label { font-weight: bold; } div { margin-bottom: 10px; } </style></head><body> <h2>Contact Form</h2>
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} <ul> {% for category, message in messages %} <li class="{{ category }}">{{ message }}</li> {% endfor %} </ul> {% endif %} {% endwith %}
<form method="post" novalidate> {{ form.hidden_tag() }} {# Renders CSRF token and any other hidden fields #}
<div> {{ form.name.label }}<br> {{ form.name(size=30) }} {% if form.name.errors %} {% for error in form.name.errors %}<span class="error">[{{ error }}]</span>{% endfor %} {% endif %} </div>
<div> {{ form.gender.label }}<br> {% for subfield in form.gender %} {{ subfield }} {{ subfield.label }} {% endfor %} {% if form.gender.errors %} {% for error in form.gender.errors %}<span class="error">[{{ error }}]</span>{% endfor %} {% endif %} </div>
<div> {{ form.address.label }}<br> {{ form.address(rows=4, cols=30) }} {% if form.address.errors %} {% for error in form.address.errors %}<span class="error">[{{ error }}]</span>{% endfor %} {% endif %} </div>
<div> {{ form.email.label }}<br> {{ form.email(size=30) }} {% if form.email.errors %} {% for error in form.email.errors %}<span class="error">[{{ error }}]</span>{% endfor %} {% endif %} </div>
<div> {{ form.age.label }}<br> {{ form.age(size=5) }} {% if form.age.errors %} {% for error in form.age.errors %}<span class="error">[{{ error }}]</span>{% endfor %} {% endif %} </div>
<div> {{ form.language.label }}<br> {{ form.language() }} {% if form.language.errors %} {% for error in form.language.errors %}<span class="error">[{{ error }}]</span>{% endfor %} {% endif %} </div>
<div> {{ form.submit() }} </div> </form>
</body></html>Create a simple templates/success.html:
<!doctype html><html><head><title>Success</title></head><body> <h2>Form Submitted Successfully!</h2> <p><a href="{{ url_for('contact') }}">Go back to the form</a></p></body></html>Run app.py and navigate to http://localhost:5000/contact. You will see the rendered form. If you submit without filling required fields or with invalid data, the form will re-render, displaying error messages next to the relevant fields, thanks to the validation rules and the error rendering logic in the template.
If you fill the form correctly and submit, you will be redirected to the success page, and a message will be printed to the console where you ran the Flask app.
For more advanced form handling, explore the documentation for WTForms and Flask-WTF. Topics include custom validators, file uploads, advanced field types, and more.