Content Index
- Why are forms essential in Flask?
- What is WTForms and how does it work with Flask 3
- WTForms: Step-by-step form creation
- Main fields
- Validations
- Custom validations
- CSRF Protection
- Creating a form with WTForms for tasks
- Best practices and common errors
- Complete example: task form in Flask 3
- Form
- Conclusion
- ❓Frequently Asked Questions
Forms are a crucial part of web applications today; they are the mechanism par excellence used to obtain user data. Although web forms are purely HTML and can be used directly in Flask, it brings the drawback that these forms must have validations applied, both on the client side and, more importantly, on the server side, so that when the data is received before processing and saving, it is completely valid.
We stayed where we were and we know how to use the session in Flask.
In my first projects and when I was starting with frameworks like Flask, I remember how frustrating it was to maintain duplicated validations: a required in HTML and another block of Python code. WTForms came to solve just that, and in this guide, I'll tell you how to use it correctly in Flask 3, with real examples and best practices.
Another problem is that an equivalent must be created between the form fields and the models in the database; that is, these form fields usually have a direct or at least quite direct relationship with the tables in the database. If we are going to create a module to manage publications whose form fields would be title, description, and content, these fields must be present in the database table to be able to register them. Therefore, we have double work to define these fields in the database (in the case of Flask, through models) and the form fields.
Why are forms essential in Flask?
A form doesn't just capture data: it must also validate, protect, and process it before saving it to the database.
In Flask, you could do it with pure HTML and request.form functions, but you'll soon notice that this means repeating logic in two different places.
With WTForms, we save ourselves the "double work" of defining the same fields and validations in HTML and in the database models. Furthermore, Python's syntax makes everything much cleaner and easier to maintain.
What is WTForms and how does it work with Flask 3
WTForms is an independent library for handling server-side forms and validations. Flask, being a microframework, relies on packages like this to extend its functionalities and perform certain actions.
Flask-WTF is its official integration with Flask, adding support for Flask's Jinja2 templates, CSRF handling, and helpers that simplify the entire workflow.
These situations occur with any server-side technology, which ultimately consists of defining fields and validations with the same purpose but with different logic on the client and the server; that is, if we want a field for the name, in which it is required, in HTML it would be like:
<input type="text" name="task" required>And on the server, using Flask, it would be something like:
task = request.args.form('task')
if task is not None:
#TO DO And if at any time, we are asked to change the validation or add more, we have to make changes on both sides.
Luckily, in the case of Flask, there is a package that greatly facilitates the process and the specified logic. The WTForms package provides several fields to which we can apply server-side validations, and through Jinja, we can easily represent these fields so that they are converted to HTML form fields; these fields are actually properties that correspond to a class that inherits from FlaskForm, to whose fields/properties we can define validations; a typical validation class looks like:
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import InputRequired
class Task(FlaskForm):
name = StringField('Name', validators=[InputRequired()])Validations are applied mainly on the server side, which is the most important side, although, depending on the validation, certain rules are specified:
<input id="name" name="name" required type="text" value="">WTForms: Step-by-step form creation
Let's start by installing the package with:
$ pip install Flask-WTF Two packages are installed: the plugin:
WTForms==*And the connector for Flask:
Flask-WTF==*For simplicity, we will refer to WTForm as simply WTF or Flask WTF.
Main fields
We have different types of fields that we can define to create a form using Flask WTF, which are the following:
- fields.StringField: Represents a text type field.
- fields.FileField: Represents a file type field.
- fields.DateField and fields.DateTimeField: Represents a date or datetime type field in Python; it also accepts an optional parameter called format that receives the String format for the date, which by default is (format='%Y-%m-%d').
- fields.IntegerField: Represents an integer type field.
- fields.FloatField: Represents a floating-point type field.
- fields.PasswordField: Represents a password type field.
- fields.RadioField: Represents a radio type field in HTML, as expected, it is specified using a parameter to select a list of options; this list can be a list or tuple in Python.
- fields.SelectField: Represents a selection type field in which a parameter called choices is specified, where a list of options is provided.
You can get more information about the available fields at:
https://wtforms.readthedocs.io/en/master/fields/
Validations
In addition to defining the fields, it is possible to add validations to the form fields; for this, we have predefined classes in:
from wtforms.validators import *Among the main ones:
- InputRequired: Indicates that the field is required.
- NumberRange: Sets a minimum and maximum number.
- DataRequired: Indicates that the field is required.
- Length: Sets a minimum and maximum number for the size of a string.
- Regexp: Evaluates a regular expression.
You can get more information about the available validations at:
https://wtforms.readthedocs.io/en/master/validators/
And then, from the controller, it is checked whether the form is valid before performing any other operation on it, such as saving to the database:
@app.route('/submit', methods=['GET', 'POST'])
def submit():
form = MyForm()
if form.validate_on_submit():
# TO DOAnd from the template, we can show the errors that occurred for a particular field, for example, the one called name:
{% if form.name.errors %}
<ul class="errors">
{% for error in form.name.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}Or all fields:
{% if form.errors %}
<ul class="errors">
{% for error in form.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}Custom validations
Many times it is necessary to add more complex rules than simply indicating whether a field is required, or by type, or by a regular expression; instead, we need to perform validations using functions and conditionals, among others. In these cases, we can use custom validations, either by fields; for example, for the name field:
my_app\tasks\forms.py
from wtforms.validators import ValidationError
***
class Task(FlaskForm):
***
def validate_name(form, field):
if len(field.data) > 2:
raise ValidationError('Name must be less than 2 characters')CSRF Protection
When using a form, we will see an error like the following:
RuntimeError: A secret key is required to use CSRF.WTForms forms use CSRF protection, and we must specify a class for generating this token; for this, we can use the configuration file:
my_app\config.py
class Config(object):
SQLALCHEMY_DATABASE_URI="mysql+pymysql://root:@localhost:3306/test_flask"
SECRET_KEY="SECRETKEY"
***In this example, the secret key is SECRETKEY, but you can customize it to your liking.
The CSRF (Cross Site Request Forgery) token is an automatic security measure in Flask-WTF.
To activate it, you just need to define SECRET_KEY in your configuration, as we showed before.
In my first attempts, I forgot to configure it and received the famous "A secret key is required to use CSRF" error. Since then, I consider it one of those details that must be ready from the start of the project.
Creating a form with WTForms for tasks
Having a clear understanding of how forms work with Flask WTF, let's implement a form for tasks:
my_app\tasks\forms.py
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import InputRequired
class Task(FlaskForm):
name = StringField('Name', validators=[InputRequired()])As you can see in the code above, since the field for tasks consists of specifying the name, a text type field is used, defined by the StringField class and made required using InputRequired.
From the controller, we create an instance of the form:
my_app\tasks\controllers.py
from my_app.tasks import forms
***
@taskRoute.route('/create', methods=('GET', 'POST'))
def create():
# form = forms.Task(csrf_enabled=False)
form = forms.Task()
if form.validate_on_submit():
print("ready")
# return redirect('/success')
# return render_template('submit.html', form=form)
# task_list.append(request.form.get('task')) #request.args.get('task')
return render_template('dashboard/task/create.html', form=form)Best practices and common errors
- ✅ Always validate on the server. The client can be easily manipulated.
- Avoid duplicating validations between HTML and Python.
- Separate responsibilities: the form validates, the controller processes, and the model saves.
- Use custom error messages to improve the user experience.
In my experience, these small adjustments make a big difference in real projects, especially as forms grow in complexity.
Complete example: task form in Flask 3
Form
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import InputRequired
class Task(FlaskForm):
name = StringField('Name', validators=[InputRequired()])
Controlador
@app.route('/create', methods=['GET', 'POST'])
def create():
form = Task()
if form.validate_on_submit():
print("Tarea creada correctamente")
return render_template('task_form.html', form=form)
Template (task_form.html)
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name() }}
{% for error in form.name.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
<button type="submit">Enviar</button>
</form>This example reflects the basis of any functional form in Flask 3, with secure validation and clean syntax.
Conclusion
Using WTForms in Flask 3 not only simplifies form validation but also improves code security and maintainability.
In my case, this approach eliminated unnecessary duplication and allowed me to concentrate on the business logic, not the markup.
If you are developing with Flask 3, integrating WTForms is practically mandatory: it offers you control, clarity, and a solid foundation for any professional project.
❓Frequently Asked Questions
- Are WTForms and Flask-WTF the same?
- WTForms is the base library; Flask-WTF is its integration with Flask.
- What changes in Flask 3?
- Better compatibility with Python 3.12, modular structure, and updated extension support.
- How to handle custom validations?
- Use validate inside FlaskForm.
Next step, generate test data with the Flask Seeder.