Available for day contractsFrom 21st September I have availability for day and half day contracts. Please contact for more information.

Contact →
mikepreston.org

Python Flask

A lightweight WSGI web application framework for Python that provides tools, libraries, and technologies to build web applications.

Python Flask Cheatsheet

A lightweight WSGI web application framework for Python that provides tools, libraries, and technologies to build web applications.

Overview

Flask is a micro-framework that gives developers flexibility and control over their application architecture. It follows the WSGI specification and uses Werkzeug as its WSGI toolkit and Jinja2 as its template engine.

Flask Application Architecture

Application ComponentsFlask Application StructureClient RequestWSGI ServerFlask AppURL RoutingView FunctionsResponse TypeTemplate RenderingJSON ResponseRedirectJinja2 EngineHTML ResponseClient ResponseBlueprintsExtensionsConfigurationStatic FilesTemplatesApplication ComponentsFlask Application StructureClient RequestWSGI ServerFlask AppURL RoutingView FunctionsResponse TypeTemplate RenderingJSON ResponseRedirectJinja2 EngineHTML ResponseClient ResponseBlueprintsExtensionsConfigurationStatic FilesTemplates

Request Lifecycle

TemplateView FunctionMiddlewareFlask AppWSGI ServerClientTemplateView FunctionMiddlewareFlask AppWSGI ServerClientHTTP RequestWSGI Requestbefore_request hooksContinue/AbortRoute to ViewRender Template (optional)HTML ContentResponse Objectafter_request hooksWSGI ResponseHTTP ResponseTemplateView FunctionMiddlewareFlask AppWSGI ServerClientTemplateView FunctionMiddlewareFlask AppWSGI ServerClientHTTP RequestWSGI Requestbefore_request hooksContinue/AbortRoute to ViewRender Template (optional)HTML ContentResponse Objectafter_request hooksWSGI ResponseHTTP Response

Creating Routes and Handling Requests

Key Concepts

  • Routes: URL patterns mapped to view functions using decorators
  • View Functions: Python functions that handle requests and return responses
  • URL Variables: Dynamic parts of URLs captured as function arguments
  • HTTP Methods: GET, POST, PUT, DELETE, PATCH, etc.
  • Blueprints: Modular application components for organising routes

Common Patterns

Basic Application Setup

from flask import Flask

app = Flask(__name__)

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

if __name__ == '__main__':
    app.run(debug=True)

Route Definitions

# Basic route
@app.route('/home')
def home():
    return 'Home Page'

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

# Route with typed variable
@app.route('/post/<int:post_id>')
def show_post(post_id):
    return f'Post ID: {post_id}'

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

# Route with default values
@app.route('/page/', defaults={'page': 1})
@app.route('/page/<int:page>')
def show_page(page):
    return f'Page: {page}'

URL Variable Converters

# Available converters
@app.route('/string/<string:name>')      # Default, accepts text without slashes
@app.route('/integer/<int:id>')          # Accepts positive integers
@app.route('/decimal/<float:price>')     # Accepts positive floating point
@app.route('/filepath/<path:subpath>')   # Accepts text with slashes
@app.route('/unique/<uuid:code>')        # Accepts UUID strings

Blueprints for Modular Applications

# blueprints/auth.py
from flask import Blueprint

auth_bp = Blueprint('auth', __name__, url_prefix='/auth')

@auth_bp.route('/login')
def login():
    return 'Login Page'

@auth_bp.route('/logout')
def logout():
    return 'Logged Out'

# app.py
from flask import Flask
from blueprints.auth import auth_bp

app = Flask(__name__)
app.register_blueprint(auth_bp)

URL Building

from flask import url_for

# Generate URLs programmatically
@app.route('/profile/<username>')
def profile(username):
    return f'Profile: {username}'

with app.test_request_context():
    print(url_for('index'))                    # /
    print(url_for('profile', username='john')) # /profile/john
    print(url_for('static', filename='style.css'))  # /static/style.css

Examples

RESTful API Routes

from flask import Flask, jsonify, request

app = Flask(__name__)

# In-memory data store
items = []

