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
| Validator | Usage | Description |
|---|---|---|
DataRequired() | Default | Field 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
| Field | Import | HTML Input |
|---|---|---|
StringField | wtforms | <input type="text"> |
TextAreaField | wtforms | <textarea> |
PasswordField | wtforms | <input type="password"> |
HiddenField | wtforms | <input type="hidden"> |
EmailField | wtforms | <input type="email"> |
TelField | wtforms | <input type="tel"> |
URLField | wtforms | <input type="url"> |
DateField | wtforms | <input type="date"> |
DateTimeField | wtforms | <input type="datetime-local"> |
DateTimeLocalField | wtforms | <input type="datetime-local"> |
IntegerField | wtforms | <input type="number"> |
FloatField | wtforms | <input type="number" step="any"> |
DecimalField | wtforms | <input type="number"> |
BooleanField | wtforms | <input type="checkbox"> |
RadioField | wtforms | <input type="radio"> |
SelectField | wtforms | <select> |
SelectMultipleField | wtforms | <select multiple> |
FileField | wtforms | <input type="file"> |
MultipleFileField | wtforms | <input type="file" multiple> |
SubmitField | wtforms | <input type="submit"> |
FormField | wtforms | Embed a sub-form |
FieldList | wtforms | Repeated 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 %}