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.