flask について
Flaskアプリを開発する Python 開発者向けに、アプリケーションファクトリー、Blueprint、Flask-SQLAlchemy の実践パターンを紹介します。認証、API、セキュリティ設定、バージョン関連の注意点も扱います。
主な機能
- 01アプリケーションファクトリーとプロジェクト構成
- 02Blueprint によるルート整理
- 03Flask-SQLAlchemy のデータモデル
- 04Flask-Login 認証と Flask-WTF フォーム
- 05JSON API のルートパターン
- 06一般的なエラーとバージョン問題の予防
活用シーン
- 01テストしやすく保守可能な Flask プロジェクトを構築
- 02アプリを複数の Blueprint に分割
- 03データモデルとユーザー認証を実装
- 04コンテキスト、セッション、依存関係の競合を調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill flaskインストール後、Claude Code で/flaskを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsontemplates/.env.exampleconfig.pypyproject.tomlrun.pytemplates/app/__init__.pyextensions.pymodels.pytemplates/app/auth/__init__.pyforms.pyroutes.pytemplates/app/main/__init__.pyroutes.pytemplates/tests/__init__.pyconftest.pytest_auth.pytest_main.py詳細
Flask スキル
アプリケーションファクトリーパターン、Blueprint、Flask-SQLAlchemy を使った、本番環境で検証済みのパターンです。
最新バージョン(2026年1月確認):
- Flask:3.1.2
- Flask-SQLAlchemy:3.1.1
- Flask-Login:0.6.3
- Flask-WTF:1.2.2
- Werkzeug:3.1.5
- Python:3.9 以降が必須(Flask 3.1.0 で 3.8 のサポートが終了)
クイックスタート
uv でプロジェクトをセットアップ
# Create project
uv init my-flask-app
cd my-flask-app
# Add dependencies
uv add flask flask-sqlalchemy flask-login flask-wtf python-dotenv
# Run development server
uv run flask --app app run --debug
最小限の実行可能な例
# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return {"message": "Hello, World!"}
if __name__ == "__main__":
app.run(debug=True)
実行:uv run flask --app app run --debug
既知の問題の予防
このスキルでは、文書化されている 9 件の問題を予防します。
問題 #1:stream_with_context のティアダウンに関するリグレッション(Flask 3.1.2)
エラー:stream_with_context 使用時、ティアダウン関数で KeyError が発生する
出典:GitHub Issue #5804
発生する理由:Flask 3.1.2 ではリグレッションが導入され、レスポンスの生成完了前に stream_with_context が teardown_request() を複数回呼び出します。ティアダウンコールバックでデフォルト値を指定せずに g.pop(key) を使うと、2 回目の呼び出しで失敗します。
予防策:
# WRONG - fails on second teardown call
@app.teardown_request
def _teardown_request(_):
g.pop("hello") # KeyError on second call
# RIGHT - idempotent teardown
@app.teardown_request
def _teardown_request(_):
g.pop("hello", None) # Provide default value
状況:PR #5812 の副次的な効果として、Flask 3.2.0 で修正される予定です。それまでは、すべてのティアダウンコールバックが冪等であることを確認してください。
問題 #2:非同期ビューと gevent の非互換性
エラー:gevent で同時に非同期リクエストを処理すると RuntimeError が発生する
出典:GitHub Issue #5881
発生する理由:gevent によるモンキーパッチが有効な場合、Asgiref は失敗します。asyncio は OS スレッドごとに 1 つのイベントループを想定していますが、gevent のモンキーパッチにより threading.Thread は実際のスレッドではなくグリーンスレッドを作成するため、両方のループが同じ物理スレッド上で実行され、互いにブロックします。
予防策:非同期処理(asyncio/uvloop)か gevent のどちらか一方を選び、併用しないでください。両方を併用する必要がある場合:
import asyncio
import gevent.monkey
import gevent.selectors
from flask import Flask
gevent.monkey.patch_all()
loop = asyncio.EventLoop(gevent.selectors.DefaultSelector())
gevent.spawn(loop.run_forever)
class GeventFlask(Flask):
def async_to_sync(self, func):
def run(*args, **kwargs):
coro = func(*args, **kwargs)
future = asyncio.run_coroutine_threadsafe(coro, loop)
return future.result()
return run
app = GeventFlask(__name__)
注:これは「どちらの目的も完全に損なう」方法です(メンテナーのコメント)。個別の非同期リクエストは機能しますが、この回避策がないと同時リクエストは失敗します。
問題 #3:リダイレクト後にテストクライアントのセッションが更新されない
エラー:テストで follow_redirects=True を使用した後、セッションの状態が正しくない
出典:GitHub Issue #5786
発生する理由:Flask < 3.1.2 では、リダイレクトをたどった後にテストクライアントのセッションが正しく更新されませんでした。
予防策:
# If using Flask >= 3.1.2, follow_redirects works correctly
def test_login_redirect(client):
response = client.post('/login',
data={'email': '[email protected]', 'password': 'pass'},
follow_redirects=True)
assert 'user_id' in session # Works in 3.1.2+
# For Flask < 3.1.2, make separate requests
response = client.post('/login', data={...})
assert response.status_code == 302
response = client.get(response.location) # Explicit redirect follow
状況:Flask 3.1.2 で修正済みです。最新バージョンにアップグレードしてください。
問題 #4:スレッド内でアプリケーションコンテキストが失われる(コミュニティ提供)
エラー:バックグラウンドスレッドで RuntimeError: Working outside of application context が発生する
出典:Sentry.io Guide
発生する理由:新しいスレッドに current_app を渡す場合は、_get_current_object() を使ってプロキシオブジェクトをアンラップし、そのスレッド内でアプリケーションコンテキストをプッシュする必要があります。
予防策:
from flask import current_app
import threading
# WRONG - current_app is a proxy, loses context in thread
def background_task():
app_name = current_app.name # Fails!
@app.route('/start')
def start_task():
thread = threading.Thread(target=background_task)
thread.start()
# RIGHT - unwrap proxy and push context
def background_task(app):
with app.app_context():
app_name = app.name # Works!
@app.route('/start')
def start_task():
app = current_app._get_current_object()
thread = threading.Thread(target=background_task, args=(app,))
thread.start()
確認済み:本番アプリケーションで一般的なパターンであり、Flask の公式ドキュメントにも記載されています。
問題 #5:Flask-Login のセッション保護による予期しないログアウト(コミュニティ提供)
エラー:IP アドレスの変更時にユーザーが予期せずログアウトする 出典:Flask-Login Docs 発生する理由:Flask-Login の「strong」セッション保護モードでは、IP アドレスなどのセッション識別子が変更されると、セッション全体が削除されます。モバイルネットワークや VPN を使用するユーザーに影響します。
予防策:
# app/extensions.py
from flask_login import LoginManager
login_manager = LoginManager()
login_manager.session_protection = "basic" # Default, less strict
# login_manager.session_protection = "strong" # Strict, may logout on IP change
# login_manager.session_protection = None # Disabled (not recommended)
注:デフォルトでは、Flask-Login は同時セッション(同じユーザーが複数のブラウザーで利用すること)を許可します。これを防ぐには、カスタムのセッショントラッキングを実装してください。
確認済み:Flask-Login の公式ドキュメント、および 2024 年公開の複数のブログ記事。
課題 #6:CSRF 保護によるキャッシュの干渉(コミュニティからの報告)
エラー:キャッシュされたページでフォームを送信すると、「CSRF token missing/invalid」が発生します
出典:Flask-WTF ドキュメント
原因:ウェブサーバーのキャッシュポリシーでページが WTF_CSRF_TIME_LIMIT より長くキャッシュされると、ブラウザーは有効期限切れの CSRF トークンを含むキャッシュ済みページを表示します。
予防策:
# Option 1: Align cache duration with token lifetime
WTF_CSRF_TIME_LIMIT = None # Never expire (less secure)
# Option 2: Exclude forms from cache
@app.after_request
def add_cache_headers(response):
if request.method == 'GET' and 'form' in request.endpoint:
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
return response
# Option 3: Configure webserver to not cache POST targets
# In Nginx: add "proxy_cache_bypass $cookie_session" for form routes
確認済み:Flask-WTF の公式ドキュメントにある警告、および 2024 年のセキュリティベストプラクティスガイド。
課題 #7:リクエストごとの max_content_length のオーバーライド(新機能)
機能:Flask 3.1.0 では、Request.max_content_length をリクエストごとにカスタマイズできるようになりました
出典:Flask 3.1.0 リリースノート
使用方法:
from flask import Flask, request
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB default
@app.route('/upload', methods=['POST'])
def upload():
# Override for this specific route
request.max_content_length = 100 * 1024 * 1024 # 100MB for uploads
file = request.files['file']
# ...
注:3.1.0 では MAX_FORM_MEMORY_SIZE と MAX_FORM_PARTS の設定オプションも追加されました。セキュリティドキュメントを参照してください。
課題 #8:SECRET_KEY のローテーション(新機能)
機能:Flask 3.1.0 では、キーのローテーションに使用する SECRET_KEY_FALLBACKS が追加されました
出典:Flask 3.1.0 リリースノート
使用方法:
# config.py
class Config:
SECRET_KEY = "new-secret-key-2024"
SECRET_KEY_FALLBACKS = [
"old-secret-key-2023",
"older-secret-key-2022"
]
注:この機能を使用するには、拡張機能側で明示的なサポートが必要です。フォールバックキーを使用するには、Flask-Login と Flask-WTF の更新が必要になる場合があります。
課題 #9:Werkzeug 3.1 以降との依存関係の競合
エラー:flask==2.2.4 incompatible with werkzeug==3.1.3
出典:Flask 3.1.0 リリースノート | GitHub Issue #5652
原因:Flask 3.1.0 では、依存関係の最小バージョンが更新され、Werkzeug >= 3.1、ItsDangerous >= 2.2、Blinker >= 1.9 になりました。古いバージョンに固定されているプロジェクトでは、競合が発生します。
予防策:
# Update all Pallets projects together
pip install flask>=3.1.0 werkzeug>=3.1.0 itsdangerous>=2.2.0 blinker>=1.9.0
# Or with uv
uv add "flask>=3.1.0" "werkzeug>=3.1.0" "itsdangerous>=2.2.0" "blinker>=1.9.0"
プロジェクト構成(アプリケーションファクトリー)
保守しやすいアプリケーションを作成するには、ブループリントとともにファクトリーパターンを使用します。
my-flask-app/
├── pyproject.toml
├── config.py # Configuration classes
├── run.py # Entry point
│
├── app/
│ ├── __init__.py # Application factory (create_app)
│ ├── extensions.py # Flask extensions (db, login_manager)
│ ├── models.py # SQLAlchemy models
│ │
│ ├── main/ # Main blueprint
│ │ ├── __init__.py
│ │ └── routes.py
│ │
│ ├── auth/ # Auth blueprint
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ └── forms.py
│ │
│ ├── templates/
│ │ ├── base.html
│ │ ├── main/
│ │ └── auth/
│ │
│ └── static/
│ ├── css/
│ └── js/
│
└── tests/
├── conftest.py
└── test_main.py
基本パターン
アプリケーションファクトリー
# app/__init__.py
from flask import Flask
from app.extensions import db, login_manager
from config import Config
def create_app(config_class=Config):
"""Application factory function."""
app = Flask(__name__)
app.config.from_object(config_class)
# Initialize extensions
db.init_app(app)
login_manager.init_app(app)
# Register blueprints
from app.main import bp as main_bp
from app.auth import bp as auth_bp
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp, url_prefix="/auth")
# Create database tables
with app.app_context():
db.create_all()
return app
主な利点:
- 設定の異なる複数のアプリインスタンスを作成できます(テスト)
- 循環インポートを回避できます
- 拡張機能は一度だけ初期化し、後からアプリに関連付けます
拡張機能モジュール
# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
db = SQLAlchemy()
login_manager = LoginManager()
login_manager.login_view = "auth.login"
login_manager.login_message_category = "info"
ファイルを分ける理由:循環インポートを防ぎます。モデルは app をインポートせずに db をインポートできます。
設定
# config.py
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
"""Base configuration."""
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key")
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///app.db")
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
"""Development configuration."""
DEBUG = True
class TestingConfig(Config):
"""Testing configuration."""
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
WTF_CSRF_ENABLED = False
class ProductionConfig(Config):
"""Production configuration."""
DEBUG = False
エントリーポイント
# run.py
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run()
実行:flask --app run run --debug
ブループリント
ブループリントの作成
# app/main/__init__.py
from flask import Blueprint
bp = Blueprint("main", __name__)
from app.main import routes # Import routes after bp is created!
# app/main/routes.py
from flask import render_template, jsonify
from app.main import bp
@bp.route("/")
def index():
return render_template("main/index.html")
@bp.route("/api/health")
def health():
return jsonify({"status": "ok"})
テンプレートを使用するブループリント
# app/auth/__init__.py
from flask import Blueprint
bp = Blueprint(
"auth",
__name__,
template_folder="templates", # Blueprint-specific templates
static_folder="static", # Blueprint-specific static files
)
from app.auth import routes
データベースモデル
# app/models.py
from datetime import datetime
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app.extensions import db, login_manager
class User(UserMixin, db.Model):
"""User model for authentication."""
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(256), nullable=False)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f"<User {self.email}>"
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
Flask-Login による認証
認証フォーム
# app/auth/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from app.models import User
class LoginForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
password = PasswordField("Password", validators=[DataRequired()])
remember = BooleanField("Remember Me")
submit = SubmitField("Login")
class RegistrationForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
password = PasswordField("Password", validators=[DataRequired(), Length(min=8)])
confirm = PasswordField("Confirm Password", validators=[
DataRequired(), EqualTo("password", message="Passwords must match")
])
submit = SubmitField("Register")
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError("Email already registered.")
認証ルート
# app/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm
from app.extensions import db
from app.models import User
@bp.route("/register", methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
return redirect(url_for("main.index"))
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash("Registration successful! Please log in.", "success")
return redirect(url_for("auth.login"))
return render_template("auth/register.html", form=form)
@bp.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return redirect(url_for("main.index"))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and user.check_password(form.password.data):
login_user(user, remember=form.remember.data)
next_page = request.args.get("next")
flash("Logged in successfully!", "success")
return redirect(next_page or url_for("main.index"))
flash("Invalid email or password.", "danger")
return render_template("auth/login.html", form=form)
@bp.route("/logout")
@login_required
def logout():
logout_user()
flash("You have been logged out.", "info")
return redirect(url_for("main.index"))
ルートの保護
from flask_login import login_required, current_user
@bp.route("/dashboard")
@login_required
def dashboard():
return render_template("main/dashboard.html", user=current_user)
API ルート(JSON)
テンプレートを使用しない REST API の場合:
# app/api/__init__.py
from flask import Blueprint
bp = Blueprint("api", __name__)
from app.api import routes
# app/api/routes.py
from flask import jsonify, request
from flask_login import login_required, current_user
from app.api import bp
from app.extensions import db
from app.models import User
@bp.route("/users", methods=["GET"])
@login_required
def get_users():
users = User.query.all()
return jsonify([
{"id": u.id, "email": u.email}
for u in users
])
@bp.route("/users", methods=["POST"])
def create_user():
data = request.get_json()
if not data or "email" not in data or "password" not in data:
return jsonify({"error": "Missing required fields"}), 400
if User.query.filter_by(email=data["email"]).first():
return jsonify({"error": "Email already exists"}), 409
user = User(email=data["email"])
user.set_password(data["password"])
db.session.add(user)
db.session.commit()
return jsonify({"id": user.id, "email": user.email}), 201
プレフィックスを指定して登録します:
app.register_blueprint(api_bp, url_prefix="/api/v1")
重要なルール
必ず行うこと
- アプリケーションファクトリーパターンを使用する - テストが容易になり、グローバル変数を避けられます
- 拡張機能を別ファイルに配置する - 循環インポートを防ぎます
- ブループリントの
__init__.pyの末尾でルートをインポートする -bpの作成後に行います appではなくcurrent_appを使用する - リクエストコンテキスト内で使用しますwith app.app_context()を使用する - リクエスト外で db にアクセスする場合に使用します
絶対に行わないこと
- モデルで
appを絶対にインポートしない - 循環インポートの原因になります - アプリケーションコンテキストの外で
dbに絶対にアクセスしない - RuntimeError が発生します - シークレットをコードに絶対に保存しない - 環境変数を使用します
- 本番環境で
app.run()を絶対に使用しない - Gunicorn を使用します - CSRF 保護を絶対に無効にしない - Flask-WTF を有効にしておきます
よくあるエラーと解決方法
循環インポートエラー
エラー:ImportError: cannot import name 'X' from partially initialized module
原因:モデルが app をインポートし、app がモデルをインポートしています
解決方法:extensions.py パターンを使用します:
# WRONG - circular import
# app/__init__.py
from app.models import User # models.py imports db from here!
# RIGHT - deferred import
# app/__init__.py
def create_app():
# ... setup ...
from app.models import User # Import inside factory
アプリケーションコンテキスト外での操作
エラー:RuntimeError: Working outside of application context
原因:リクエストの外で current_app、g、または db にアクセスしています
解決方法:
# WRONG
from app import create_app
app = create_app()
users = User.query.all() # No context!
# RIGHT
from app import create_app
app = create_app()
with app.app_context():
users = User.query.all() # Has context
Blueprint が見つかりません
エラー:werkzeug.routing.BuildError: Could not build url for endpoint
原因:url_for() で誤った Blueprint プレフィックスを使用しています
修正:
# WRONG
url_for("login")
# RIGHT - include blueprint name
url_for("auth.login")
CSRF トークンがありません
エラー:Bad Request: The CSRF token is missing
原因:CSRF トークンを含めずにフォームを送信しています
修正:テンプレートにトークンを含めます:
<form method="post">
{{ form.hidden_tag() }}
</form>
テスト
# tests/conftest.py
import pytest
from app import create_app
from app.extensions import db
from config import TestingConfig
@pytest.fixture
def app():
app = create_app(TestingConfig)
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def runner(app):
return app.test_cli_runner()
# tests/test_main.py
def test_index(client):
response = client.get("/")
assert response.status_code == 200
def test_register(client):
response = client.post("/auth/register", data={
"email": "[email protected]",
"password": "testpass123",
"confirm": "testpass123",
}, follow_redirects=True)
assert response.status_code == 200
実行:uv run pytest
デプロイ
開発
flask --app run run --debug
Gunicorn を使用した本番環境
uv add gunicorn
uv run gunicorn -w 4 -b 0.0.0.0:8000 "run:app"
Docker
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install uv && uv sync
EXPOSE 8000
CMD ["uv", "run", "gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "run:app"]
環境変数(.env)
SECRET_KEY=your-production-secret-key
DATABASE_URL=postgresql://user:pass@localhost/dbname
FLASK_ENV=production
参考資料
最終確認日:2026-01-21 | スキルバージョン:2.0.0 | 変更内容:既知の問題を9件追加(stream_with_context のリグレッション、async/gevent の競合、テストクライアントのセッション、スレッドのコンテキスト、Flask-Login のセッション保護、CSRF キャッシュ、新しい 3.1.0 の機能、Werkzeug の依存関係) メンテナー:Jezweb | [email protected]