導入:Flask アプリは「機能不足」ではなく「構造不足」で壊れる
Flask の案件が技術的負債に沈むとき、原因はほとんど Flask の機能不足ではありません。構造の欠如です。app = Flask(__name__) を 1 ファイルのトップに書いて始めたアプリが、ルートとモデルとフォームを足していくうちに、ある日 ImportError: cannot import name 'app' from partially initialized module で起動しなくなる——これは Flask を触ったことのある人なら一度は見た光景でしょう。
この破綻は偶然ではなく、構造的な必然です。グローバルな app を中心に据えた瞬間、ビューもモデルも拡張も「app を import する」ことを強いられ、その app 側もビューを import する——依存が円を描き、import 順序という最も脆い前提にアプリ全体がぶら下がります。
本記事は、この破綻を Flask 3.1 系(現行安定版、本稿執筆時点で 3.1.3)の公式ドキュメントに忠実に解消する設計——**アプリケーションファクトリ(create_app)**と Blueprint、そして両者をつなぐ extensions.py の init_app パターン——を、本番品質で組み上げる深掘りガイドです。これは Flask 本番運用ガイド の §1(アプリ構成)と §3(ファクトリ)を、循環 import の回避と Blueprint の落とし穴という観点から徹底的に展開したスポークにあたります。全体像はまずピラーを参照してください。
筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを Python / Flask / SQLAlchemy / PostgreSQL で設計・実装し、API Gateway → ALB → ECS(Fargate) 上で本番運用してきました。エンドポイントが増え、認証・課金・管理画面が別々の関心事として育っていく中で、ここで示すファクトリと Blueprint の規律だけが、コードベースを「変更しやすい状態(ETC)」に保ち続けました。本記事はその実戦の設計判断を、公式仕様に照らして言語化したものです。
💡 この記事で扱うバージョン:Flask 3.1 系(最新安定版 3.1.3)を前提とします。Flask 3.1 は Python 3.9 以上が必要です。なお Flask 3.0 で
Flask.__version__は廃止されたため、バージョンを取得したいときはimportlib.metadata.version("flask")を使ってください(後述)。本稿のコードは公式ドキュメントのパターンに基づきます。
1. なぜ単一ファイルを超えるのか:グローバル app と循環 import の破綻
設計を語る前に、何が壊れるのかを正確に理解します。Flask の最小形はこうでした。
# hello.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello, World!"
学習にはこれで十分です。問題は、機能が増えてビューやモデルを別ファイルに切り出した瞬間に起きます。典型的な「壊れ方」を再現します。
# models.py — モデルが app を欲しがる
from app import app # ← app.py を import
db = ... # app.config を読んで初期化したい
# app.py — app がビュー(モデルを使う)を欲しがる
from flask import Flask
app = Flask(__name__)
import models # ← models.py を import(models は app を import する)
app.py を起動すると、import models の行で models.py が from app import app を実行します。しかしこの時点で app.py はまだ import models の行を実行中で、モジュールの初期化が完了していません。結果、app という名前がまだ束縛されておらず、ImportError: cannot import name 'app' from partially initialized module 'app'(循環 import)で落ちます。
この破綻には、3 つの独立した症状があります。
| 症状 | 何が起きるか | 根本原因 |
|---|---|---|
| テストで設定を差し替えられない | app は import 時に確定するので、テスト用 DB やテスト設定に切り替える隙がない | app がモジュールトップで即時生成される |
| 複数構成を同時に持てない | 本番・ステージング・テストで異なる設定のアプリを並行して作れない | app がモジュール内でシングルトン化している |
| 循環 import を誘発する | ビュー/モデルが app を import し、app 側もそれらを import する | 依存方向が双方向になっている |
⚠️ アンチパターン:「とりあえず動くから」とグローバル
appのまま機能を足し続け、循環 import が出たらimport文を関数の中に押し込んで(遅延 import で)その場しのぎをする。これは負債を隠すだけで、依存が円を描いている事実は消えません。次節のアプリケーションファクトリは、依存方向を「上から下」に一方向化することで、この円を構造から断ち切ります。
2. アプリケーションファクトリ:create_app を深掘りする
解決策は アプリケーションファクトリです。app をモジュールトップで生成するのをやめ、関数の中で組み立てて返す。「いつ・どの設定で・どの部品を載せて」アプリを作るかを、呼び出し側が完全に制御できるようになります。
2.1 公式チュートリアルの正準形
Flask 公式チュートリアル(Flaskr)が示すファクトリの正準形がこれです。すべての要素に意味があるので、一行ずつ読み解きます。
# src/myapp/__init__.py
import os
from flask import Flask
def create_app(test_config: dict | None = None) -> Flask:
# ① instance_relative_config=True で、設定をリポジトリ外の instance/ から読む
app = Flask(__name__, instance_relative_config=True)
# ② コード内の安全な既定値(本番では必ず上書きする)
app.config.from_mapping(
SECRET_KEY="dev",
DATABASE=os.path.join(app.instance_path, "myapp.sqlite"),
)
if test_config is None:
# ③ 本番/開発:instance/config.py があれば上書き(無ければ黙ってスキップ)
app.config.from_pyfile("config.py", silent=True)
else:
# ④ テスト:呼び出し側が渡した設定で上書き
app.config.from_mapping(test_config)
# ⑤ instance フォルダは Flask が自動生成しない。明示的に作る
os.makedirs(app.instance_path, exist_ok=True)
@app.route("/health")
def health():
return {"status": "ok"}
return app
- ①
instance_relative_config=True:設定ファイルの解決基点を、パッケージ内ではなくインスタンスフォルダ(app.instance_path、既定で<project>/instance/)に切り替えます。これにより秘密情報を含む設定をリポジトリ外に置けます(§6.2)。 - ②
from_mapping(...):コードに直書きできる安全な既定値だけをここに置きます。SECRET_KEY="dev"は開発用のダミーで、本番では必ず上書きされる前提です。 - ③④
test_config分岐:この分岐がファクトリの心臓です。引数がNone(通常起動)ならinstance/config.pyから本番設定を読み、引数が渡されれば(テスト)その設定で上書きします。テストは「実ファイルを置かずに」設定を注入できます。 - ⑤
os.makedirs(app.instance_path, exist_ok=True):Flask はインスタンスフォルダを自動生成しません。SQLite やアップロードをそこに置くなら、ファクトリで明示的に作る必要があります。公式チュートリアルもこの一行を入れています。
起動は flask CLI がファクトリを自動検出します。
flask --app myapp run --debug
2.2 ファクトリの自動検出と引数渡し
flask --app は、対象モジュールに create_app または make_app という名前の関数があれば、それをファクトリとして自動的に呼び出します。引数を渡したいときは、括弧内を Python リテラルとして書けます。
# 引数なしのファクトリを自動検出
flask --app myapp run --debug
# ファクトリに文字列リテラル "dev" を渡す(括弧内は Python リテラルとして解釈)
flask --app 'hello:create_app("dev")' run
この書式は本番の Gunicorn でも同じです。gunicorn 'myapp:create_app()' のようにファクトリを直接指せます。Gunicorn のワーカー数・ProxyFix・Docker 化といった本番デプロイの詳細は 本番デプロイガイド に集約しています。
2.3 なぜファクトリがテスト容易性と複数環境を「同時に」解決するのか
ファクトリの本質的な価値は、アプリの生成を「遅延」させ「パラメータ化」することにあります。
- テスト容易性:
create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:"})のように、テストごとにクリーンな設定でアプリを生成できます。pytest の fixture でアプリを作り直せば、テスト間の状態汚染も起きません(テストの詳細は テスト実践ガイド へ)。 - 複数環境:同じ
create_appを、本番は環境変数の設定で、テストはtest_config引数で呼び分けるだけです。環境ごとにコードを分岐させる必要がありません。
💡 ファクトリは「将来の拡張」ではなく「現在の要件」:アプリケーションファクトリを「規模が大きくなったら導入する」ものと捉えるのは誤りです。テスト容易性は最初から必要な要件であり、ファクトリはそれを満たす最小の構造です。小さなアプリでも
create_appとextensions.pyの分離だけは最初から入れる——これが負債を生まない出発点です(§7 の YAGNI 規律と矛盾しません。分割すべきは Blueprint であって、ファクトリの導入は別問題です)。
3. extensions.py パターン:裸の拡張 → init_app で束縛する
ファクトリが循環 import を断ち切る鍵は、拡張オブジェクトの扱い方にあります。Flask-SQLAlchemy や Flask-Migrate のような拡張を、app と一緒に生成してしまうと、再び循環 import の罠に戻ります。公式が示す解法は明快です——拡張を「アプリに束縛されていない裸の状態」で生成し、ファクトリ内で init_app(app) を呼んで初めて束縛する。
3.1 二段階初期化の構造
# src/myapp/extensions.py — どのアプリにも束縛されていない「裸」の拡張
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy() # この時点では app を知らない
migrate = Migrate() # この時点では app を知らない
# src/myapp/__init__.py(抜粋)
from flask import Flask
from .extensions import db, migrate
def create_app(test_config: dict | None = None) -> Flask:
app = Flask(__name__, instance_relative_config=True)
# ...設定読み込み(§2 / §6)...
# ここで初めて app に束縛する(二段階初期化の第二段)
db.init_app(app)
migrate.init_app(app, db)
# Blueprint の登録は import を「関数の中」で行う(§4)
from .blueprints.api import bp as api_bp
app.register_blueprint(api_bp, url_prefix="/api")
return app
ポイントは、db = SQLAlchemy() が extensions.py でアプリと無関係に生成されることです。モデル定義は from myapp.extensions import db で db を import しますが、この db は app を一切知らないため、app を import する必要がありません。依存方向が models → extensions の一方向になり、円が消えます。
3.2 なぜこれが循環 import を構造的に解消するのか
依存グラフで見ると、二段階初期化の威力が明確になります。
| アプローチ | 依存方向 | 結果 |
|---|---|---|
グローバル app + db = SQLAlchemy(app) | models → app → models | 循環(起動不能) |
extensions.py + db.init_app(app) | models → extensions、__init__ → extensions、__init__ → models(関数内 import) | 一方向(健全) |
extensions.py が「アプリにも、モデルにも、ビューにも依存しない最下層」になることで、すべてのモジュールが安心して extensions を import できます。これは依存性逆転の素朴な実装——具体(特定のアプリ)ではなく抽象(裸の拡張オブジェクト)に依存する——でもあります。
3.3 1 つの拡張オブジェクトが複数アプリに再利用できる
この設計には、循環 import の回避以外にもう 1 つ重要な帰結があります。db という単一の拡張オブジェクトが、init_app を通じて複数のアプリに束縛できることです。本番アプリとテストアプリが、それぞれ別の DB 設定で同じ db オブジェクトを使えます。これがファクトリ(§2)と拡張パターンが噛み合う理由です——ファクトリが複数のアプリを作り、各アプリに同じ拡張を init_app で結びつける。
💡
current_appがこの設計を完成させる:ファクトリでアプリを作ると「appをモジュールトップから import できない」状況が生まれます(import 時には存在しないため)。拡張やビューが現在のアプリの設定へアクセスするには、appを引き回す代わりにcurrent_appプロキシを使います。current_app.config["..."]は「いま処理中のアプリ」へ動的に解決されるため、ファクトリ構成と完全に整合します。current_app/g/ アプリケーションコンテキストの仕組みは コンテキスト徹底解説 で深掘りしています。永続化層(SQLAlchemy 2.0 の型付き ORM)の設計は SQLAlchemy 2.0 実践ガイド を併読してください。
4. Blueprint 徹底解説:分割の単位と公式仕様の落とし穴
アプリケーションファクトリが「アプリをどう作るか」を解決したら、次は「機能をどう分割するか」です。それが Blueprint です。Blueprint は「ルート・テンプレート・静的ファイル・エラーハンドラ・CLI コマンドをひとまとめにした、登録可能な機能ユニット」です。Blueprint API は Flask 0.7 で追加されました。
4.1 定義と登録:name は URL ではなく endpoint の前綴り
最も重要な誤解から潰します。Blueprint の name(第一引数)は URL を決めません。endpoint(url_for で使う名前)を前綴りします。
# src/myapp/blueprints/simple_page.py
from flask import Blueprint, render_template
from werkzeug.exceptions import abort
# 第1引数 = name(endpoint の前綴り)、第2引数 = import_name
simple_page = Blueprint("simple_page", __name__, template_folder="templates")
@simple_page.route("/<page>")
def show(page):
return render_template(f"pages/{page}.html")
# create_app 内で登録
from .blueprints.simple_page import simple_page
app.register_blueprint(simple_page)
# URL を前綴りしたいなら url_prefix を登録時に指定する
app.register_blueprint(simple_page, url_prefix="/pages")
ここで url_for がどう変わるかが核心です。
| 指定 | endpoint | 生成される URL(url_prefix="/pages") |
|---|---|---|
url_for("simple_page.show", page="about") | simple_page.show | /pages/about |
name(simple_page)は endpoint に . 区切りで前綴りされ、URL には url_prefix が前綴りされる——この 2 つは別物です。name を変えても URL は変わらず、url_prefix を変えても endpoint は変わりません。この分離を理解していないと、url_for で BuildError を出して延々ハマります。
⚠️ アンチパターン:
Blueprint("/admin", __name__)のように、nameに URL パスを書く。nameは endpoint の名前空間であって URL ではありません。スラッシュを含むnameは不正です。URL を付けたいならapp.register_blueprint(admin, url_prefix="/admin")を使ってください。同様に、url_for("show")のように Blueprint 名を省くと、ビューが別 Blueprint にいるとき解決に失敗します。同一 Blueprint 内ならurl_for(".show")(先頭ドット)で相対参照できます。
4.2 Blueprint の入れ子(nesting)
Blueprint は入れ子にできます。親 Blueprint に子を登録し、親をアプリに登録すると、子は親の name と url_prefix の両方を継承します。
parent = Blueprint("parent", __name__, url_prefix="/parent")
child = Blueprint("child", __name__, url_prefix="/child")
@child.route("/create")
def create():
return "created"
parent.register_blueprint(child) # 親に子を登録
app.register_blueprint(parent) # アプリに親を登録
この結果、endpoint は parent.child.create、URL は /parent/child/create になります。
url_for("parent.child.create") # → "/parent/child/create"
name も url_prefix も連結される点に注意してください。subdomain= を使ったサブドメインでの入れ子も同様に機能します。入れ子は「管理画面の中にユーザー管理と請求管理がある」といった階層を素直に表現できますが、深くしすぎると endpoint 名が長大になります。入れ子は 2 段までを目安にし、それ以上は別アプリ(マイクロサービス)への分割を検討するのが現実的です。
4.3 Blueprint ローカルのテンプレートと静的ファイル:落とし穴の宝庫
Blueprint は自前のテンプレート・静的ファイルを持てますが、ここに公式仕様上の落とし穴が集中しています。
テンプレート:Blueprint("x", __name__, template_folder="templates") で Blueprint 固有のテンプレートフォルダを追加できます。ただし優先順位に注意が必要です。
- Blueprint のテンプレートフォルダは、アプリのテンプレートフォルダより低い優先度で追加されます。つまり同じ相対パスのテンプレートがあれば、アプリ側が Blueprint 側を上書きできます(カスタマイズの余地を残す設計)。
- 複数の Blueprint が同じ相対パスのテンプレートを提供した場合、最初に登録された Blueprint が勝ちます。
⚠️ テンプレート衝突の罠:複数の Blueprint がそれぞれ
templates/index.htmlを持つと、後から登録した Blueprint のindex.htmlは永久に解決されません(最初に登録されたものが勝つため)。これを避けるには、Blueprint ごとにサブフォルダで名前空間を切ります——templates/admin/index.html、templates/auth/index.htmlのように。テンプレートの相対パスを Blueprint 名で名前空間化するのが、衝突を防ぐ唯一確実な方法です。
静的ファイル:ここがさらに罠です。Blueprint("admin", __name__, static_folder="static") で静的フォルダを持てますが、url_prefix を持たない Blueprint は、自分の静的フォルダを配信できません。
- Blueprint の静的ファイルは
url_prefix + /staticで配信され、endpoint はadmin.staticになります。 url_prefixがない Blueprint では、アプリの/staticが勝ち、Blueprint の静的フォルダはフォールバックとして検索すらされません。
# url_prefix があれば静的フォルダが配信される
admin = Blueprint("admin", __name__, static_folder="static")
app.register_blueprint(admin, url_prefix="/admin")
# → /admin/static/... で配信、endpoint は "admin.static"
# → url_for("admin.static", filename="style.css") → "/admin/static/style.css"
実務では、Blueprint ごとに静的ファイルを散らすより、アプリ単一の /static に集約してビルドツール(Vite 等)でバンドルするほうが運用が単純です。Blueprint ローカル静的は「プラグイン的に完全独立した機能ユニットを配布する」ような特殊な場合に限るのが無難です。
4.4 Blueprint のエラーハンドラと「404 を捕捉できない」例外
Blueprint は @bp.errorhandler(404) でエラーハンドラを登録できますが、ここに最も誤解されやすい落とし穴があります。
@simple_page.errorhandler(404)
def page_not_found(e):
# これは「無効な URL アクセス」では呼ばれない!
return render_template("pages/404.html"), 404
Blueprint の errorhandler(404) は、その Blueprint の自身のビュー関数の中で abort(404) や raise が起きたときにだけ呼ばれます。 存在しない URL へのアクセス(ルーティング失敗)では呼ばれません。理由は明快で——Blueprint は URL 空間を「所有」していないからです。/nonexistent というリクエストがどの Blueprint に属するべきかを Flask は判断できないため、無効 URL の 404 はアプリレベルのハンドラでしか捕捉できません。
アプリ全体に効くハンドラを Blueprint から登録したいときは、app_errorhandler を使います。
# 自分の Blueprint のビュー内の abort(403) だけを捕捉
@simple_page.errorhandler(403)
def handle_403(e):
return render_template("pages/403.html"), 403
# アプリ全体の 404(無効 URL を含む)を、この Blueprint から登録する
@simple_page.app_errorhandler(404)
def handle_app_404(e):
return render_template("404.html"), 404
| デコレータ | スコープ | 無効 URL の 404 を捕捉? |
|---|---|---|
@bp.errorhandler(404) | その Blueprint 自身のビュー内の abort/raise のみ | ❌ しない |
@bp.app_errorhandler(404) | アプリ全体(無効 URL を含む) | ✅ する |
@app.errorhandler(404) | アプリ全体 | ✅ する |
エラーハンドラの解決順序、HTTPException の一括ハンドリング、API での JSON エラー設計の詳細は エラー処理・可観測性ガイド に分けています。
4.5 Blueprint の CLI コマンド
Blueprint は @bp.cli.command(...) で、その Blueprint に紐づく flask CLI コマンドを定義できます。既定では、Blueprint 名を冠したグループの下に入ります。
# blueprints/admin/__init__.py
import click
from flask import Blueprint
admin = Blueprint("admin", __name__)
@admin.cli.command("create")
@click.argument("name")
def create(name):
"""管理ユーザーを作成する。"""
click.echo(f"created admin: {name}")
# 既定では Blueprint 名(admin)のグループ配下に入る
flask admin create alice
グループ名は変更できます。Blueprint("admin", __name__, cli_group="other") で別名のグループに、cli_group=None でトップレベルに昇格させて flask create alice にできます。db migrate のような独立した運用コマンド群を Blueprint 単位で持たせると、CLI が機能ごとに整理されて運用しやすくなります。
5. 推奨パッケージ構成:src/ レイアウトの具体例
ここまでの設計要素——ファクトリ・extensions.py・Blueprint・Config——を、1 つのディレクトリ構成に落とします。これが本番 Flask の「型」です。src/ レイアウト(パッケージを src/ 配下に置く)を採用すると、テスト時に「インストール済みのパッケージ」として import され、「カレントディレクトリのファイルをうっかり import する」事故を防げます。
myapp/
├── pyproject.toml
├── src/
│ └── myapp/
│ ├── __init__.py # create_app(アプリケーションファクトリ)§2
│ ├── config.py # 環境別 Config クラス(Base/Production/Test)§6
│ ├── extensions.py # db = SQLAlchemy() など「裸」の拡張 §3
│ ├── errors.py # app_errorhandler でアプリ全体のエラー整形 §4.4
│ ├── models/
│ │ ├── __init__.py
│ │ ├── user.py # from myapp.extensions import db
│ │ └── invoice.py
│ └── blueprints/
│ ├── __init__.py
│ ├── auth/ # 認証 Blueprint(login/logout/signup)
│ │ ├── __init__.py # auth = Blueprint("auth", __name__)
│ │ ├── routes.py
│ │ └── templates/auth/ # 名前空間化したテンプレート §4.3
│ └── api/ # API Blueprint(REST エンドポイント)
│ ├── __init__.py # api = Blueprint("api", __name__)
│ ├── routes.py
│ └── schemas.py # marshmallow スキーマ(境界バリデーション)
├── instance/ # .gitignore(秘密・SQLite・アップロード)§6.2
│ └── config.py
├── migrations/ # Flask-Migrate(Alembic)
└── tests/
├── conftest.py # app / client / runner fixtures
└── test_*.py
この構成での依存方向を明示します——すべて「上から下」の一方向です。
__init__.py (create_app)
├──→ config.py (Config クラスを読む)
├──→ extensions.py (db, migrate を init_app)
├──→ blueprints/* (register_blueprint)
└──→ errors.py (app_errorhandler を登録)
blueprints/api/routes.py
├──→ extensions.py (db を使う)
├──→ models/* (モデルを使う)
└──→ blueprints/api/schemas.py
models/* ──→ extensions.py (db.Model を継承するためだけに db を import)
extensions.py が最下層にいて、誰も __init__.py(create_app)を import しない——この形を保つ限り、循環 import は構造的に発生しません。API Blueprint の schemas.py における「Router → Schema(境界)→ Model の層分離」は、marshmallow × Flask × SQLAlchemy で本番 REST API を設計する で詳説しています。本記事の Blueprint が「どこに」を、その記事が「中で何を」を担います。
💡
models/とblueprints/をパッケージ(ディレクトリ+__init__.py)にする:単一ファイルのmodels.pyで始めても問題ありませんが、モデルが増えたらmodels/パッケージに昇格させ、models/__init__.pyで各モデルを再エクスポートします。こうするとfrom myapp.models import Userという import パスを安定させたまま、内部のファイル分割を自由に変えられます。import パスの安定性は、リファクタの自由度に直結します。
6. 環境別 Config:from_object + from_prefixed_env の二段構え
ファクトリの test_config 分岐(§2.1)と組み合わせて、環境ごとに設定を切り替える仕組みを整えます。本番で揺るがしてはならない原則は 1 つ——秘密情報(SECRET_KEY・DB パスワード・API キー)をコードに書かない。これは 12-factor の核であり、セキュリティの最低ラインです。
6.1 Config クラスの継承階層
app.config.from_object(...) は、大文字の属性だけを設定として読み込みます。これを使い、Base → Production / Test の継承で環境差分を表現します。
# src/myapp/config.py
class BaseConfig:
"""全環境共通の安全な既定値。"""
JSON_SORT_KEYS = False
SESSION_COOKIE_HTTPONLY = True # 既定 True だが意図を明示
SESSION_COOKIE_SAMESITE = "Lax"
SQLALCHEMY_TRACK_MODIFICATIONS = False
class ProductionConfig(BaseConfig):
SESSION_COOKIE_SECURE = True # HTTPS 限定 Cookie
class TestConfig(BaseConfig):
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
# create_app 内(§2 の設定読み込み部分を置き換える)
def create_app(test_config: dict | None = None) -> Flask:
app = Flask(__name__, instance_relative_config=True)
# ① Config クラスで安全な既定値を与える
app.config.from_object("myapp.config.ProductionConfig")
# ② 環境変数で本番値を上書きする(FLASK_ 前綴り)
app.config.from_prefixed_env()
if test_config is not None:
app.config.from_mapping(test_config) # テストは最後に上書き
db.init_app(app)
# ...Blueprint 登録...
return app
⚠️
from_objectの罠:クラス参照は「インスタンス化されない」。from_object("myapp.config.ProductionConfig")は、クラスの大文字属性をそのまま読むだけで、クラスをインスタンス化しません。つまり@propertyで計算した設定値は読み込まれません(propertyオブジェクトのまま無視される)。動的に計算した設定が必要なら、クラスではなくインスタンスを渡す(from_object(ProductionConfig()))か、from_prefixed_env()で環境変数から流し込んでください。
6.2 from_prefixed_env とインスタンスフォルダ
from_prefixed_env() は Flask 3.0 で追加された、コンテナ/12-factor 時代に最適なメソッドです。FLASK_ で始まる環境変数を自動で読み込み、値は json.loads で型付けします。
# 本番の環境変数(コンテナの Secrets から注入)
export FLASK_SECRET_KEY='...' # python -c 'import secrets; print(secrets.token_hex())'
export FLASK_SQLALCHEMY_DATABASE_URI='postgresql+psycopg://...'
export FLASK_MAX_CONTENT_LENGTH=10485760 # json.loads されて int になる
export FLASK_SQLALCHEMY_ENGINE_OPTIONS__pool_size=10 # __ でネストしたキーに代入
FLASK_MAX_CONTENT_LENGTH=10485760 は文字列ではなく int 10485760 として読み込まれ、__(二重アンダースコア)でネストした辞書キーにも代入できます。「環境変数は全部文字列で扱いづらい」という典型的な悩みを Flask 側が解消します。
設定の読み込み口は他にもあります。本記事で使う from_object / from_prefixed_env / from_pyfile のほか、JSON/TOML を読む from_file(path, load=tomllib.load, text=False)、単一の環境変数からパスを読む from_envvar があります。設定管理の全体像は Flask 本番運用ガイド の §4 にまとまっています。
インスタンスフォルダ(instance/、Flask 0.8 で追加)は、instance_relative_config=True(§2.1)のときに from_pyfile("config.py") の解決基点になる「リポジトリにコミットしない設定置き場」です。instance/ は必ず .gitignore に入れてください。Cookie・CSRF・SECRET_KEY 周りのセキュリティ設定の深掘りは セキュリティ実装ガイド を参照してください。
6.3 Flask のバージョンを取得する
設定やヘルスチェックでフレームワークのバージョンを出したくなることがあります。Flask 3.0 で Flask.__version__ は廃止されました。代わりに標準ライブラリの importlib.metadata を使います。
from importlib.metadata import version
flask_version = version("flask") # "3.1.3" のような文字列
7. いつ分割するか:「3 つ目で抽出」の YAGNI 規律
ここまで Blueprint と src レイアウトを説明してきましたが、最も重要なのは「いつ分割するか」の判断です。設計力とは、構造を増やす力ではなく、構造を増やすべき瞬間を見極める力です。
最初から blueprints/auth/、blueprints/api/、blueprints/admin/ と細かく割る必要はありません。これは典型的な早すぎる抽象化で、まだ存在しない要件のために認知負荷を先払いする行為です。筆者が本番で守ってきた規律はシンプルです。
| 段階 | 構造 | 判断 |
|---|---|---|
| 1 機能目 | create_app 内に直接ルートを書く | Blueprint はまだ不要 |
| 2 機能目 | 1 つの Blueprint にまとめる | 「分けたくなる」が、まだ我慢 |
| 3 機能目 | 関心事ごとに Blueprint へ抽出する | ここで初めて分割 |
この「3 つ目で抽出」は、DRY の「2 回は偶然、3 回はパターン」と同じ哲学です。2 つの似たルートは偶然の一致かもしれませんが、3 つ揃えば「これは独立した関心事だ」という確信が持てます。確信が持てるまで分割を遅らせることで、間違った境界線で割ってしまう(後で割り直す羽目になる)リスクを避けられます。
💡 分割の単位は「変更理由(SRP)」で決める:Blueprint を「URL のプレフィックス」で割るのではなく、「何が変わったら一緒に変わるか」で割ってください。認証ロジックが変わるとき一緒に変わるものは
auth/に、API のスキーマが変わるとき一緒に変わるものはapi/に。同じ理由で変わるコードを 1 つの Blueprint に集め、違う理由で変わるコードを分ける——これが単一責任原則の、Blueprint 粒度での適用です。URL は結果であって、分割の基準ではありません。
ただし例外があります。create_app と extensions.py の分離だけは、機能が 1 つでも最初から入れてください(§2.3)。これは「将来の拡張のため」ではなく「テスト容易性という現在の要件」を満たすための分離だからです。YAGNI が適用されるのは Blueprint の細分化であって、ファクトリの導入ではありません。この区別を取り違えないことが、過小設計(テストできない)と過剰設計(割りすぎ)の両方を避ける鍵です。
まとめ:循環 import は「設計」で構造的に消す
大規模 Flask アプリの破綻は、Flask の機能不足ではなく構造不足から来ます。本記事の規律を再掲します。
- グローバル
appを捨てる。モジュールトップのapp = Flask(__name__)は、テスト不能・単一構成・循環 import という三大破綻の根源。 - **アプリケーションファクトリ(
create_app(test_config=None))**を背骨にする。instance_relative_config=Trueとtest_config分岐で、テスト容易性と複数環境を同時に解決する。 - 拡張は
extensions.pyで裸に生成し、init_app(app)で束縛する。extensionsを最下層に置くことで依存を一方向化し、循環 import を構造から消す。1 つの拡張が複数アプリに再利用できる。 - Blueprint で機能を分割する。
nameは endpoint の前綴りで URL ではない、無効 URL の 404 は Blueprint のerrorhandlerでは捕捉できずapp_errorhandlerが要る、url_prefixのない Blueprint は静的フォルダを配信できない、テンプレートは最初に登録したものが勝つ——この公式仕様の落とし穴を踏まない。 - **環境別 Config は
from_object(既定値)→from_prefixed_env(環境上書き)**の二段構え。from_objectはクラスをインスタンス化しない点に注意し、秘密はインスタンスフォルダと環境変数でコードから追い出す。 - 分割は「3 つ目で抽出」の YAGNI 規律で。Blueprint の細分化は遅らせてよいが、
create_appとextensions.pyの分離だけは最初から入れる。
これらは「規模が大きくなったら導入する」ものではなく、最初の数百行から効く設計です。Flask は「核だけ」を提供し、構造はあなたに委ねます。その自由に責任を持つ最小の規律が、アプリケーションファクトリと Blueprint です。全体像と各論(コンテキスト・テスト・デプロイ・セキュリティ・可観測性)の地図は Flask 本番運用ガイド に戻って俯瞰してください。