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.
- Official docs: flask.palletsprojects.com
- Version: 3.2.x
- License: BSD-3-Clause
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 %}
© 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
- Flask Official Documentation - Comprehensive guide and API reference
- Flask Mega-Tutorial - In-depth tutorial by Miguel Grinberg
- Awesome Flask - Curated list of Flask resources
- Flask Extensions - Official extension registry
- Jinja2 Documentation - Template engine reference
- Werkzeug Documentation - WSGI toolkit (Flask's foundation)