@app.route('/api/items', methods=['GET'])
def get_items():
    return jsonify(items)

@app.route('/api/items', methods=['POST'])
def create_item():
    data = request.get_json()
    items.append(data)
    return jsonify(data), 201

@app.route('/api/items/<int:item_id>', methods=['GET'])
def get_item(item_id):
    if item_id < len(items):
        return jsonify(items[item_id])
    return jsonify({'error': 'Not found'}), 404

@app.route('/api/items/<int:item_id>', methods=['PUT'])
def update_item(item_id):
    if item_id < len(items):
        data = request.get_json()
        items[item_id] = data
        return jsonify(data)
    return jsonify({'error': 'Not found'}), 404

@app.route('/api/items/<int:item_id>', methods=['DELETE'])
def delete_item(item_id):
    if item_id < len(items):
        deleted = items.pop(item_id)
        return jsonify(deleted)
    return jsonify({'error': 'Not found'}), 404

Route with Query Parameters

from flask import request

@app.route('/search')
def search():
    query = request.args.get('q', '')
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 10, type=int)

    return jsonify({
        'query': query,
        'page': page,
        'per_page': per_page
    })

Request and Response Objects

Key Concepts

  • Request Object: Contains all incoming request data (headers, form data, files, etc.)
  • Response Object: Represents the outgoing HTTP response
  • Context Locals: Thread-local objects (request, g, session) available during request
  • Sessions: Secure cookies for storing user data across requests

Common Patterns

Accessing Request Data

from flask import request

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

    # JSON data
    json_data = request.get_json()

    # Query parameters
    search = request.args.get('search')

    # Headers
    auth_header = request.headers.get('Authorization')
    content_type = request.content_type

    # Request metadata
    method = request.method
    url = request.url
    remote_addr = request.remote_addr
    user_agent = request.user_agent.string

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

    return 'Data received'

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', 'jpeg', 'gif'}

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB limit

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 f'File {filename} uploaded successfully'

    return 'File type not allowed', 400

Creating Responses

from flask import make_response, jsonify, redirect, url_for

# Simple string response
@app.route('/text')
def text_response():
    return 'Plain text response'

# Response with status code
@app.route('/created')
def created_response():
    return 'Resource created', 201

# Response with headers
@app.route('/custom-headers')
def custom_headers():
    response = make_response('Response with custom headers')
    response.headers['X-Custom-Header'] = 'Custom Value'
    response.headers['Cache-Control'] = 'no-cache'
    return response

# JSON response
@app.route('/json')
def json_response():
    return jsonify({
        'status': 'success',
        'data': {'id': 1, 'name': 'Example'}
    })

# Redirect
@app.route('/old-page')
def old_page():
    return redirect(url_for('new_page'))

# Set cookies
@app.route('/set-cookie')
def set_cookie():
    response = make_response('Cookie set')
    response.set_cookie('user_id', '12345',
                        max_age=3600,
                        httponly=True,
                        secure=True,
                        samesite='Lax')
    return response

# Delete cookie
@app.route('/delete-cookie')
def delete_cookie():
    response = make_response('Cookie deleted')
    response.delete_cookie('user_id')
    return response

Sessions

from flask import session

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

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

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

@app.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('index'))

Examples

Complete Request Handler

from flask import Flask, request, jsonify, make_response

app = Flask(__name__)

@app.route('/api/process', methods=['POST'])
def process_request():
    # Validate content type
    if not request.is_json:
        return jsonify({'error': 'Content-Type must be application/json'}), 415

    # Get JSON data
    data = request.get_json()

    # Validate required fields
    if 'name' not in data:
        return jsonify({'error': 'Missing required field: name'}), 400

    # Process data
    result = {
        'id': 1,
        'name': data['name'],
        'processed': True
    }

    # Create response
    response = make_response(jsonify(result))
    response.status_code = 201
    response.headers['Location'] = url_for('get_item', item_id=1)

    return response

Templating with Jinja2

Key Concepts

  • Jinja2: Default template engine for Flask
  • Template Inheritance: Base templates with extendable blocks
  • Filters: Transform variables in templates
  • Macros: Reusable template functions
  • Context Processors: Add variables to all templates

