Flask Web Framework: Complete Guide for 2026

February 12, 202622 min read

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.

⚙ Related resources: Debug API responses with the JSON Formatter, learn API testing in our API Testing Guide, containerize your app with the Docker Guide, and reference our Python String Methods Cheat Sheet.

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
PhilosophyMicro-framework, choose your own componentsBatteries-included, convention over configuration
ORMNone built-in (use SQLAlchemy)Built-in Django ORM
Admin PanelFlask-Admin (optional)Built-in, production-ready
AuthFlask-Login (optional)Built-in user model and auth views
Best ForAPIs, microservices, learningFull web apps, CMS, e-commerce
Learning CurveGentle — small core to learnSteeper — 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

Avoid

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.

⚙ Essential tools: Debug API responses with the JSON Formatter, learn testing strategies in our API Testing Guide, and keep our Python String Methods Cheat Sheet bookmarked.

Related Resources

API Testing Complete Guide
Test your Flask API endpoints effectively
Docker Containers Guide
Containerize Flask apps for consistent deployments
Django Complete Guide
Compare Flask with Django's batteries-included approach
JSON Formatter
Format and validate API responses from Flask endpoints
Python String Methods
Quick reference for string manipulation in Python
REST API Design Guide
Best practices for designing RESTful API endpoints