Skip to content

Rendering Forms in Templates

You can render forms manually (recommended for control) or use quick rendering.

Template example:

html
<form method="post">
  {{ form.hidden_tag() }}
 
  <div>
    {{ form.name.label }}
    {{ form.name() }}
    {% for error in form.name.errors %}
      <p class="error">{{ error }}</p>
    {% endfor %}
  </div>
 
  <div>
    {{ form.email.label }}
    {{ form.email() }}
    {% for error in form.email.errors %}
      <p class="error">{{ error }}</p>
    {% endfor %}
  </div>
 
  {{ form.submit() }}
</form>
html
{{ form.name(class_="input", placeholder="Your name") }}

Note: use class_ (underscore) because class is a Python keyword.

As forms grow, you can move repeated field rendering into a partial:

  • templates/partials/_field.html

Then include it for each field.

You do not write the <input> tags. Calling the field produces the HTML, with the id, name, type and any validator-derived attributes already correct:

template
<form method="post">
  {{ form.hidden_tag() }}
  {{ form.name.label }} {{ form.name(class_="input") }}
  <button type="submit">Sign up</button>
</form>

Measured output for each field:

what the fields produce
<input id="name" minlength="2" name="name" required type="text" value="">
<input id="pw" name="pw" type="password" value="">
<textarea id="bio" name="bio"></textarea>
<input id="age" name="age" type="number" value="">
<input id="agree" name="agree" type="checkbox" value="y">
<select id="role" name="role"><option value="u">User</option>...</select>
<label for="name">Your name</label>

Because id and the label’s for match, clicking the label focuses the input — an accessibility win you get without thinking about it.

diagram Diagram mermaid
measured
<input id="csrf_token" name="csrf_token" type="hidden" value="ImUzNjI0ZTdmNWY0Mm...">

It renders every hidden field on the form, the CSRF token included. Leaving it out is the most common reason a form that “looks fine” never submits successfully.

field_with_errors.html
<div class="field">
  {{ form.name.label }}
  {{ form.name(class_="input") }}
  {% for error in form.name.errors %}
    <span class="error">{{ error }}</span>
  {% endfor %}
</div>

Measured with name='a' submitted:

rendered
<span class='err'>Field must be at least 2 characters long.</span>

field.errors is a plain list, empty before validation and on success — so the loop simply produces nothing when there is nothing to say.

every field at once
{% for field in form if field.widget.input_type != 'hidden' %}
  <div class="field">
    {{ field.label }} {{ field() }}
    {% for e in field.errors %}<span class="error">{{ e }}</span>{% endfor %}
  </div>
{% endfor %}

A FlaskForm is iterable, so a macro like this renders any form you pass it. Measured, the loop yields the labels in declaration order: Your name, Password, Bio, Age, Agree, Role. Filtering out hidden inputs keeps the CSRF token from getting a label and a wrapper div of its own — render it separately with hidden_tag().

sketch Building the form markup piece by piece p5.js
Each call adds one part of the form. Leaving out hidden_tag removes the CSRF token, and every submission then fails validation.
pch.quizTag pch.quizDefaultTitle
  1. What does form.hidden_tag() render, and what breaks without it?

    pch.quizShowAnswer

    B — the hidden CSRF token input; without it every POST fails with 'The CSRF token is missing.' — It renders every hidden field, the signed csrf_token included. Omitting it is the most common reason a form that looks correct never validates.

  2. Why does clicking a field's label focus its input without any extra work?

    pch.quizShowAnswer

    B — wtforms renders matching id and for attributes on the input and label — Measured: <input id="name" ...> and <label for="name">. The matching pair is what makes the label clickable, which is an accessibility requirement you get by default.

  3. After a successful POST, why redirect rather than render the result directly?

    pch.quizShowAnswer

    B — so the browser reload button does not resubmit the form and duplicate the record — This is the Post/Redirect/Get pattern. Without the redirect, refreshing the result page repeats the POST.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading