Flask Cheatsheet

Forms

Use this Flask reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

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 %}