Flask cheat sheet

A scannable Flask reference: 16 short snippets across 7 topics, each linking back to the lesson it came from.

At a glance

TopicWhat it covers
Your first Flask appFlask matches the most specific rule and raises on ambiguity. Using url_for means renaming a route does not break yourlesson
Templates with Jinja2Output is auto-escaped: {{ value }} renders HTML special characters safely, which is what protects you from XSSlesson
Configuration and project structureOrganise config as classes, read secrets from the environment, keep machine-specific files in the instance folder, andlesson
Schema migrations with Flask-MigrateInitialise Alembic through Flask-Migrate, review the scripts autogenerate produces, and apply schema and data changeslesson
Building and testing a JSON APIReturn consistent JSON with honest status codes, then test the contract with the test client, fixtures and a databaselesson
File uploads, static files and sending emailAccept uploads without writing an attacker-controlled filename, cache static assets correctly, and send mail withoutlesson
Deployment: gunicorn, nginx and DockerRun Flask under a real WSGI server, put nginx in front for TLS and static files, and containerise the app withoutlesson

Quick snippets

Your first Flask app

The smallest app

pip install flask
python app.py                 # http://127.0.0.1:5000
flask --app app run --debug    # the CLI way

Configuration and project layout

myapp/
  app.py
  config.py
  templates/
  static/
  requirements.txt

Configuration and project layout

import os

app.config.update(
    SECRET_KEY=os.environ["SECRET_KEY"],   # required for sessions and flash
    DEBUG=False,
    JSON_SORT_KEYS=False,
)

Full lesson: Your first Flask app →

Templates with Jinja2

Rendering

from flask import render_template

@app.get("/posts")
def posts():
    items = [{"title": "Hello", "slug": "hello"}]
    return render_template("posts.html", posts=items, user=None)

Template inheritance

{# templates/base.html #}
<!DOCTYPE html>
<html lang="en">
<head><title>{% block title %}Site{% endblock %}</title></head>
<body>
  <header>{% include "_nav.html" %}</header>
  <main>{% block content %}{% endblock %}</main>
</body>
</html>

Template inheritance

{# templates/posts.html #}
{% extends "base.html" %}
{% block title %}Posts · Site{% endblock %}
{% block content %}
  <h1>Posts</h1>
{% endblock %}

Full lesson: Templates with Jinja2 →

Configuration and project structure

Development, test and production

export FLASK_CONFIG=production
export SECRET_KEY="$(python -c 'import secrets; print(secrets.token_hex(32))')"
export DATABASE_URL="postgresql+psycopg://app:pw@db:5432/app"
flask --app app:create_app run

Full lesson: Configuration and project structure →

Schema migrations with Flask-Migrate

init, migrate, upgrade

pip install flask-migrate

flask db init                       # creates migrations/ + a starter script
flask db migrate -m "add post.published"
flask db upgrade                    # apply pending revisions
flask db current                    # revision the database is on
flask db history --verbose          # the whole chain
flask db downgrade -1               # back one revision
flask db revision -m "manual change" --autogenerate
flask db revision -m "data fix"          # empty script to edit by hand

init, migrate, upgrade

migrations/
  alembic.ini          # generated, points at the app metadata
  env.py               # loads the Flask app, wires target_metadata
  versions/
    a1b2c3d4_add_post_published.py
    b2c3d4e5_add_tags_table.py

Reviewing and editing the generated script

def upgrade():
    # data migration in the same script: run SQL, then tighten the constraint
    op.execute("UPDATE posts SET slug = lower(replace(title, ' ', '-')) WHERE slug IS NULL")
    op.alter_column("posts", "slug", nullable=False)

    # or insert reference rows, with the table's real columns
    op.bulk_insert(
        sa.table("tags", sa.column("id", sa.Integer), sa.column("name", sa.String)),
        [{"id": 1, "name": "flask"}, {"id": 2, "name": "python"}],
    )

Full lesson: Schema migrations with Flask-Migrate →

Building and testing a JSON API

Edge cases worth a test

coverage run -m pytest && coverage report --skip-covered --fail-under=85
pytest -x -q --maxfail=1                # stop at the first failure while iterating

Full lesson: Building and testing a JSON API →

File uploads, static files and sending email

Static assets and caching

<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
<img src="{{ url_for('static', filename='img/logo.svg') }}" alt="Logo">
<!-- cache buster when the file has no content hash in its name -->
<img src="{{ url_for('static', filename='img/chart.png', v=asset_version) }}" alt="Chart">
{# asset_version is a build id such as a commit sha, injected by a context processor #}

Static assets and caching

# per-response caching for everything Flask serves from static/
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0                  # dev: no caching
# production: hand /static/ to nginx or a CDN and let it set the header
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 60 * 60 * 24 * 365

Full lesson: File uploads, static files and sending email →

Deployment: gunicorn, nginx and Docker

WSGI servers and gunicorn

pip install gunicorn

# factory pattern: resolve create_app() at start-up
gunicorn -w 4 -k gthread --threads 4 -b 127.0.0.1:8000 --timeout 60 \
  --access-logfile - --error-logfile - "app:create_app()"

# simpler layouts can point at a module-level app object
gunicorn -w 4 -b 127.0.0.1:8000 run:app

# reload workers without dropping connections
kill -HUP "$(cat /run/gunicorn.pid)"

nginx in front

# trust the proxy headers, or request.remote_addr is 127.0.0.1
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)

# ProxyFix rewrites the scheme, so url_for(_external=True) generates https
# and request.remote_addr, request.host and request.is_secure become correct.
# Only ever wrap the app once, for the exact number of trusted proxies.

Containers and release hygiene

docker build -t registry.example.com/app:1.4.2 .

# secrets arrive at run time, not at build time
docker run --rm -p 8000:8000 \
  -e FLASK_CONFIG=production \
  -e SECRET_KEY="$(cat /run/secrets/flask_key)" \
  -e DATABASE_URL="postgresql+psycopg://app:pw@db:5432/app" \
  registry.example.com/app:1.4.2

# schema first, then the new code, from a one-off container
docker run --rm -e FLASK_CONFIG=production registry.example.com/app:1.4.2 flask db upgrade

Full lesson: Deployment: gunicorn, nginx and Docker →

FAQ

Is this Flask cheat sheet free to use?
Yes. No sign-up and no tracking: the page is static, every example is on the page itself, and you can print it or save it as a one-page reference.
Where do the examples come from?
Every snippet is taken from the 7 lessons of the Flask course on this site, and each section links back to the lesson it was pulled from.
How do I go deeper than a cheat sheet?
Open the full Flask course — it carries the worked explanations, the edge cases and the exercises behind every line here.

Python 3 NumPy pandas Matplotlib Jupyter Notebook FastAPI

Last refreshed 2026-09-27.