Common Patterns

Basic Template Rendering

from flask import render_template

@app.route('/hello/<name>')
def hello(name):
    return render_template('hello.html', name=name)
<!-- templates/hello.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Hello</title>
</head>
<body>
    <h1>Hello, {{ name }}!</h1>
</body>
</html>

Template Inheritance

<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Default Title{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    {% block extra_css %}{% endblock %}
</head>
<body>
    <nav>
        {% block nav %}
        <a href="{{ url_for('index') }}">Home</a>
        <a href="{{ url_for('about') }}">About</a>
        {% endblock %}
    </nav>

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

    <footer>
        {% block footer %}
        <p>&copy; 2024 My Application</p>
        {% endblock %}
    </footer>

    {% block extra_js %}{% endblock %}
</body>
</html>
<!-- templates/page.html -->
{% extends "base.html" %}

{% block title %}My Page{% endblock %}

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

{% block extra_js %}
<script src="{{ url_for('static', filename='js/page.js') }}"></script>
{% endblock %}

Control Structures

<!-- Conditionals -->
{% if user %}
    <p>Welcome, {{ user.name }}!</p>
{% elif guest %}
    <p>Welcome, Guest!</p>
{% else %}
    <p>Please log in.</p>
{% endif %}

<!-- Loops -->
<ul>
{% for item in items %}
    <li>{{ item.name }} - £{{ item.price }}</li>
{% else %}
    <li>No items found.</li>
{% endfor %}
</ul>

<!-- Loop variables -->
<ul>
{% for user in users %}
    <li>
        {{ loop.index }}: {{ user.name }}
        {% if loop.first %}(First){% endif %}
        {% if loop.last %}(Last){% endif %}
    </li>
{% endfor %}
</ul>

Filters

<!-- Built-in filters -->
{{ name|capitalize }}
{{ description|truncate(100) }}
{{ price|round(2) }}
{{ html_content|safe }}
{{ items|length }}
{{ date|default('N/A') }}
{{ text|striptags }}
{{ list|join(', ') }}
{{ dict|tojson }}

<!-- Custom filters -->
# Register custom filter
@app.template_filter('currency')
def currency_filter(value):
    return f'£{value:,.2f}'

# Usage in template: {{ price|currency }}

Macros

<!-- templates/macros.html -->
{% macro input(name, value='', type='text', placeholder='') %}
    <input type="{{ type }}"
           name="{{ name }}"
           value="{{ value }}"
           placeholder="{{ placeholder }}"
           class="form-input">
{% endmacro %}

{% macro form_field(label, name, value='', type='text', error=None) %}
    <div class="form-group {% if error %}has-error{% endif %}">
        <label for="{{ name }}">{{ label }}</label>
        {{ input(name, value, type) }}
        {% if error %}
            <span class="error">{{ error }}</span>
        {% endif %}
    </div>
{% endmacro %}
<!-- templates/form.html -->
{% from "macros.html" import form_field %}

<form method="post">
    {{ form_field('Username', 'username', errors.username) }}
    {{ form_field('Password', 'password', type='password') }}
    {{ form_field('Email', 'email', type='email') }}
    <button type="submit">Submit</button>
</form>

Context Processors

@app.context_processor
def inject_globals():
    return {
        'site_name': 'My Flask App',
        'current_year': datetime.now().year
    }

# Now available in all templates
# {{ site_name }} and {{ current_year }}

Examples

Complete Template with Forms

<!-- templates/register.html -->
{% extends "base.html" %}

{% block title %}Register{% endblock %}

{% block content %}
<div class="container">
    <h1>Create Account</h1>

    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            {% for category, message in messages %}
                <div class="alert alert-{{ category }}">{{ message }}</div>
            {% endfor %}
        {% endif %}
    {% endwith %}

    <form method="post" enctype="multipart/form-data">
        <div class="form-group">
            <label for="username">Username</label>
            <input type="text" id="username" name="username"
                   value="{{ request.form.username }}" required>
        </div>

        <div class="form-group">
            <label for="email">Email</label>
            <input type="email" id="email" name="email"
                   value="{{ request.form.email }}" required>
        </div>

        <div class="form-group">
            <label for="password">Password</label>
            <input type="password" id="password" name="password" required>
        </div>

        <div class="form-group">
            <label for="avatar">Profile Picture</label>
            <input type="file" id="avatar" name="avatar" accept="image/*">
        </div>

        <button type="submit" class="btn btn-primary">Register</button>
    </form>
</div>
{% endblock %}

Static Files and Asset Management

Key Concepts

  • Static Folder: Default location for CSS, JavaScript, images, and other assets
  • URL Generation: Using url_for('static', filename='...') for asset paths
  • Cache Busting: Strategies for ensuring browsers load updated assets
  • CDN Integration: Serving static files from content delivery networks

Common Patterns

Basic Static File Setup

# Default static folder structure
# myapp/
#   static/
#     css/
#       style.css
#     js/
#       main.js
#     images/
#       logo.png
#   templates/
#   app.py

from flask import Flask

app = Flask(__name__,
            static_folder='static',      # Default
            static_url_path='/static')   # Default

Referencing Static Files in Templates

<!-- CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">

<!-- JavaScript -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>

<!-- Images -->
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">

<!-- Fonts -->
<link rel="preload" href="{{ url_for('static', filename='fonts/custom.woff2') }}"
      as="font" type="font/woff2" crossorigin>

Custom Static File Configuration

# Multiple static folders
app = Flask(__name__)

# Serve from different folder
app.static_folder = 'assets'
app.static_url_path = '/assets'

# Blueprint with its own static files
admin_bp = Blueprint('admin', __name__,
                     static_folder='admin_static',
                     static_url_path='/admin/static')

# Access: {{ url_for('admin.static', filename='admin.css') }}

Cache Busting

import os
import hashlib

def get_file_hash(filepath):
    """Generate hash for cache busting."""
    with open(filepath, 'rb') as f:
        return hashlib.md5(f.read()).hexdigest()[:8]

@app.context_processor
def override_url_for():
    return dict(url_for=dated_url_for)

def dated_url_for(endpoint, **values):
    if endpoint == 'static':
        filename = values.get('filename', None)
        if filename:
            file_path = os.path.join(app.root_path, 'static', filename)
            if os.path.isfile(file_path):
                values['v'] = get_file_hash(file_path)
    return url_for(endpoint, **values)

# Result: /static/css/style.css?v=abc12345

Serving Files from Different Locations

from flask import send_from_directory, send_file

# Serve from custom directory
@app.route('/downloads/<filename>')
def download_file(filename):
    return send_from_directory(
        app.config['DOWNLOAD_FOLDER'],
        filename,
        as_attachment=True
    )

# Serve generated file
@app.route('/report.pdf')
def get_report():
    return send_file(
        'reports/latest.pdf',
        mimetype='application/pdf',
        as_attachment=True,
        download_name='report.pdf'
    )

Examples

Complete Asset Configuration

from flask import Flask
import os

app = Flask(__name__)

# Configuration
app.config.update(
    STATIC_FOLDER='static',
    UPLOAD_FOLDER='uploads',
    MAX_CONTENT_LENGTH=16 * 1024 * 1024,
    SEND_FILE_MAX_AGE_DEFAULT=31536000  # 1 year cache
)

# Ensure upload folder exists
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

# Production static file handling
if not app.debug:
    from whitenoise import WhiteNoise
    app.wsgi_app = WhiteNoise(
        app.wsgi_app,
        root='static/',
        prefix='static/'
    )

Asset Manifest for Production

import json

class AssetManager:
    def __init__(self, manifest_path):
        with open(manifest_path) as f:
            self.manifest = json.load(f)

    def get_asset(self, name):
        return self.manifest.get(name, name)

# manifest.json (generated by build tool)
# {
#   "main.js": "main.abc123.js",
#   "style.css": "style.def456.css"
# }

assets = AssetManager('static/manifest.json')

@app.context_processor
def asset_processor():
    return {'asset': assets.get_asset}

# Usage: {{ url_for('static', filename=asset('main.js')) }}

Middleware and Error Handling

Key Concepts

  • Request Hooks: before_request, after_request, teardown_request
  • Error Handlers: Custom handling for HTTP errors and exceptions
  • Application Context: g object for storing data during request
  • WSGI Middleware: Low-level request/response processing

Common Patterns

Request Hooks

from flask import g, request
import time

@app.before_request
def before_request():
    """Execute before each request."""
    g.start_time = time.time()
    g.user = get_current_user()

@app.after_request
def after_request(response):
    """Execute after each request (before sending response)."""
    # Add timing header
    if hasattr(g, 'start_time'):
        elapsed = time.time() - g.start_time
        response.headers['X-Response-Time'] = f'{elapsed:.3f}s'

    # Add security headers
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'SAMEORIGIN'

    return response

@app.teardown_request
def teardown_request(exception):
    """Execute after request, even if exception occurred."""
    db = getattr(g, 'db', None)
    if db is not None:
        db.close()

@app.teardown_appcontext
def teardown_appcontext(exception):
    """Execute when application context is torn down."""
    pass

Error Handlers

from flask import render_template, jsonify, request

# HTTP error handlers
@app.errorhandler(404)
def not_found(error):
    if request.accept_mimetypes.accept_json:
        return jsonify({'error': 'Not found'}), 404
    return render_template('errors/404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    db.session.rollback()  # Rollback any failed transaction
    if request.accept_mimetypes.accept_json:
        return jsonify({'error': 'Internal server error'}), 500
    return render_template('errors/500.html'), 500

@app.errorhandler(403)
def forbidden(error):
    return render_template('errors/403.html'), 403

@app.errorhandler(400)
def bad_request(error):
    return jsonify({'error': 'Bad request', 'message': str(error)}), 400

# Exception handlers
@app.errorhandler(Exception)
def handle_exception(error):
    """Handle all unhandled exceptions."""
    app.logger.error(f'Unhandled exception: {error}')
    if request.accept_mimetypes.accept_json:
        return jsonify({'error': 'An unexpected error occurred'}), 500
    return render_template('errors/500.html'), 500

# Custom exception
class APIError(Exception):
    def __init__(self, message, status_code=400):
        self.message = message
        self.status_code = status_code

@app.errorhandler(APIError)
def handle_api_error(error):
    return jsonify({'error': error.message}), error.status_code

Aborting Requests

from flask import abort

@app.route('/user/<int:user_id>')
def get_user(user_id):
    user = User.query.get(user_id)
    if user is None:
        abort(404)
    if not user.is_active:
        abort(403)
    return jsonify(user.to_dict())

# Abort with custom message
@app.route('/admin')
def admin():
    if not current_user.is_admin:
        abort(403, description='Admin access required')
    return render_template('admin.html')

WSGI Middleware

class LoggingMiddleware:
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        # Log request
        path = environ.get('PATH_INFO', '')
        method = environ.get('REQUEST_METHOD', '')
        print(f'{method} {path}')

        return self.app(environ, start_response)

app.wsgi_app = LoggingMiddleware(app.wsgi_app)

Authentication Middleware

from functools import wraps
from flask import request, jsonify, g

def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get('Authorization')
        if not token:
            return jsonify({'error': 'No token provided'}), 401

        user = verify_token(token)
        if not user:
            return jsonify({'error': 'Invalid token'}), 401

        g.user = user
        return f(*args, **kwargs)
    return decorated

def require_role(role):
    def decorator(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            if not hasattr(g, 'user') or g.user.role != role:
                return jsonify({'error': 'Insufficient permissions'}), 403
            return f(*args, **kwargs)
        return decorated
    return decorator

@app.route('/api/admin')
@require_auth
@require_role('admin')
def admin_endpoint():
    return jsonify({'message': 'Admin access granted'})

Examples

Complete Error Handling Setup

from flask import Flask, render_template, jsonify, request
import logging
from logging.handlers import RotatingFileHandler

app = Flask(__name__)

# Configure logging
if not app.debug:
    handler = RotatingFileHandler(
        'logs/app.log',
        maxBytes=10240000,
        backupCount=10
    )
    handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
    ))
    handler.setLevel(logging.INFO)
    app.logger.addHandler(handler)
    app.logger.setLevel(logging.INFO)

# Error templates
# templates/errors/base_error.html
# templates/errors/404.html
# templates/errors/500.html

@app.errorhandler(404)
def not_found_error(error):
    app.logger.warning(f'Page not found: {request.url}')
    return render_template('errors/404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    app.logger.error(f'Server error: {error}', exc_info=True)
    return render_template('errors/500.html'), 500

Testing and Debugging

Key Concepts

  • Test Client: Flask's built-in client for testing without running server
  • Test Fixtures: pytest fixtures for app and client setup
  • Debug Mode: Development server with debugger and reloader
  • Logging: Application logging configuration
  • Flask-DebugToolbar: Interactive debugging toolbar

Common Patterns

Basic Test Setup with pytest

# conftest.py
import pytest
from myapp import create_app

@pytest.fixture
def app():
    """Create application for testing."""
    app = create_app({
        'TESTING': True,
        'SECRET_KEY': 'test-secret-key',
        'WTF_CSRF_ENABLED': False,
        'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:'
    })

    with app.app_context():
        # Set up database
        db.create_all()
        yield app
        # Tear down
        db.drop_all()

@pytest.fixture
def client(app):
    """Test client for the application."""
    return app.test_client()

@pytest.fixture
def runner(app):
    """CLI runner for testing commands."""
    return app.test_cli_runner()

@pytest.fixture
def auth_client(client):
    """Authenticated test client."""
    client.post('/auth/login', data={
        'username': 'testuser',
        'password': 'testpass'
    })
    return client

Writing Tests

# test_routes.py
import pytest
import json

def test_index(client):
    """Test index page."""
    response = client.get('/')
    assert response.status_code == 200
    assert b'Welcome' in response.data

def test_not_found(client):
    """Test 404 page."""
    response = client.get('/nonexistent')
    assert response.status_code == 404

def test_create_item(client):
    """Test creating an item."""
    response = client.post('/api/items',
        data=json.dumps({'name': 'Test Item'}),
        content_type='application/json'
    )
    assert response.status_code == 201
    data = json.loads(response.data)
    assert data['name'] == 'Test Item'

def test_login_required(client):
    """Test protected route without authentication."""
    response = client.get('/dashboard')
    assert response.status_code == 302
    assert '/login' in response.location

def test_login(client):
    """Test login functionality."""
    response = client.post('/auth/login', data={
        'username': 'testuser',
        'password': 'testpass'
    }, follow_redirects=True)
    assert response.status_code == 200
    assert b'Dashboard' in response.data

def test_file_upload(client):
    """Test file upload."""
    data = {
        'file': (io.BytesIO(b'test content'), 'test.txt')
    }
    response = client.post('/upload',
        data=data,
        content_type='multipart/form-data'
    )
    assert response.status_code == 200

Testing with Context

def test_request_context(app):
    """Test within request context."""
    with app.test_request_context('/hello', method='POST'):
        assert request.path == '/hello'
        assert request.method == 'POST'

def test_application_context(app):
    """Test within application context."""
    with app.app_context():
        # Access app-specific objects
        assert current_app.config['TESTING'] is True

def test_session(client):
    """Test session data."""
    with client.session_transaction() as sess:
        sess['user_id'] = 1

    response = client.get('/profile')
    assert response.status_code == 200

Debug Configuration

from flask import Flask

app = Flask(__name__)

# Development configuration
if app.debug:
    app.config.update(
        DEBUG=True,
        TEMPLATES_AUTO_RELOAD=True,
        EXPLAIN_TEMPLATE_LOADING=True
    )

# Run with debug mode
if __name__ == '__main__':
    app.run(debug=True, port=5000)

# Or use environment variable
# FLASK_DEBUG=1 flask run

Logging Configuration

import logging
from logging.handlers import RotatingFileHandler, SMTPHandler

def configure_logging(app):
    # Console handler
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.DEBUG if app.debug else logging.INFO)
    console_handler.setFormatter(logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    ))

    # File handler
    file_handler = RotatingFileHandler(
        'logs/app.log',
        maxBytes=1024 * 1024 * 10,  # 10MB
        backupCount=10
    )
    file_handler.setLevel(logging.INFO)
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
    ))

    # Email handler for errors
    if not app.debug and app.config.get('MAIL_SERVER'):
        mail_handler = SMTPHandler(
            mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
            fromaddr=app.config['MAIL_DEFAULT_SENDER'],
            toaddrs=app.config['ADMINS'],
            subject='Application Error'
        )
        mail_handler.setLevel(logging.ERROR)
        app.logger.addHandler(mail_handler)

    app.logger.addHandler(console_handler)
    app.logger.addHandler(file_handler)
    app.logger.setLevel(logging.DEBUG)

