Skip to main content
Technology & EngineeringPython Web314 lines

Flask

Flask application patterns for routing, blueprints, extensions, and application factories

Quick Summary31 lines
You are an expert in Flask for building web applications with a lightweight, composable architecture.

## Key Points

- Always use the application factory pattern. It enables testing with different configs and avoids circular imports.
- Organize code into blueprints by domain area. Each blueprint gets its own templates, static files, and routes.
- Use Flask-Migrate (Alembic) for all schema changes instead of `db.create_all()` in production.
- Store secrets in environment variables and load them through `app.config.from_prefixed_env()` or a dedicated config class.
- Use `flask.g` for per-request state (like database connections) and `flask.session` for user session data.
- Register error handlers at the application level with `app_errorhandler` so they apply across all blueprints.
- Working outside the application context. Accessing `db.session`, `current_app`, or `url_for` outside a request or CLI command raises a `RuntimeError`. Wrap such code in `with app.app_context():`.
- Circular imports between `__init__.py` and route modules. Fix this by importing routes at the bottom of `__init__.py` after the blueprint is created.
- Using `db.create_all()` in production instead of migrations. This cannot handle schema changes on existing tables.
- Forgetting to call `db.session.rollback()` in error handlers, leaving the session in a broken state for the next request.
- Not setting `SECRET_KEY` to a strong random value in production, which compromises session cookie security.

## Quick Example

```bash
pip install flask flask-sqlalchemy flask-migrate flask-login flask-wtf
```

```python
# app/api/__init__.py
from flask import Blueprint
bp = Blueprint("api", __name__)

from . import routes  # noqa
```
skilldb get python-web-skills/FlaskFull skill: 314 lines
Paste into your CLAUDE.md or agent config

Flask — Python Web Development

You are an expert in Flask for building web applications with a lightweight, composable architecture.

Core Philosophy

Overview

Flask is a micro-framework for Python that provides routing, request handling, templating with Jinja2, and a robust extension ecosystem. Its minimal core gives developers full control over project structure, making it well suited for small services and large applications alike.

Setup & Configuration

pip install flask flask-sqlalchemy flask-migrate flask-login flask-wtf

Application factory pattern:

# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager

db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()


def create_app(config_name="default"):
    app = Flask(__name__)
    app.config.from_object(config[config_name])

    db.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)
    login_manager.login_view = "auth.login"

    from .main import bp as main_bp
    app.register_blueprint(main_bp)

    from .api import bp as api_bp
    app.register_blueprint(api_bp, url_prefix="/api")

    return app
# config.py
import os


class Config:
    SECRET_KEY = os.environ.get("SECRET_KEY", "change-me")
    SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///app.db")
    SQLALCHEMY_TRACK_MODIFICATIONS = False


class DevelopmentConfig(Config):
    DEBUG = True


class ProductionConfig(Config):
    DEBUG = False


config = {
    "default": DevelopmentConfig,
    "production": ProductionConfig,
}

Core Patterns

Blueprints

# app/main/__init__.py
from flask import Blueprint

bp = Blueprint("main", __name__, template_folder="templates")

from . import routes  # noqa: E402, F401
# app/main/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from . import bp
from .. import db
from ..models import Article


@bp.route("/")
def index():
    page = request.args.get("page", 1, type=int)
    articles = (
        Article.query
        .filter_by(published=True)
        .order_by(Article.created_at.desc())
        .paginate(page=page, per_page=20)
    )
    return render_template("index.html", articles=articles)


@bp.route("/article/<slug>")
def article_detail(slug):
    article = Article.query.filter_by(slug=slug).first_or_404()
    return render_template("article.html", article=article)


@bp.route("/article/new", methods=["GET", "POST"])
@login_required
def create_article():
    form = ArticleForm()
    if form.validate_on_submit():
        article = Article(
            title=form.title.data,
            body=form.body.data,
            author=current_user,
        )
        db.session.add(article)
        db.session.commit()
        flash("Article created.", "success")
        return redirect(url_for("main.article_detail", slug=article.slug))
    return render_template("create_article.html", form=form)

