Form Validation with Flask-WTF & WTForms
WTForms is a Python library that declares web forms as classes with typed fields and validators, automatically checking submitted data and collecting error messages for you.
Learn Form Validation with Flask-WTF & WTForms in our free Flask course — a beginner-friendly interactive lesson with worked examples, a practice exercise…
Part of the free Flask course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
In this lesson you'll declare a form, attach validators like DataRequired and Length , run validation against submitted data, read the errors, and see how Flask-WTF adds CSRF protection.
In WTForms, a form is a class . Each attribute is a field — StringField , PasswordField , IntegerField , and so on — and you pass each field a list of validators . Validators are small rules: DataRequired() means the field cannot be empty, Length(min=3) enforces a minimum length.
To validate real input you feed the form a MultiDict (the dictionary-like type Flask's request.form actually is). The runnable example builds a registration form and validates a good submission, then reads back the cleaned data.
Output: valid? True and a data dict with the username, email, and password — the cleaned values you would persist to the database.
When a submission breaks the rules, validate() returns False and fills form.errors — a dictionary keyed by field name, where each value is a list of human-readable messages. This is exactly what you loop over in a template to show users what went wrong.
The example sends a too-short username, a malformed email, and a short password, then prints the collected errors.
Output: valid? False , then one line per field — username reports the length rule, email reports the pattern, and password reports the minimum length.
In a real app you do not pass request.form yourself. Flask-WTF gives you FlaskForm , which reads the request automatically, and validate_on_submit() , which returns True only on a POST whose data passes validation. It also injects a hidden, signed CSRF token so forged cross-site submissions are rejected.
Here is the production view and template. It is read-only because it needs the flask_wtf package, but the form class is the same one you ran above.
Complete the login form. Replace each ___ so the username is required and the password must be at least 6 characters.
Your template did not render form.hidden_tag() , or app.secret_key is unset. Add both — the token cannot be signed without a secret key.
❌ Install 'email_validator' for email validation support
The Email() validator needs an extra package: pip install email_validator . Until then, use a Regexp rule as a stand-in.
With Flask-WTF use validate_on_submit() , not validate() . It returns False on a GET so you can show the empty form instead of a wall of errors.
Build and validate a contact form with three rules.
Lesson complete — your forms validate themselves!
You can declare a form as a class, attach validators, run validate() , read form.errors , and you understand how Flask-WTF layers on FlaskForm and CSRF protection.
🚀 Up next: SQLAlchemy Relationships — model how your data connects with one-to-many and many-to-many links.
Practice quiz
In WTForms, how is a form defined?
- As a dictionary
- As a function
- As a class with field attributes
- As a JSON schema
Answer: As a class with field attributes. A WTForms form is a class where each attribute is a field like StringField or PasswordField.
Which validator means a field cannot be empty?
- DataRequired()
- NotNull()
- Mandatory()
- Filled()
Answer: DataRequired(). DataRequired() enforces that the field must contain data.
Which class does Flask-WTF provide on top of WTForms?
- WebForm
- FormBase
- FlaskWTForm
- FlaskForm
Answer: FlaskForm. Flask-WTF adds FlaskForm, which auto-reads request.form and adds CSRF protection.
What does validate_on_submit() return?
- Always True
- True only on a POST whose data passes validation
- The cleaned data dict
- The list of errors
Answer: True only on a POST whose data passes validation. validate_on_submit() returns True only for a valid POST, and False on GET or invalid input.
Where does WTForms record validation messages?
- form.errors
- form.messages
- form.problems
- form.invalid
Answer: form.errors. form.errors is a dict keyed by field name, each value a list of messages.
Which validator enforces a minimum length?
- Size(min=3)
- Length(min=3)
- MinLength(3)
- Chars(min=3)
Answer: Length(min=3). Length(min=3, max=20) enforces minimum and maximum character counts.
What does CSRF protection defend against?
- SQL injection
- Slow queries
- Forged cross-site request submissions
- Cross-site scripting
Answer: Forged cross-site request submissions. CSRF tricks a logged-in user's browser into sending an unintended request; the token blocks forged ones.
Which template call renders the hidden CSRF token?
- form.csrf()
- form.token()
- form.secure()
- form.hidden_tag()
Answer: form.hidden_tag(). form.hidden_tag() outputs the CSRF token; omit it and Flask-WTF rejects the POST with a 400.
Where are cleaned values available after validation?
- form.data
- form.cleaned
- form.values
- form.results
Answer: form.data. After validation passes, the cleaned values are accessible through form.data.
What does the built-in Email() validator require?
- A secret key
- Nothing extra
- The email_validator package
- A database connection
Answer: The email_validator package. Email() needs the email_validator package installed; otherwise use a Regexp rule as a stand-in.