# Usage
app.logger.debug('Debug message')
app.logger.info('Info message')
app.logger.warning('Warning message')
app.logger.error('Error message')

Examples

Complete Test Suite

# test_api.py
import pytest
import json
from myapp import create_app, db
from myapp.models import User, Item

class TestAPI:
    @pytest.fixture(autouse=True)
    def setup(self, app, client):
        self.app = app
        self.client = client

        # Create test data
        with app.app_context():
            user = User(username='testuser')
            user.set_password('testpass')
            db.session.add(user)
            db.session.commit()

    def test_get_items_empty(self):
        response = self.client.get('/api/items')
        assert response.status_code == 200
        assert json.loads(response.data) == []

    def test_create_item(self):
        response = self.client.post('/api/items',
            json={'name': 'Test', 'price': 9.99}
        )
        assert response.status_code == 201
        data = json.loads(response.data)
        assert data['name'] == 'Test'
        assert data['price'] == 9.99

    def test_create_item_validation(self):
        response = self.client.post('/api/items',
            json={'price': 9.99}  # Missing name
        )
        assert response.status_code == 400

    def test_get_item(self):
        # Create item first
        self.client.post('/api/items',
            json={'name': 'Test', 'price': 9.99}
        )

        response = self.client.get('/api/items/1')
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data['name'] == 'Test'

    def test_update_item(self):
        self.client.post('/api/items',
            json={'name': 'Test', 'price': 9.99}
        )

        response = self.client.put('/api/items/1',
            json={'name': 'Updated', 'price': 19.99}
        )
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data['name'] == 'Updated'

    def test_delete_item(self):
        self.client.post('/api/items',
            json={'name': 'Test', 'price': 9.99}
        )

        response = self.client.delete('/api/items/1')
        assert response.status_code == 200

        response = self.client.get('/api/items/1')
        assert response.status_code == 404

