npx skills add https://github.com/jezweb/claude-skills --skill flask经过生产环境测试的 Flask 模式,包含应用工厂模式、蓝图和 Flask-SQLAlchemy。
最新版本(2026年1月验证):
# 创建项目
uv init my-flask-app
cd my-flask-app
# 添加依赖
uv add flask flask-sqlalchemy flask-login flask-wtf python-dotenv
# 运行开发服务器
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 个已记录的问题:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
错误:使用 stream_with_context 时,清理函数中出现 KeyError
来源:GitHub Issue #5804
原因:Flask 3.1.2 引入了一个回归问题,stream_with_context 会在响应生成完成前多次触发 teardown_request() 调用。如果清理回调函数使用 g.pop(key) 而不提供默认值,则会在第二次调用时失败。
预防方法:
# 错误 - 第二次清理调用时失败
@app.teardown_request
def _teardown_request(_):
g.pop("hello") # 第二次调用时出现 KeyError
# 正确 - 幂等清理
@app.teardown_request
def _teardown_request(_):
g.pop("hello", None) # 提供默认值
状态:将在 Flask 3.2.0 中作为 PR #5812 的副作用修复。在此之前,请确保所有清理回调都是幂等的。
错误:使用 gevent 处理并发异步请求时出现 RuntimeError
来源:GitHub Issue #5881
原因:当 gevent 猴子补丁激活时,Asgiref 会失败。Asyncio 期望每个操作系统线程有一个事件循环,但 gevent 的猴子补丁使 threading.Thread 创建 greenlet 而不是真正的线程,导致两个循环在同一物理线程上运行并相互阻塞。
预防方法:选择异步(使用 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__)
注意:这"完全违背了两者的初衷"(维护者评论)。单独的异步请求可以工作,但没有此变通方法,并发请求会失败。
错误:测试中使用 follow_redirects=True 后会话状态不正确
来源:GitHub Issue #5786
原因:在 Flask < 3.1.2 中,测试客户端的会话在跟随重定向后没有正确更新。
预防方法:
# 如果使用 Flask >= 3.1.2,follow_redirects 正常工作
def test_login_redirect(client):
response = client.post('/login',
data={'email': 'test@example.com', 'password': 'pass'},
follow_redirects=True)
assert 'user_id' in session # 在 3.1.2+ 中有效
# 对于 Flask < 3.1.2,发出单独的请求
response = client.post('/login', data={...})
assert response.status_code == 302
response = client.get(response.location) # 显式跟随重定向
状态:已在 Flask 3.1.2 中修复。升级到最新版本。
错误:后台线程中出现 RuntimeError: Working outside of application context
来源:Sentry.io 指南
原因:将 current_app 传递给新线程时,必须使用 _get_current_object() 解包代理对象,并在线程中推送应用上下文。
预防方法:
from flask import current_app
import threading
# 错误 - current_app 是代理,在线程中丢失上下文
def background_task():
app_name = current_app.name # 失败!
@app.route('/start')
def start_task():
thread = threading.Thread(target=background_task)
thread.start()
# 正确 - 解包代理并推送上下文
def background_task(app):
with app.app_context():
app_name = app.name # 有效!
@app.route('/start')
def start_task():
app = current_app._get_current_object()
thread = threading.Thread(target=background_task, args=(app,))
thread.start()
已验证:生产应用中的常见模式,在官方 Flask 文档中有记录。
错误:IP 地址更改时用户意外注销 来源:Flask-Login 文档 原因:Flask-Login 的"强"会话保护模式会在会话标识符(如 IP 地址)更改时删除整个会话。这会影响使用移动网络或 VPN 的用户。
预防方法:
# app/extensions.py
from flask_login import LoginManager
login_manager = LoginManager()
login_manager.session_protection = "basic" # 默认,限制较少
# login_manager.session_protection = "strong" # 严格,IP 更改时可能注销
# login_manager.session_protection = None # 禁用(不推荐)
注意:默认情况下,Flask-Login 允许并发会话(同一用户在多个浏览器上)。要防止这种情况,请实现自定义会话跟踪。
已验证:官方 Flask-Login 文档,多篇 2024 年博客文章。
错误:缓存页面上的表单提交失败,提示"CSRF token missing/invalid"
来源:Flask-WTF 文档
原因:如果 Web 服务器缓存策略缓存页面的时间超过 WTF_CSRF_TIME_LIMIT,浏览器会提供带有过期 CSRF 令牌的缓存页面。
预防方法:
# 选项 1:将缓存持续时间与令牌生命周期对齐
WTF_CSRF_TIME_LIMIT = None # 永不过期(安全性较低)
# 选项 2:从缓存中排除表单
@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
# 选项 3:配置 Web 服务器不缓存 POST 目标
# 在 Nginx 中:为表单路由添加 "proxy_cache_bypass $cookie_session"
已验证:官方 Flask-WTF 文档警告,2024 年安全最佳实践指南。
功能: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 默认值
@app.route('/upload', methods=['POST'])
def upload():
# 为此特定路由覆盖
request.max_content_length = 100 * 1024 * 1024 # 上传 100MB
file = request.files['file']
# ...
注意:3.1.0 中还添加了 MAX_FORM_MEMORY_SIZE 和 MAX_FORM_PARTS 配置选项。请参阅安全文档。
功能: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 可能需要更新才能使用后备密钥。
错误: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。固定到旧版本的项目将发生冲突。
预防方法:
# 一起更新所有 Pallets 项目
pip install flask>=3.1.0 werkzeug>=3.1.0 itsdangerous>=2.2.0 blinker>=1.9.0
# 或使用 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 # 配置类
├── run.py # 入口点
│
├── app/
│ ├── __init__.py # 应用工厂 (create_app)
│ ├── extensions.py # Flask 扩展 (db, login_manager)
│ ├── models.py # SQLAlchemy 模型
│ │
│ ├── main/ # 主蓝图
│ │ ├── __init__.py
│ │ └── routes.py
│ │
│ ├── auth/ # 认证蓝图
│ │ ├── __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):
"""应用工厂函数。"""
app = Flask(__name__)
app.config.from_object(config_class)
# 初始化扩展
db.init_app(app)
login_manager.init_app(app)
# 注册蓝图
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")
# 创建数据库表
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"
为什么单独文件?:防止循环导入 - 模型可以导入 db 而无需导入 app。
# config.py
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
"""基础配置。"""
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):
"""开发配置。"""
DEBUG = True
class TestingConfig(Config):
"""测试配置。"""
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
WTF_CSRF_ENABLED = False
class ProductionConfig(Config):
"""生产配置。"""
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 # 在 bp 创建后导入!
# 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", # 蓝图特定模板
static_folder="static", # 蓝图特定静态文件
)
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):
"""用于身份验证的用户模型。"""
__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))
# 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("邮箱", validators=[DataRequired(), Email()])
password = PasswordField("密码", validators=[DataRequired()])
remember = BooleanField("记住我")
submit = SubmitField("登录")
class RegistrationForm(FlaskForm):
email = StringField("邮箱", validators=[DataRequired(), Email()])
password = PasswordField("密码", validators=[DataRequired(), Length(min=8)])
confirm = PasswordField("确认密码", validators=[
DataRequired(), EqualTo("password", message="密码必须匹配")
])
submit = SubmitField("注册")
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError("邮箱已注册。")
# 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("注册成功!请登录。", "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("登录成功!", "success")
return redirect(next_page or url_for("main.index"))
flash("邮箱或密码无效。", "danger")
return render_template("auth/login.html", form=form)
@bp.route("/logout")
@login_required
def logout():
logout_user()
flash("您已注销。", "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)
对于没有模板的 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": "缺少必填字段"}), 400
if User.query.filter_by(email=data["email"]).first():
return jsonify({"error": "邮箱已存在"}), 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 创建后current_app而非app - 在请求上下文中with app.app_context() - 在请求外访问数据库时app - 导致循环导入db - 导致 RuntimeErrorapp.run() - 使用 Gunicorn错误:ImportError: cannot import name 'X' from partially initialized module
原因:模型导入 app,app 导入模型
修复:使用 extensions.py 模式:
# 错误 - 循环导入
# app/__init__.py
from app.models import User # models.py 从这里导入 db!
# 正确 - 延迟导入
# app/__init__.py
def create_app():
# ... 设置 ...
from app.models import User # 在工厂内导入
错误:RuntimeError: Working outside of application context
原因:在请求外访问 current_app、g 或 db
修复:
# 错误
from app import create_app
app = create_app()
users = User.query.all() # 无上下文!
# 正确
from app import create_app
app = create_app()
with app.app_context():
users = User.query.all() # 有上下文
错误:werkzeug.routing.BuildError: Could not build url for endpoint
原因:在 url_for() 中使用错误的蓝图前缀
修复:
# 错误
url_for("login")
# 正确 - 包含蓝图名称
url_for("auth.login")
错误:Bad Request: The CSRF token is missing
原因:表单提交没有 CSRF 令牌
修复:在模板中包含令牌:
<form method="post">
{{ form.hidden_tag() }} <!-- 添加 CSRF 令牌 -->
<!-- 表单字段 -->
</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": "test@example.com",
"password": "testpass123",
"confirm": "testpass123",
}, follow_redirects=True)
assert response.status_code == 200
运行:uv run pytest
flask --app run run --debug
uv add gunicorn
uv run gunicorn -w 4 -b 0.0.0.0:8000 "run:app"
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"]
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 | jeremy@jezweb.net
每周安装次数
425
仓库
GitHub 星标数
650
首次出现
2026年1月20日
安全审计
安装于
claude-code322
opencode285
gemini-cli281
codex253
cursor244
antigravity238
Production-tested patterns for Flask with the application factory pattern, Blueprints, and Flask-SQLAlchemy.
Latest Versions (verified January 2026):
# 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)
Run: uv run flask --app app run --debug
This skill prevents 9 documented issues:
Error : KeyError in teardown functions when using stream_with_context Source : GitHub Issue #5804 Why It Happens : Flask 3.1.2 introduced a regression where stream_with_context triggers teardown_request() calls multiple times before response generation completes. If teardown callbacks use g.pop(key) without a default, they fail on the second call.
Prevention :
# 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
Status : Will be fixed in Flask 3.2.0 as side effect of PR #5812. Until then, ensure all teardown callbacks are idempotent.
Error : RuntimeError when handling concurrent async requests with gevent Source : GitHub Issue #5881 Why It Happens : Asgiref fails when gevent monkey-patching is active. Asyncio expects a single event loop per OS thread, but gevent's monkey-patching makes threading.Thread create greenlets instead of real threads, causing both loops to run on the same physical thread and block each other.
Prevention : Choose either async (with asyncio/uvloop) OR gevent, not both. If you must use both:
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__)
Note : This "defeats the whole purpose of both" (maintainer comment). Individual async requests work, but concurrent requests fail without this workaround.
Error : Session state incorrect after follow_redirects=True in tests Source : GitHub Issue #5786 Why It Happens : In Flask < 3.1.2, the test client's session wasn't correctly updated after following redirects.
Prevention :
# If using Flask >= 3.1.2, follow_redirects works correctly
def test_login_redirect(client):
response = client.post('/login',
data={'email': 'test@example.com', '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
Status : Fixed in Flask 3.1.2. Upgrade to latest version.
Error : RuntimeError: Working outside of application context in background threads Source : Sentry.io Guide Why It Happens : When passing current_app to a new thread, you must unwrap the proxy object using _get_current_object() and push app context in the thread.
Prevention :
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()
Verified : Common pattern in production applications, documented in official Flask docs.
Error : Users logged out unexpectedly when IP address changes Source : Flask-Login Docs Why It Happens : Flask-Login's "strong" session protection mode deletes the entire session if session identifiers (like IP address) change. This affects users on mobile networks or VPNs.
Prevention :
# 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)
Note : By default, Flask-Login allows concurrent sessions (same user on multiple browsers). To prevent this, implement custom session tracking.
Verified : Official Flask-Login documentation, multiple 2024 blog posts.
Error : Form submissions fail with "CSRF token missing/invalid" on cached pages Source : Flask-WTF Docs Why It Happens : If webserver cache policy caches pages longer than WTF_CSRF_TIME_LIMIT, browsers serve cached pages with expired CSRF tokens.
Prevention :
# 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
Verified : Official Flask-WTF documentation warning, security best practices guides from 2024.
Feature : Flask 3.1.0 added ability to customize Request.max_content_length per-request Source : Flask 3.1.0 Release Notes
Usage :
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']
# ...
Note : Also added MAX_FORM_MEMORY_SIZE and MAX_FORM_PARTS config options in 3.1.0. See security documentation.
Feature : Flask 3.1.0 added SECRET_KEY_FALLBACKS for key rotation Source : Flask 3.1.0 Release Notes
Usage :
# config.py
class Config:
SECRET_KEY = "new-secret-key-2024"
SECRET_KEY_FALLBACKS = [
"old-secret-key-2023",
"older-secret-key-2022"
]
Note : Extensions need explicit support for this feature. Flask-Login and Flask-WTF may need updates to use fallback keys.
Error : flask==2.2.4 incompatible with werkzeug==3.1.3 Source : Flask 3.1.0 Release Notes | GitHub Issue #5652 Why It Happens : Flask 3.1.0 updated minimum dependency versions: Werkzeug >= 3.1, ItsDangerous >= 2.2, Blinker >= 1.9. Projects pinned to older versions will have conflicts.
Prevention :
# 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"
For maintainable applications, use the factory pattern with blueprints:
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
Key Benefits :
# 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"
Why separate file? : Prevents circular imports - models can import db without importing app.
# 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()
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))
# 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)
For REST APIs without templates:
# 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
Register with prefix:
app.register_blueprint(api_bp, url_prefix="/api/v1")
__init__.py - After bp is createdcurrent_app not app - Inside request contextwith app.app_context() - When accessing db outside requestsapp in models - Causes circular importsdb before app context - RuntimeErrorapp.run() in production - Use GunicornError : ImportError: cannot import name 'X' from partially initialized module
Cause : Models importing app, app importing models
Fix : Use extensions.py pattern:
# 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
Error : RuntimeError: Working outside of application context
Cause : Accessing current_app, g, or db outside request
Fix :
# 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
Error : werkzeug.routing.BuildError: Could not build url for endpoint
Cause : Using wrong blueprint prefix in url_for()
Fix :
# WRONG
url_for("login")
# RIGHT - include blueprint name
url_for("auth.login")
Error : Bad Request: The CSRF token is missing
Cause : Form submission without CSRF token
Fix : Include token in templates:
<form method="post">
{{ form.hidden_tag() }} <!-- Adds CSRF token -->
<!-- form fields -->
</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": "test@example.com",
"password": "testpass123",
"confirm": "testpass123",
}, follow_redirects=True)
assert response.status_code == 200
Run: uv run pytest
flask --app run run --debug
uv add gunicorn
uv run gunicorn -w 4 -b 0.0.0.0:8000 "run:app"
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"]
SECRET_KEY=your-production-secret-key
DATABASE_URL=postgresql://user:pass@localhost/dbname
FLASK_ENV=production
Last verified : 2026-01-21 | Skill version : 2.0.0 | Changes : Added 9 known issues (stream_with_context regression, async/gevent conflicts, test client sessions, threading context, Flask-Login session protection, CSRF cache, new 3.1.0 features, Werkzeug dependencies) Maintainer : Jezweb | jeremy@jezweb.net
Weekly Installs
425
Repository
GitHub Stars
650
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code322
opencode285
gemini-cli281
codex253
cursor244
antigravity238
agent-browser 浏览器自动化工具 - Vercel Labs 命令行网页操作与测试
140,500 周安装