Models with Flask-SQLAlchemy

# app/models.py
from datetime import datetime, timezone
from flask_login import UserMixin
from slugify import slugify
from . import db, login_manager


class User(UserMixin, 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)
    articles = db.relationship("Article", backref="author", lazy="dynamic")


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))


class Article(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    slug = db.Column(db.String(200), unique=True, nullable=False)
    body = db.Column(db.Text, nullable=False)
    published = db.Column(db.Boolean, default=False)
    author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
    created_at = db.Column(
        db.DateTime, default=lambda: datetime.now(timezone.utc)
    )

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if not self.slug:
            self.slug = slugify(self.title)

Error Handlers

@bp.app_errorhandler(404)
def not_found(error):
    return render_template("errors/404.html"), 404


@bp.app_errorhandler(500)
def internal_error(error):
    db.session.rollback()
    return render_template("errors/500.html"), 500

JSON API Endpoints

# app/api/__init__.py
from flask import Blueprint
bp = Blueprint("api", __name__)

from . import routes  # noqa
# app/api/routes.py
from flask import jsonify, request, abort
from . import bp
from ..models import Article
from .. import db


@bp.route("/articles")
def list_articles():
    page = request.args.get("page", 1, type=int)
    pagination = Article.query.filter_by(published=True).paginate(
        page=page, per_page=20
    )
    return jsonify({
        "items": [a.to_dict() for a in pagination.items],
        "total": pagination.total,
        "page": page,
        "pages": pagination.pages,
    })


@bp.route("/articles", methods=["POST"])
def create_article():
    data = request.get_json()
    if not data or "title" not in data:
        abort(400)
    article = Article(title=data["title"], body=data.get("body", ""))
    db.session.add(article)
    db.session.commit()
    return jsonify(article.to_dict()), 201

Context Processors and Template Helpers

@bp.app_context_processor
def inject_globals():
    return {
        "site_name": "My Blog",
        "current_year": datetime.now().year,
    }


@bp.app_template_filter("timeago")
def timeago_filter(dt):
    delta = datetime.now(timezone.utc) - dt
    if delta.days > 0:
        return f"{delta.days}d ago"
    hours = delta.seconds // 3600
    return f"{hours}h ago"

Testing

import pytest
from app import create_app, db as _db


@pytest.fixture
def app():
    app = create_app("testing")
    with app.app_context():
        _db.create_all()
        yield app
        _db.drop_all()


@pytest.fixture
def client(app):
    return app.test_client()


def test_index(client):
    response = client.get("/")
    assert response.status_code == 200


def test_create_article(client):
    response = client.post(
        "/api/articles",
        json={"title": "Test", "body": "Content"},
    )
    assert response.status_code == 201

Best Practices

  • Always use the application factory pattern. It enables testing with different configs and avoids circular imports.
  • Organize code into blueprints by domain area. Each blueprint gets its own templates, static files, and routes.
  • Use Flask-Migrate (Alembic) for all schema changes instead of db.create_all() in production.
  • Store secrets in environment variables and load them through app.config.from_prefixed_env() or a dedicated config class.
  • Use flask.g for per-request state (like database connections) and flask.session for user session data.
  • Register error handlers at the application level with app_errorhandler so they apply across all blueprints.

Common Pitfalls

  • Working outside the application context. Accessing db.session, current_app, or url_for outside a request or CLI command raises a RuntimeError. Wrap such code in with app.app_context():.
  • Circular imports between __init__.py and route modules. Fix this by importing routes at the bottom of __init__.py after the blueprint is created.
  • Using db.create_all() in production instead of migrations. This cannot handle schema changes on existing tables.
  • Forgetting to call db.session.rollback() in error handlers, leaving the session in a broken state for the next request.
  • Not setting SECRET_KEY to a strong random value in production, which compromises session cookie security.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add python-web-skills

Get CLI access →