Quick Reference

Category Command/Pattern Description
App Creation app = Flask(__name__) Create Flask application
Run Server flask run --debug Start development server
Route @app.route('/path') Define URL route
Methods methods=['GET', 'POST'] Specify HTTP methods
URL Variable <variable> or <int:id> Capture URL parameters
Blueprint Blueprint('name', __name__) Create modular component
Request Data request.form['key'] Get form data
Query Params request.args.get('key') Get query parameters
JSON Data request.get_json() Parse JSON body
Response make_response(data) Create custom response
JSON Response jsonify(dict) Return JSON response
Redirect redirect(url_for('view')) Redirect to URL
Render render_template('file.html') Render Jinja2 template
URL Building url_for('view', param=val) Generate URL
Static Files url_for('static', filename='...') Reference static file
Session session['key'] = value Store session data
Flash flash('message', 'category') Flash message
Before Request @app.before_request Run before each request
After Request @app.after_request Run after each request
Error Handler @app.errorhandler(404) Handle HTTP errors
Abort abort(404) Abort with status code
Context g.variable = value Store request-scoped data
Test Client app.test_client() Create test client
Config app.config['KEY'] = value Set configuration
Logger app.logger.info('msg') Log message

Common Issues and Solutions

Issue: "Address already in use" Error

