Flask Cheatsheet

Forms

Use this Flask reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

HTML Forms Without Extensions

from flask import request, redirect, url_for, render_template, flash

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        email = request.form.get("email", "").strip()
        password = request.form.get("password", "")
        if not email or not password:
            flash("All fields are required.", "error")
            return render_template("login.html"), 422
        # validate...
        return redirect(url_for("dashboard"))
    return render_template("login.html")
<!-- login.html -->
<form method="POST" action="{{ url_for('login') }}">
  <input type="email" name="email" required>
  <input type="password" name="password" required>
  <button type="submit">Log In</button>
</form>

WTForms / Flask-WTF

pip install flask-wtf
# config
app.config["SECRET_KEY"] = "change-me"   # required for CSRF
app.config["WTF_CSRF_ENABLED"] = True    # default True

Defining a Form

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField, SelectField, IntegerField, FileField
from wtforms.validators import DataRequired, Email, Length, EqualTo, Optional, NumberRange, Regexp, URL, ValidationError

class LoginForm(FlaskForm):
    email    = StringField("Email",    validators=[DataRequired(), Email()])
    password = PasswordField("Password", validators=[DataRequired(), Length(min=8)])
    remember = BooleanField("Remember me")
    submit   = SubmitField("Log In")

class RegisterForm(FlaskForm):
    username = StringField("Username",
                           validators=[DataRequired(), Length(3, 20),
                                       Regexp(r"^[\w]+$", message="Letters/digits/underscore only")])
    email    = StringField("Email", validators=[DataRequired(), Email()])
    password = PasswordField("Password", validators=[DataRequired(), Length(8)])
    confirm  = PasswordField("Confirm", validators=[DataRequired(), EqualTo("password")])
    submit   = SubmitField("Register")

    def validate_email(self, field):
        """Custom per-field validator (called automatically)."""
        if db.session.scalar(select(User).where(User.email == field.data)):
            raise ValidationError("Email already registered.")

Using a Form in a View

@app.route("/login", methods=["GET", "POST"])
def login():
    form = LoginForm()
    if form.validate_on_submit():      # POST + passes all validators
        user = authenticate(form.email.data, form.password.data)
        if user:
            return redirect(url_for("dashboard"))
        form.email.errors.append("Invalid credentials.")
    return render_template("login.html", form=form)

validate_on_submit() = request.method in ("POST", "PUT", "PATCH", "DELETE") and form.validate().

Rendering in Jinja

