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