Cause: Port 5000 is already occupied by another process.

Solution:

# Find process using port
lsof -i :5000
# or
netstat -tulpn | grep 5000

# Kill the process
kill -9 <PID>

# Or run on different port
flask run --port 5001

Issue: Templates Not Found

Cause: Templates folder not in expected location.

Solution:

# Ensure correct template folder path
app = Flask(__name__, template_folder='templates')

# Or check folder structure
# myapp/
#   templates/
#     index.html
#   app.py

# Verify template exists
import os
print(os.path.exists('templates/index.html'))

Issue: Static Files Return 404

Cause: Incorrect static file configuration or path.

Solution:

# Check static folder configuration
app = Flask(__name__,
            static_folder='static',
            static_url_path='/static')

# Use url_for in templates
<link href="{{ url_for('static', filename='css/style.css') }}">

# Verify file exists
# myapp/static/css/style.css

Issue: CSRF Token Missing

Cause: Form submission without CSRF protection.

Solution:

from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect(app)

# In template
<form method="post">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
    <!-- or with Flask-WTF -->
    {{ form.hidden_tag() }}
</form>

Issue: Session Data Not Persisting

Cause: Missing or incorrect secret key.

Solution:

import os

# Set secret key (required for sessions)
app.secret_key = os.environ.get('SECRET_KEY') or 'dev-secret-key'

