NexusCS

Flask

Python
Quick reference guide for Flask, the Python micro web framework. Covers routing, templates, blueprints, and more.
python
web
framework
api

Getting started

Introduction

Flask is a lightweight WSGI web application framework in Python. It's designed to make getting started quick and easy, with the ability to scale up to complex applications.

Installation

# Install Flask
python -m pip install Flask

Minimal Application

# hello.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return '<p>Hello, World!</p>'

Run the app:

# Development server
flask --app hello run

# With debug mode
flask --app hello run --debug

# Custom host and port
flask --app hello run --host=0.0.0.0 --port=8080

Application Factory Pattern

# app.py
from flask import Flask

def create_app(config=None):
    app = Flask(__name__)

    if config:
        app.config.from_object(config)

    @app.route('/')
    def index():
        return 'Hello from factory!'

    return app
# Run with factory
flask --app 'app:create_app()' run

Routing

Basic Routes

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Index Page'

@app.route('/hello')
def hello():
    return 'Hello, World'

@app.route('/user/<username>')
def show_user(username):
    return f'User: {username}'

@app.route('/post/<int:post_id>')
def show_post(post_id):
    return f'Post {post_id}'

Variable Converters

Converter Description Example
string Accepts any text (default) <string:name>
int Accepts integers <int:id>
float Accepts floats <float:price>
path Like string but accepts slashes <path:subpath>
uuid Accepts UUID strings <uuid:id>
@app.route('/file/<path:filepath>')
def show_file(filepath):
    # filepath can contain slashes
    return f'File: {filepath}'

@app.route('/product/<uuid:product_id>')
def show_product(product_id):
    # product_id is a UUID object
    return f'Product: {product_id}'

HTTP Methods

from flask import request

# Allow specific methods
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        return do_login()
    else:
        return show_login_form()

# Or use dedicated decorators
@app.get('/users')
def get_users():
    return 'List of users'

@app.post('/users')
def create_user():
    return 'Create user'

@app.put('/users/<int:id>')
def update_user(id):
    return f'Update user {id}'

@app.delete('/users/<int:id>')
def delete_user(id):
    return f'Delete user {id}'

URL Building

from flask import url_for

@app.route('/')
def index():
    return 'index'

@app.route('/user/<username>')
def profile(username):
    return f'{username}\'s profile'

with app.test_request_context():
    print(url_for('index'))
    # Output: /

    print(url_for('profile', username='john'))
    # Output: /user/john

    print(url_for('profile', username='jane', page=2))
    # Output: /user/jane?page=2

Trailing Slashes

# WITH trailing slash (canonical)
@app.route('/projects/')
def projects():
    return 'Projects'
# /projects redirects to /projects/
# /projects/ works

# WITHOUT trailing slash (canonical)
@app.route('/about')
def about():
    return 'About'
# /about works
# /about/ returns 404

Request & Response

Request Object

from flask import request

@app.route('/submit', methods=['POST'])
def submit():
    # Form data
    username = request.form['username']

    # Query parameters
    page = request.args.get('page', 1, type=int)

    # JSON data
    data = request.json

    # Files
    file = request.files['file']

    # Cookies
    token = request.cookies.get('token')

    # Headers
    user_agent = request.headers.get('User-Agent')

    # Other attributes
    method = request.method
    url = request.url
    remote_addr = request.remote_addr

    return 'OK'

Request Data Access

from flask import request

@app.route('/data', methods=['POST'])
def handle_data():
    # Get form data with default
    name = request.form.get('name', 'Anonymous')

    # Get all values for a key (multiple selects)
    colors = request.form.getlist('colors')

    # Check if key exists
    if 'email' in request.form:
        email = request.form['email']

    # JSON data
    if request.is_json:
        data = request.get_json()
        # or
        data = request.json

    return 'Data received'

Response Types

from flask import make_response, jsonify, redirect, render_template

@app.route('/string')
def return_string():
    return 'Plain text response'

@app.route('/json')
def return_json():
    # Auto-converted to JSON
    return {'key': 'value', 'number': 42}

@app.route('/jsonify')
def return_jsonify():
    # Explicit JSON response
    return jsonify(key='value', number=42)

@app.route('/template')
def return_template():
    return render_template('index.html', name='John')

