Flask Web Framework: Complete Guide for 2026
Flask is Python's most popular micro-framework. It gives you a URL router, a template engine, a development server, and a debugger — then gets out of your way. You choose the database library, the authentication system, the form validator, and every other component. That freedom makes Flask the go-to choice for REST APIs, microservices, prototypes, and any project where you want to understand every layer of your application.
This guide takes you from installation through production deployment, with working code at every step. Whether you are building a simple API, a full web application, or a microservice, the patterns here will carry you from first route to production.
Table of Contents
- What Is Flask? Why Flask?
- Installation & Project Setup
- Your First Flask App
- Routing & URL Parameters
- Templates with Jinja2
- Forms & Request Handling
- Database with Flask-SQLAlchemy
- REST API Development
- Authentication & Sessions
- Blueprints & App Structure
- Deployment
- Best Practices & Common Pitfalls
- FAQ
1. What Is Flask? Why Flask?
Flask was created by Armin Ronacher in 2010. It wraps two libraries he also wrote: Werkzeug (a WSGI toolkit for HTTP handling) and Jinja2 (a template engine). Flask itself is the glue that ties them together with a clean API, configuration management, and an extension ecosystem.
Flask vs Django
| Feature | Flask | Django |
|---|---|---|
| Philosophy | Micro-framework, choose your own components | Batteries-included, convention over configuration |
| ORM | None built-in (use SQLAlchemy) | Built-in Django ORM |
| Admin Panel | Flask-Admin (optional) | Built-in, production-ready |
| Auth | Flask-Login (optional) | Built-in user model and auth views |
| Best For | APIs, microservices, learning | Full web apps, CMS, e-commerce |
| Learning Curve | Gentle — small core to learn | Steeper — more conventions to absorb |
Choose Flask when you want to understand every component, you are building an API or microservice, or you need a lightweight foundation. Choose Django when you need an admin panel, built-in auth, and an ORM out of the box for a data-heavy application.
2. Installation & Project Setup
Always use a virtual environment. This keeps your project dependencies isolated from your system Python.
# Create project directory and virtual environment
mkdir myflaskapp && cd myflaskapp
python3 -m venv venv
source venv/bin/activate # Linux/macOS
# venv\Scripts\activate # Windows
# Install Flask
pip install flask
# Verify installation
python -c "import flask; print(flask.__version__)"
Recommended Project Structure
For anything beyond a single file, organize your project like this:
myflaskapp/
app/
__init__.py # Application factory
models.py # Database models
routes/
__init__.py
main.py # Main blueprint
api.py # API blueprint
templates/
base.html
index.html
static/
css/
js/
config.py # Configuration classes
requirements.txt
run.py # Entry point
3. Your First Flask App
A minimal Flask application is five lines of code:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello, Flask!"
if __name__ == "__main__":
app.run(debug=True)
Save this as app.py and run it:
# Method 1: Run directly
python app.py
# Method 2: Use the flask command
export FLASK_APP=app.py
export FLASK_DEBUG=1
flask run
Open http://127.0.0.1:5000 in your browser. The debug=True flag enables the interactive debugger and auto-reloading — the server restarts automatically when you save code changes.
The Application Factory Pattern
For real projects, use a factory function instead of a global app object. This makes testing easier and allows multiple app configurations:
# app/__init__.py
from flask import Flask
def create_app(config_name="development"):
app = Flask(__name__)
app.config.from_object(f"config.{config_name.title()}Config")
# Initialize extensions
from app.extensions import db, migrate
db.init_app(app)
migrate.init_app(app, db)
# Register blueprints
from app.routes.main import main_bp
app.register_blueprint(main_bp)
return app
4. Routing & URL Parameters
Routes map URLs to Python functions. Flask uses decorators to define them.
@app.route("/")
def index():
return "Home page"
# Variable rules: capture parts of the URL
@app.route("/user/<username>")
def profile(username):
return f"Profile: {username}"
# Type converters: int, float, path, uuid
@app.route("/post/<int:post_id>")
def show_post(post_id):
return f"Post #{post_id}"
# Multiple HTTP methods on one route
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
return do_login()
return render_template("login.html")
URL Building with url_for()
Never hardcode URLs. Use url_for() to generate them from function names. This way, if you change a URL pattern, all references update automatically:
from flask import url_for
# In Python code
url_for("index") # "/"
url_for("profile", username="alice") # "/user/alice"
url_for("static", filename="css/style.css") # "/static/css/style.css"
# In Jinja2 templates
# <a href="{{ url_for('index') }}">Home</a>
5. Templates with Jinja2
Jinja2 separates your HTML from your Python logic. Templates live in the templates/ directory by default.
Template Inheritance
Define a base layout and extend it in child templates. This eliminates duplicate HTML:
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My App{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav>
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
</nav>
<main>{% block content %}{% endblock %}</main>
</body>
</html>
<!-- templates/index.html -->
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>Welcome, {{ user.name }}</h1>
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.body | truncate(200) }}</p>
</article>
{% endfor %}
{% endblock %}
Useful Jinja2 Features
{# Filters: transform values inline #}
{{ name | capitalize }}
{{ price | round(2) }}
{{ html_content | safe }} {# mark as safe HTML, skip escaping #}
{{ items | join(", ") }}
{# Conditionals #}
{% if user.is_admin %}
<a href="/admin">Admin Panel</a>
{% elif user.is_authenticated %}
<span>Welcome back</span>
{% else %}
<a href="/login">Log in</a>
{% endif %}
{# Macros: reusable template functions #}
{% macro input_field(name, label, type="text") %}
<label for="{{ name }}">{{ label }}</label>
<input type="{{ type }}" id="{{ name }}" name="{{ name }}">
{% endmacro %}
{{ input_field("email", "Email Address", type="email") }}
Render templates from your route with render_template():
from flask import render_template
@app.route("/")
def index():
posts = Post.query.order_by(Post.created.desc()).limit(10).all()
return render_template("index.html", posts=posts, user=current_user)
6. Forms & Request Handling
Flask gives you raw access to request data through the request object. For production forms, use Flask-WTF which adds CSRF protection and validation.
from flask import request, redirect, url_for, flash
@app.route("/contact", methods=["GET", "POST"])
def contact():
if request.method == "POST":
name = request.form.get("name")
email = request.form.get("email")
message = request.form.get("message")
if not all([name, email, message]):
flash("All fields are required.", "error")
return redirect(url_for("contact"))
# Process the form data
send_message(name, email, message)
flash("Message sent!", "success")
return redirect(url_for("index"))
return render_template("contact.html")
Flask-WTF for Validated Forms
pip install flask-wtf
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email, Length
class ContactForm(FlaskForm):
name = StringField("Name", validators=[DataRequired(), Length(max=100)])
email = StringField("Email", validators=[DataRequired(), Email()])
message = TextAreaField("Message", validators=[DataRequired(), Length(max=2000)])
submit = SubmitField("Send")
@app.route("/contact", methods=["GET", "POST"])
def contact():
form = ContactForm()
if form.validate_on_submit():
send_message(form.name.data, form.email.data, form.message.data)
flash("Message sent!", "success")
return redirect(url_for("index"))
return render_template("contact.html", form=form)
File Uploads
import os
from werkzeug.utils import secure_filename
UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "pdf"}
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 16 MB 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 provided", 400
file = request.files["file"]
if file.filename == "" or not allowed_file(file.filename):
return "Invalid file", 400
filename = secure_filename(file.filename)
file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename))
return redirect(url_for("index"))
7. Database with Flask-SQLAlchemy
Flask-SQLAlchemy integrates SQLAlchemy with Flask's application context, giving you a powerful ORM with session management and connection pooling.
pip install flask-sqlalchemy flask-migrate
Configuration and Models
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from datetime import datetime, timezone
db = SQLAlchemy()
migrate = Migrate()
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)
password_hash = db.Column(db.String(256), nullable=False)
created = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
posts = db.relationship("Post", backref="author", lazy="dynamic")
def __repr__(self):
return f"<User {self.username}>"
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
body = db.Column(db.Text, nullable=False)
created = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
# In your factory function:
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
db.init_app(app)
migrate.init_app(app, db)
CRUD Operations
# CREATE
user = User(username="alice", email="alice@example.com", password_hash=hashed)
db.session.add(user)
db.session.commit()
# READ
user = User.query.filter_by(username="alice").first_or_404()
all_posts = Post.query.order_by(Post.created.desc()).all()
recent = Post.query.filter(Post.created >= last_week).limit(10).all()
# UPDATE
user.email = "newemail@example.com"
db.session.commit()
# DELETE
db.session.delete(user)
db.session.commit()
Migrations with Flask-Migrate
# Initialize migrations (once)
flask db init
# Generate a migration after changing models
flask db migrate -m "add posts table"
# Apply the migration
flask db upgrade
# Rollback one migration
flask db downgrade
8. REST API Development
Flask is one of the most popular choices for building REST APIs. Use jsonify() for responses and request.get_json() to parse incoming data. Format and debug your API responses with the JSON Formatter.
from flask import jsonify, request, abort
@app.route("/api/posts", methods=["GET"])
def get_posts():
page = request.args.get("page", 1, type=int)
per_page = request.args.get("per_page", 20, type=int)
pagination = Post.query.order_by(Post.created.desc()).paginate(
page=page, per_page=min(per_page, 100), error_out=False
)
return jsonify({
"posts": [{"id": p.id, "title": p.title, "body": p.body} for p in pagination.items],
"total": pagination.total,
"page": page,
"pages": pagination.pages
})
@app.route("/api/posts", methods=["POST"])
def create_post():
data = request.get_json()
if not data or not data.get("title") or not data.get("body"):
abort(400, description="Title and body are required.")
post = Post(title=data["title"], body=data["body"], user_id=current_user.id)
db.session.add(post)
db.session.commit()
return jsonify({"id": post.id, "title": post.title}), 201
@app.route("/api/posts/<int:post_id>", methods=["PUT"])
def update_post(post_id):
post = Post.query.get_or_404(post_id)
data = request.get_json()
post.title = data.get("title", post.title)
post.body = data.get("body", post.body)
db.session.commit()
return jsonify({"id": post.id, "title": post.title})
@app.route("/api/posts/<int:post_id>", methods=["DELETE"])
def delete_post(post_id):
post = Post.query.get_or_404(post_id)
db.session.delete(post)
db.session.commit()
return "", 204
Error Handling for APIs
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Not found", "message": str(error)}), 404
@app.errorhandler(400)
def bad_request(error):
return jsonify({"error": "Bad request", "message": str(error)}), 400
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return jsonify({"error": "Internal server error"}), 500
For comprehensive API testing strategies, see our API Testing Complete Guide.
9. Authentication & Sessions
Session-Based Auth with Flask-Login
pip install flask-login flask-bcrypt
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from flask_bcrypt import Bcrypt
login_manager = LoginManager()
bcrypt = Bcrypt()
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(256), nullable=False)
def set_password(self, password):
self.password_hash = bcrypt.generate_password_hash(password).decode("utf-8")
def check_password(self, password):
return bcrypt.check_password_hash(self.password_hash, password)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
user = User.query.filter_by(username=request.form["username"]).first()
if user and user.check_password(request.form["password"]):
login_user(user, remember=request.form.get("remember"))
return redirect(url_for("dashboard"))
flash("Invalid credentials.", "error")
return render_template("login.html")
@app.route("/dashboard")
@login_required
def dashboard():
return render_template("dashboard.html")
JWT for API Authentication
pip install flask-jwt-extended
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
app.config["JWT_SECRET_KEY"] = os.environ.get("JWT_SECRET_KEY")
jwt = JWTManager(app)
@app.route("/api/auth/login", methods=["POST"])
def api_login():
data = request.get_json()
user = User.query.filter_by(username=data.get("username")).first()
if not user or not user.check_password(data.get("password")):
return jsonify({"error": "Invalid credentials"}), 401
token = create_access_token(identity=user.id)
return jsonify({"access_token": token})
@app.route("/api/me")
@jwt_required()
def api_me():
user = User.query.get(get_jwt_identity())
return jsonify({"username": user.username, "email": user.email})
10. Blueprints & App Structure
Blueprints split a large Flask application into modular, reusable components. Each blueprint can have its own routes, templates, and static files.
# app/routes/main.py
from flask import Blueprint, render_template
main_bp = Blueprint("main", __name__, template_folder="templates")
@main_bp.route("/")
def index():
return render_template("index.html")
@main_bp.route("/about")
def about():
return render_template("about.html")
# app/routes/api.py
from flask import Blueprint, jsonify
api_bp = Blueprint("api", __name__, url_prefix="/api")
@api_bp.route("/health")
def health():
return jsonify({"status": "ok"})
# app/__init__.py - register blueprints
def create_app():
app = Flask(__name__)
from app.routes.main import main_bp
from app.routes.api import api_bp
app.register_blueprint(main_bp)
app.register_blueprint(api_bp)
return app
Full Project Layout
myflaskapp/
app/
__init__.py # create_app() factory
extensions.py # db, migrate, login_manager, bcrypt
models/
__init__.py
user.py
post.py
routes/
__init__.py
main.py # Blueprint: pages
api.py # Blueprint: /api/*
auth.py # Blueprint: /auth/*
templates/
base.html
main/
auth/
static/
config.py # DevelopmentConfig, ProductionConfig, TestingConfig
migrations/ # Flask-Migrate auto-generated
tests/
conftest.py
test_auth.py
test_api.py
requirements.txt
Dockerfile
docker-compose.yml
run.py # from app import create_app; app = create_app()
11. Deployment
Never use Flask's development server in production. It is single-threaded, unoptimized, and not designed for security.
Gunicorn (WSGI Server)
pip install gunicorn
# Run with 4 workers
gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"
# With access logging and timeout
gunicorn -w 4 -b 0.0.0.0:8000 --access-logfile - --timeout 120 "app:create_app()"
Docker Deployment
For Docker fundamentals, see our Docker Containers Guide.
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:create_app()"]
# docker-compose.yml
services:
web:
build: .
ports:
- "8000:8000"
environment:
- FLASK_ENV=production
- DATABASE_URL=postgresql://user:pass@db/myapp
- SECRET_KEY=${SECRET_KEY}
depends_on:
- db
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
pgdata:
Environment Configuration
# config.py
import os
class Config:
SECRET_KEY = os.environ.get("SECRET_KEY", "change-this-in-production")
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = "sqlite:///dev.db"
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
12. Best Practices & Common Pitfalls
Do
- Use the application factory pattern — it makes testing and multiple configurations straightforward.
- Store secrets in environment variables, never in source code. Use
python-dotenvfor local development. - Use Flask-Migrate for every schema change. Never modify the database schema manually.
- Add CSRF protection to all forms with Flask-WTF.
- Set
MAX_CONTENT_LENGTHto prevent denial-of-service via large uploads. - Use
url_for()instead of hardcoding URLs in templates and redirects. - Write tests — Flask provides a test client that simulates requests without a running server.
Avoid
- Running the dev server in production — always use Gunicorn or uWSGI behind a reverse proxy.
- Storing passwords in plain text — always hash with bcrypt or argon2.
- Circular imports — use the factory pattern and import inside functions when necessary.
- Using
| safeon user input — this disables Jinja2's auto-escaping and opens you to XSS attacks. - Committing
.envfiles or secrets to version control. - Ignoring database connection cleanup — Flask-SQLAlchemy handles this, but raw connections need
teardown_appcontext.
Testing Example
import pytest
from app import create_app
@pytest.fixture
def client():
app = create_app("testing")
with app.test_client() as client:
with app.app_context():
db.create_all()
yield client
def test_home_page(client):
response = client.get("/")
assert response.status_code == 200
def test_create_post(client):
response = client.post("/api/posts",
json={"title": "Test Post", "body": "Content here"},
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 201
assert response.get_json()["title"] == "Test Post"
Frequently Asked Questions
What is the difference between Flask and Django?
Flask is a micro-framework that gives you routing, a template engine (Jinja2), and a development server, then lets you choose every other component yourself. Django is a batteries-included framework with a built-in ORM, admin panel, authentication system, and form handling. Choose Flask when you want full control over your stack, are building a lightweight API or microservice, or prefer to pick your own database library and auth solution. Choose Django when you need a data-driven application with an admin interface, complex models, and built-in user management. Flask has a smaller learning curve and is easier to understand from top to bottom, while Django is faster for large applications because it makes more decisions for you.
How do I deploy a Flask application to production?
Never use Flask's built-in development server in production. Instead, use a production WSGI server like Gunicorn: run gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()" with 2–4 workers per CPU core. Place a reverse proxy like Nginx in front of Gunicorn to handle static files, SSL termination, and load balancing. Store secrets in environment variables, never in code. For containerized deployments, create a Dockerfile with a slim Python base image, install dependencies from requirements.txt, and use Docker Compose to orchestrate the app with its database and cache services.
How do I connect Flask to a database?
The most common approach is Flask-SQLAlchemy, which integrates SQLAlchemy's ORM with Flask's application context. Install it with pip install flask-sqlalchemy, configure the SQLALCHEMY_DATABASE_URI (e.g., sqlite:///app.db for development or a PostgreSQL URI for production), and define models as Python classes that inherit from db.Model. Use Flask-Migrate for schema migrations: flask db init to set up, flask db migrate to generate scripts, and flask db upgrade to apply them.
How do I build a REST API with Flask?
Flask is excellent for REST APIs. Use jsonify() to return JSON responses, request.get_json() to parse incoming JSON, and HTTP method decorators to define endpoints. Structure your API with Blueprints to organize related routes into modules. For input validation, use marshmallow or pydantic. Handle errors with @app.errorhandler decorators that return JSON instead of HTML. Add CORS support with Flask-CORS. For larger APIs, consider Flask-RESTX for Swagger documentation or Flask-Smorest for OpenAPI integration.
How do I handle authentication in Flask?
Flask does not include authentication out of the box, but Flask-Login is the standard extension for session-based auth. It manages user sessions, provides a login_required decorator, and handles remember-me cookies. Hash passwords with bcrypt via Flask-Bcrypt — never store them in plain text. For API authentication, use JSON Web Tokens with Flask-JWT-Extended, which provides access tokens, refresh tokens, and token revocation. For OAuth (Google, GitHub login), use Authlib or Flask-Dance. Always use HTTPS in production and enable CSRF protection with Flask-WTF for forms.
Conclusion
Flask gives you a clean foundation and the freedom to build exactly the application you need. Start with a single file to learn the fundamentals, then graduate to the application factory pattern with Blueprints when your project grows. Use Flask-SQLAlchemy for database access, Flask-Login or Flask-JWT-Extended for authentication, and Gunicorn behind Nginx or in a Docker container for production deployment.
The micro-framework philosophy means you will understand every piece of your stack. That understanding pays off when debugging production issues, optimizing performance, or onboarding new team members. Flask's ecosystem is mature, its documentation is excellent, and its patterns scale from weekend projects to production services handling millions of requests.