# For production, use strong random key
import secrets
app.secret_key = secrets.token_hex(16)

Issue: JSON Request Data is None

Cause: Missing Content-Type header or malformed JSON.

Solution:

@app.route('/api/data', methods=['POST'])
def receive_data():
    # Check if JSON
    if not request.is_json:
        return jsonify({'error': 'Content-Type must be application/json'}), 400

    # Get JSON with error handling
    try:
        data = request.get_json(force=False)  # Default behaviour
    except Exception as e:
        return jsonify({'error': 'Invalid JSON'}), 400

    if data is None:
        return jsonify({'error': 'No JSON data'}), 400

    return jsonify(data)

Issue: Circular Import Errors

Cause: Modules importing each other.

Solution:

# Use application factory pattern
# myapp/__init__.py
from flask import Flask

def create_app():
    app = Flask(__name__)

    with app.app_context():
        from . import routes
        app.register_blueprint(routes.bp)

    return app

# Or use late imports
# routes.py
from flask import Blueprint
bp = Blueprint('main', __name__)

@bp.route('/')
def index():
    from myapp.models import User  # Import inside function
    return 'Hello'

Issue: Database Connection Errors in Production

Cause: Connection pool exhaustion or timeout.

Solution:

# Configure connection pooling
app.config.update(
    SQLALCHEMY_POOL_SIZE=10,
    SQLALCHEMY_POOL_TIMEOUT=20,
    SQLALCHEMY_POOL_RECYCLE=300,  # Recycle connections every 5 minutes
    SQLALCHEMY_MAX_OVERFLOW=5
)