<form method="POST">
  {{ form.hidden_tag() }}    {# CSRF token — required #}

  {{ form.email.label }}
  {{ form.email(class="form-input", placeholder="you@example.com") }}
  {% for error in form.email.errors %}
    <span class="error">{{ error }}</span>
  {% endfor %}

  {{ form.password.label }}
  {{ form.password() }}

  {{ form.remember() }} {{ form.remember.label }}

  {{ form.submit(class="btn") }}
</form>

Built-in Validators Reference

ValidatorUsageDescription
DataRequired()DefaultField must be non-empty after strip
InputRequired()Field must be present (even whitespace)
Optional()Skips other validators if field is empty
Email()Valid email format
URL(require_tld=True)Valid URL
Length(min, max)String length bounds
NumberRange(min, max)Numeric range
EqualTo("field")Must equal another field (password confirm)
Regexp(pattern, flags, message)Regex match
AnyOf([v1, v2])Must be one of the values
NoneOf([v1, v2])Must NOT be one of the values
IPAddress(ipv4=True, ipv6=False)Valid IP
MacAddress()Valid MAC address
UUID()Valid UUID string
ValidationError("msg")raise in validate_<field>Custom error

Field Types Reference

FieldImportHTML Input
StringFieldwtforms<input type="text">
TextAreaFieldwtforms<textarea>
PasswordFieldwtforms<input type="password">
HiddenFieldwtforms<input type="hidden">
EmailFieldwtforms<input type="email">
TelFieldwtforms<input type="tel">
URLFieldwtforms<input type="url">
DateFieldwtforms<input type="date">
DateTimeFieldwtforms<input type="datetime-local">
DateTimeLocalFieldwtforms<input type="datetime-local">
IntegerFieldwtforms<input type="number">
FloatFieldwtforms<input type="number" step="any">
DecimalFieldwtforms<input type="number">
BooleanFieldwtforms<input type="checkbox">
RadioFieldwtforms<input type="radio">
SelectFieldwtforms<select>
SelectMultipleFieldwtforms<select multiple>
FileFieldwtforms<input type="file">
MultipleFileFieldwtforms<input type="file" multiple>
SubmitFieldwtforms<input type="submit">
FormFieldwtformsEmbed a sub-form
FieldListwtformsRepeated field (dynamic lists)

SelectField

class PostForm(FlaskForm):
    category = SelectField(
        "Category",
        choices=[("tech", "Technology"), ("art", "Arts"), ("sci", "Science")],
        validators=[DataRequired()],
    )
    # Dynamic choices (set in view):
    author = SelectField("Author", coerce=int)

# In view, populate dynamic choices:
form = PostForm()
form.author.choices = [(u.id, u.name) for u in db.session.scalars(select(User))]

File Uploads with Flask-WTF

from flask_wtf.file import FileField, FileRequired, FileAllowed

class UploadForm(FlaskForm):
    avatar = FileField("Avatar", validators=[
        FileRequired(),
        FileAllowed(["jpg", "jpeg", "png"], "Images only."),
    ])
    submit = SubmitField("Upload")
@app.route("/upload", methods=["GET", "POST"])
def upload():
    form = UploadForm()
    if form.validate_on_submit():
        f = form.avatar.data
        from werkzeug.utils import secure_filename
        f.save(os.path.join(app.config["UPLOAD_FOLDER"],
                            secure_filename(f.filename)))
        return redirect(url_for("index"))
    return render_template("upload.html", form=form)

Pre-populating Forms (Edit Forms)

@app.route("/posts/<int:id>/edit", methods=["GET", "POST"])
def edit_post(id):
    post = db.get_or_404(Post, id)
    form = PostForm(obj=post)         # populate from SQLAlchemy model
    if form.validate_on_submit():
        form.populate_obj(post)       # copy form data back to model
        db.session.commit()
        return redirect(url_for("show_post", id=id))
    return render_template("edit.html", form=form)

CSRF

Automatic (Flask-WTF)

Flask-WTF adds CSRF to every FlaskForm automatically. Include {{ form.hidden_tag() }} in every form.

AJAX / Fetch without a Form

from flask_wtf.csrf import generate_csrf

@app.context_processor
def csrf_token():
    return {"csrf_token": generate_csrf}
<meta name="csrf-token" content="{{ csrf_token() }}">
fetch("/api/items", {
  method: "POST",
  headers: {
    "X-CSRFToken": document.querySelector("meta[name=csrf-token]").content,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "Widget" }),
});

Exempt Routes from CSRF

There is no csrf_exempt import — exempt views with the exempt method on the CSRFProtect instance:

from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect(app)

@app.route("/webhook", methods=["POST"])
@csrf.exempt
def webhook():
    ...

Or exempt a blueprint:

csrf.exempt(api_bp)

FormField and FieldList (Nested / Dynamic)

class AddressForm(FlaskForm):
    street = StringField("Street")
    city   = StringField("City")

class UserForm(FlaskForm):
    name    = StringField("Name")
    address = FormField(AddressForm)                      # nested form
    phones  = FieldList(StringField("Phone"), min_entries=1)  # dynamic list

# Access:
form.address.street.data
form.phones[0].data
{% for phone in form.phones %}
  {{ phone() }}
{% endfor %}
<button type="button" id="add-phone">+ Add Phone</button>

Flash Messages Pattern

from flask import flash

@app.route("/submit", methods=["POST"])
def submit():
    form = ContactForm()
    if form.validate_on_submit():
        send_email(form.email.data, form.message.data)
        flash("Message sent!", "success")
        return redirect(url_for("index"))
    flash("Please fix the errors below.", "error")
    return render_template("contact.html", form=form), 422
{% for category, msg in get_flashed_messages(with_categories=True) %}
  <div class="alert alert-{{ category }}">{{ msg }}</div>
{% endfor %}