@app.route('/redirect-example')
def redirect_example():
    return redirect('/other-page')

@app.route('/custom-response')
def custom_response():
    resp = make_response(render_template('index.html'))
    resp.set_cookie('username', 'flask')
    resp.headers['X-Custom'] = 'value'
    return resp

@app.route('/tuple-response')
def tuple_response():
    # (body, status, headers)
    return 'Not Found', 404
    # or
    return {'error': 'Not Found'}, 404, {'X-Custom': 'value'}

File Uploads

from flask import request
from werkzeug.utils import secure_filename
import os

UPLOAD_FOLDER = '/path/to/uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg'}

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return 'No file part', 400

    file = request.files['file']

    if file.filename == '':
        return 'No selected file', 400

    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        return 'File uploaded successfully'

    return 'Invalid file type', 400

Templates (Jinja2)

Rendering Templates

from flask import render_template

@app.route('/hello/<name>')
def hello(name):
    return render_template('hello.html', name=name)

@app.route('/users')
def users():
    users = [
        {'name': 'John', 'age': 30},
        {'name': 'Jane', 'age': 25}
    ]
    return render_template('users.html', users=users)

Template Variables

{# templates/hello.html #}
<!DOCTYPE html>
<html>
<head>
    <title>Hello {{ name }}</title>
</head>
<body>
    <h1>Hello, {{ name }}!</h1>
    <p>Welcome to Flask.</p>
</body>
</html>

Template Tags

{# Control structures #}
{% if user %}
    <p>Hello, {{ user }}!</p>
{% else %}
    <p>Hello, Guest!</p>
{% endif %}

{% for item in items %}
    <li>{{ item }}</li>
{% endfor %}

{# Comments (not in output) #}
{# This is a comment #}

Template Filters

{# String filters #}
{{ name|upper }}
{{ description|lower }}
{{ title|title }}
{{ text|capitalize }}

{# Default value #}
{{ username|default('Anonymous') }}

{# String operations #}
{{ text|truncate(20) }}
{{ text|replace('old', 'new') }}
{{ html|safe }}  {# Don't escape HTML #}

{# Lists #}
{{ items|length }}
{{ items|first }}
{{ items|last }}
{{ items|join(', ') }}

{# Numbers #}
{{ price|round(2) }}
{{ number|int }}

Template Inheritance

{# templates/base.html #}
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My Site{% endblock %}</title>
</head>
<body>
    <header>
        <nav>{% block nav %}{% endblock %}</nav>
    </header>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        {% block footer %}
            &copy; 2026 My Site
        {% endblock %}
    </footer>
</body>
</html>
{# templates/page.html #}
{% extends "base.html" %}

{% block title %}Page Title{% endblock %}

{% block content %}
    <h1>Page Content</h1>
    <p>This is the content.</p>
{% endblock %}

Include & Macros

{# Include other templates #}
{% include 'header.html' %}

{# Define macros (reusable template functions) #}
{% macro input(name, value='', type='text') %}
    <input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}

{# Use macro #}
{{ input('username') }}
{{ input('email', type='email') }}

Sessions & Messages

Session Configuration

from flask import Flask, session

app = Flask(__name__)

# Required for sessions
app.secret_key = 'your-secret-key-here'

# Or load from config
app.config['SECRET_KEY'] = 'your-secret-key-here'

Using Sessions

from flask import session, redirect, url_for

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    session['username'] = username
    session['logged_in'] = True
    return redirect(url_for('dashboard'))

@app.route('/dashboard')
def dashboard():
    if 'username' in session:
        return f'Welcome, {session["username"]}!'
    return redirect(url_for('login'))

@app.route('/logout')
def logout():
    session.pop('username', None)
    session.pop('logged_in', None)
    # Or clear all
    session.clear()
    return redirect(url_for('index'))

Flash Messages

from flask import flash, get_flashed_messages

@app.route('/save', methods=['POST'])
def save():
    # Save data...
    flash('Data saved successfully!')
    # With category
    flash('Warning: Something to note', 'warning')
    flash('Error occurred', 'error')
    return redirect(url_for('index'))

@app.route('/')
def index():
    # Get messages in route (usually done in template)
    messages = get_flashed_messages(with_categories=True)
    return render_template('index.html')
{# templates/index.html #}
{% with messages = get_flashed_messages(with_categories=true) %}
  {% if messages %}
    <ul class="flashes">
      {% for category, message in messages %}
        <li class="flash-{{ category }}">{{ message }}</li>
      {% endfor %}
    </ul>
  {% endif %}
{% endwith %}

Configuration

Basic Configuration

from flask import Flask

app = Flask(__name__)

# Direct assignment
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'dev-secret-key'
app.config['DATABASE_URI'] = 'sqlite:///app.db'

# Update multiple values
app.config.update(
    DEBUG=True,
    SECRET_KEY='dev-secret-key',
    DATABASE_URI='sqlite:///app.db'
)

Config from Files

# From Python file
app.config.from_pyfile('config.py')

# From environment variable
app.config.from_envvar('APP_CONFIG')

# From object
app.config.from_object('config.DevelopmentConfig')

# From JSON file
import json
app.config.from_file('config.json', load=json.load)

Config Classes

# config.py
class Config:
    SECRET_KEY = 'default-secret-key'
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///dev.db'

class ProductionConfig(Config):
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = 'postgresql://user:pass@localhost/db'

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
# app.py
import os
from config import DevelopmentConfig, ProductionConfig

config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig
}

env = os.getenv('FLASK_ENV', 'development')
app.config.from_object(config[env])

Common Config Keys

Key Description Default
DEBUG Enable debug mode False
TESTING Enable testing mode False
SECRET_KEY Secret key for sessions None
SESSION_COOKIE_NAME Session cookie name 'session'
PERMANENT_SESSION_LIFETIME Session lifetime timedelta(31)
MAX_CONTENT_LENGTH Max request size (bytes) None
JSON_SORT_KEYS Sort JSON keys True

Blueprints

Creating Blueprints

# auth/routes.py
from flask import Blueprint, render_template, redirect

auth = Blueprint('auth', __name__)

@auth.route('/login')
def login():
    return render_template('auth/login.html')

@auth.route('/logout')
def logout():
    return redirect('/')

@auth.route('/register')
def register():
    return render_template('auth/register.html')

Registering Blueprints

# app.py
from flask import Flask
from auth.routes import auth
from blog.routes import blog

app = Flask(__name__)

# Simple registration
app.register_blueprint(auth)

# With URL prefix
app.register_blueprint(blog, url_prefix='/blog')

# With subdomain
app.register_blueprint(api, subdomain='api')

Blueprint Structure

# blog/__init__.py
from flask import Blueprint

blog = Blueprint('blog', __name__,
                 template_folder='templates',
                 static_folder='static',
                 static_url_path='/blog/static')

from blog import routes
# blog/routes.py
from blog import blog
from flask import render_template

@blog.route('/')
def index():
    return render_template('blog/index.html')

@blog.route('/post/<int:id>')
def post(id):
    return render_template('blog/post.html', id=id)

Blueprint URL Building

from flask import url_for

# Within same blueprint
url_for('.index')  # Relative to current blueprint

# From different blueprint
url_for('auth.login')
url_for('blog.post', id=1)
url_for('blog.static', filename='style.css')

Blueprint Error Handlers

from flask import Blueprint

api = Blueprint('api', __name__)

@api.errorhandler(404)
def api_not_found(error):
    return {'error': 'Not found'}, 404

@api.errorhandler(500)
def api_server_error(error):
    return {'error': 'Internal server error'}, 500

@api.before_request
def before_api_request():
    # Runs before each request to this blueprint
    pass

@api.after_request
def after_api_request(response):
    # Runs after each request to this blueprint
    return response

Error Handling

Error Handlers

from flask import render_template

@app.errorhandler(404)
def not_found(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    return render_template('500.html'), 500

@app.errorhandler(Exception)
def handle_exception(e):
    # Log the error
    app.logger.error(f'Unhandled exception: {e}')
    return 'Internal Server Error', 500

Raising Errors

from flask import abort

@app.route('/user/<int:user_id>')
def show_user(user_id):
    user = get_user(user_id)
    if user is None:
        abort(404)
    return render_template('user.html', user=user)

@app.route('/admin')
def admin():
    if not is_admin():
        abort(403)  # Forbidden
    return render_template('admin.html')

Custom Error Pages

{# templates/404.html #}
<!DOCTYPE html>
<html>
<head>
    <title>Page Not Found</title>
</head>
<body>
    <h1>404 - Page Not Found</h1>
    <p>The page you're looking for doesn't exist.</p>
    <a href="{{ url_for('index') }}">Go Home</a>
</body>
</html>

Logging

import logging

# Configure logging
logging.basicConfig(level=logging.DEBUG)

# Use app logger
@app.route('/log')
def log_example():
    app.logger.debug('Debug message')
    app.logger.info('Info message')
    app.logger.warning('Warning message')
    app.logger.error('Error message')
    app.logger.critical('Critical message')
    return 'Check logs'

Context & Globals

Application Context

from flask import Flask, current_app

app = Flask(__name__)
app.config['VALUE'] = 'example'

def some_function():
    # Access app outside request
    with app.app_context():
        print(current_app.config['VALUE'])

# Or push context manually
ctx = app.app_context()
ctx.push()
# Do work
ctx.pop()

Request Context

from flask import request

# In request context
@app.route('/')
def index():
    user_agent = request.headers.get('User-Agent')
    return f'Your browser is {user_agent}'

# Test request context
with app.test_request_context('/hello?name=John'):
    assert request.path == '/hello'
    assert request.args['name'] == 'John'

g Object (Request-Local Storage)

from flask import g, request
import sqlite3

@app.before_request
def before_request():
    # Store data for this request
    g.user = get_current_user()
    g.db = sqlite3.connect('database.db')

@app.teardown_request
def teardown_request(exception):
    # Clean up after request
    db = g.pop('db', None)
    if db is not None:
        db.close()

@app.route('/profile')
def profile():
    # Access stored data
    return f'Hello, {g.user.name}!'

Context Locals

from flask import current_app, request, session, g

# Available in request context
request.method
request.args
request.form
request.files
session['key']
g.user

# Available in app context
current_app.config['KEY']
current_app.logger

CLI Commands

Built-in Commands

# Run development server
flask run

# Run with options
flask run --host=0.0.0.0 --port=8080 --debug

# Open Python shell with app context
flask shell

# Show all routes
flask routes

# Show more route details
flask routes --sort rule
flask routes --all-methods

Custom Commands

import click
from flask import Flask

app = Flask(__name__)

@app.cli.command()
def initdb():
    """Initialize the database."""
    print('Initializing database...')
    # Database initialization code
    print('Database initialized!')

@app.cli.command()
@click.argument('name')
def greet(name):
    """Greet a user."""
    print(f'Hello, {name}!')

@app.cli.command()
@click.option('--count', default=1, help='Number of times to run')
def process(count):
    """Process data."""
    for i in range(count):
        print(f'Processing iteration {i+1}')

Run commands:

flask initdb
flask greet John
flask process --count=3

Command Groups

@app.cli.group()
def user():
    """User management commands."""
    pass

@user.command()
@click.argument('name')
def create(name):
    """Create a new user."""
    print(f'Creating user: {name}')

@user.command()
def list():
    """List all users."""
    print('Listing all users...')
flask user create john
flask user list

Testing

Test Client

import pytest
from app import create_app

@pytest.fixture()
def app():
    app = create_app({'TESTING': True})
    yield app

@pytest.fixture()
def client(app):
    return app.test_client()

def test_index(client):
    response = client.get('/')
    assert response.status_code == 200
    assert b'Hello' in response.data

def test_post_data(client):
    response = client.post('/submit', data={
        'username': 'test',
        'password': 'secret'
    })
    assert response.status_code == 200

JSON Testing

def test_json_api(client):
    response = client.get('/api/users')
    assert response.status_code == 200
    assert response.is_json

    data = response.get_json()
    assert len(data) > 0
    assert 'name' in data[0]

def test_post_json(client):
    response = client.post('/api/users', json={
        'name': 'John',
        'email': 'john@example.com'
    })
    assert response.status_code == 201
    data = response.get_json()
    assert data['name'] == 'John'

Session Testing

def test_login_logout(client):
    # Test login
    response = client.post('/login', data={
        'username': 'test',
        'password': 'secret'
    }, follow_redirects=True)
    assert b'Welcome' in response.data

    # Test authenticated request
    response = client.get('/dashboard')
    assert response.status_code == 200

    # Test logout
    response = client.get('/logout', follow_redirects=True)
    assert b'Logged out' in response.data

    # Test requires auth
    response = client.get('/dashboard')
    assert response.status_code == 302  # Redirect

CLI Testing

def test_cli_command(app):
    runner = app.test_cli_runner()

    result = runner.invoke(args=['initdb'])
    assert 'Database initialized' in result.output

    result = runner.invoke(args=['greet', 'John'])
    assert 'Hello, John' in result.output

Popular Extensions

Flask-SQLAlchemy

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

# Create tables
with app.app_context():
    db.create_all()

Flask-Login

from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required

login_manager = LoginManager()
login_manager.init_app(app)

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

@app.route('/login', methods=['POST'])
def login():
    user = User.query.filter_by(username=username).first()
    login_user(user)
    return redirect('/')

@app.route('/protected')
@login_required
def protected():
    return 'Logged in users only!'

Flask-WTF (Forms)

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email

class LoginForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    submit = SubmitField('Login')

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # Process form data
        return redirect('/')
    return render_template('login.html', form=form)

Flask-CORS

from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # Enable CORS for all routes

# Or specific routes
@app.route('/api/data')
@cross_origin()
def get_data():
    return {'data': 'value'}

Flask-Migrate

from flask_migrate import Migrate

app = Flask(__name__)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
# Initialize migrations
flask db init

# Create migration
flask db migrate -m "Add users table"

# Apply migrations
flask db upgrade

# Rollback
flask db downgrade

Gotchas

Debug Mode in Production

⚠️ Never run debug mode in production:

# DON'T DO THIS IN PRODUCTION
app.run(debug=True)  # Security risk!

Debug mode enables the interactive debugger and auto-reloader, which can:

  • Expose source code and variables
  • Execute arbitrary code via debugger
  • Consume excessive resources

Always use a production WSGI server (Gunicorn, uWSGI, Waitress).

Trailing Slashes Matter

⚠️ Flask treats URLs with/without trailing slashes differently:

@app.route('/projects/')  # WITH trailing slash
def projects():
    pass
# /projects redirects to /projects/ (301)
# /projects/ works (200)

@app.route('/about')  # WITHOUT trailing slash
def about():
    pass
# /about works (200)
# /about/ returns 404

Secret Key Required for Sessions

⚠️ Sessions won't work without a secret key:

# WRONG - Sessions will fail
app = Flask(__name__)
session['key'] = 'value'  # RuntimeError!

# CORRECT
app.secret_key = 'your-secret-key'
# or
app.config['SECRET_KEY'] = 'your-secret-key'

Don't Name Your File flask.py

⚠️ Naming your application file flask.py will conflict with the Flask module:

# DON'T DO THIS
# flask.py
from flask import Flask  # ImportError!

# DO THIS INSTEAD
# app.py or application.py
from flask import Flask

Context Required for Certain Operations

⚠️ Some operations require an application or request context:

# WRONG - No context
print(current_app.config['KEY'])  # RuntimeError!

# CORRECT
with app.app_context():
    print(current_app.config['KEY'])

# In views (request context exists)
@app.route('/')
def index():
    print(current_app.config['KEY'])  # Works!

Request Data Persistence

⚠️ Request data doesn't persist between requests:

@app.route('/page1')
def page1():
    request.user = 'John'  # Don't do this!
    return 'OK'

@app.route('/page2')
def page2():
    # request.user is gone
    return request.user  # AttributeError!

# Use session instead
@app.route('/page1')
def page1():
    session['user'] = 'John'
    return 'OK'

@app.route('/page2')
def page2():
    return session.get('user', 'Guest')

Mutable Default Arguments

⚠️ Be careful with mutable defaults in routes:

# WRONG
@app.route('/data')
def get_data(cache={}):  # Shared across requests!
    if 'data' not in cache:
        cache['data'] = expensive_operation()
    return cache['data']

# CORRECT
@app.route('/data')
def get_data():
    if 'data' not in g:
        g.data = expensive_operation()
    return g.data

Also see