# Ensure connections are closed
@app.teardown_appcontext
def shutdown_session(exception=None):
    db.session.remove()

Issue: Slow Performance in Debug Mode

Cause: Debug mode enables reloader and debugger overhead.

Solution:

# Disable reloader if not needed
app.run(debug=True, use_reloader=False)

# Or disable debugger
app.run(debug=False)

# For production, use proper WSGI server
# gunicorn -w 4 myapp:app

Related Topics

The following topics would complement this Flask cheatsheet:

  1. Flask-SQLAlchemy and Database Integration - ORM patterns, migrations with Alembic, query optimisation, and database relationships

  2. Flask-RESTful and API Development - Building RESTful APIs, serialisation with Marshmallow, API versioning, and documentation with Swagger/OpenAPI

  3. Flask Security and Authentication - Flask-Login, Flask-JWT-Extended, OAuth integration, password hashing, and security best practices

  4. Flask Deployment and Production - Gunicorn/uWSGI configuration, Nginx setup, Docker containerisation, and cloud deployment strategies

  5. Flask Extensions Ecosystem - Flask-Mail, Flask-Caching, Flask-Migrate, Flask-Admin, and other essential extensions

  6. Async Flask and Performance - Async views with Flask 2.0+, background tasks with Celery, caching strategies, and performance monitoring