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
graph TB
subgraph "Flask Application Structure"
A[Client Request] --> B[WSGI Server]
B --> C[Flask App]
C --> D{URL Routing}
D --> E[View Functions]
E --> F{Response Type}
F --> G[Template Rendering]
F --> H[JSON Response]
F --> I[Redirect]
G --> J[Jinja2 Engine]
J --> K[HTML Response]
H --> K
I --> K
K --> L[Client Response]
end
subgraph "Application Components"
M[Blueprints]
N[Extensions]
O[Configuration]
P[Static Files]
Q[Templates]
end
C --> M
C --> N
C --> O
C --> P
C --> Q
Request Lifecycle
sequenceDiagram
participant C as Client
participant W as WSGI Server
participant F as Flask App
participant M as Middleware
participant V as View Function
participant T as Template
C->>W: HTTP Request
W->>F: WSGI Request
F->>M: before_request hooks
M->>F: Continue/Abort
F->>V: Route to View
V->>T: Render Template (optional)
T->>V: HTML Content
V->>F: Response Object
F->>M: after_request hooks
M->>W: WSGI Response
W->>C: HTTP 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>© 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:
-
Flask-SQLAlchemy and Database Integration - ORM patterns, migrations with Alembic, query optimisation, and database relationships
-
Flask-RESTful and API Development - Building RESTful APIs, serialisation with Marshmallow, API versioning, and documentation with Swagger/OpenAPI
-
Flask Security and Authentication - Flask-Login, Flask-JWT-Extended, OAuth integration, password hashing, and security best practices
-
Flask Deployment and Production - Gunicorn/uWSGI configuration, Nginx setup, Docker containerisation, and cloud deployment strategies
-
Flask Extensions Ecosystem - Flask-Mail, Flask-Caching, Flask-Migrate, Flask-Admin, and other essential extensions
-
Async Flask and Performance - Async views with Flask 2.0+, background tasks with Celery, caching strategies, and performance monitoring