メインコンテンツへスキップ
友田 陽大
Flask 本番運用
Python
Flask
Blueprint
アーキテクチャ設計
本番運用
バックエンド

Flaskの大規模アプリ構成:アプリケーションファクトリ(create_app)とBlueprintで循環importを避けて拡張する

Flask 3.1系の大規模アプリ構成を本番品質で設計する実践ガイド。グローバルappと循環importの破綻、create_appアプリケーションファクトリ、extensions.pyの裸の拡張→init_app、Blueprintのurl_prefix・endpoint命名・入れ子・テンプレート/静的ファイル・エラーハンドラ・CLI、from_object+from_prefixed_envの環境別Config、src/レイアウトとYAGNIの分割規律を公式ドキュメントに忠実な実コードで解説します。

公開日
読了時間
24分
著者
友田 陽大
シェア
目次

導入: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.pyinit_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.pyfrom 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_appextensions.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 dbdb を import しますが、この dbapp を一切知らないため、app を import する必要がありません。依存方向が models → extensions の一方向になり、円が消えます。

3.2 なぜこれが循環 import を構造的に解消するのか

依存グラフで見ると、二段階初期化の威力が明確になります。

アプローチ依存方向結果
グローバル appdb = SQLAlchemy(app)models → app → models循環(起動不能)
extensions.pydb.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

namesimple_page)は endpoint に . 区切りで前綴りされ、URL には url_prefix が前綴りされる——この 2 つは別物です。name を変えても URL は変わらず、url_prefix を変えても endpoint は変わりません。この分離を理解していないと、url_forBuildError を出して延々ハマります。

⚠️ アンチパターン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 に子を登録し、親をアプリに登録すると、子は親の nameurl_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"

nameurl_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.htmltemplates/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__.pycreate_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_appextensions.py の分離だけは、機能が 1 つでも最初から入れてください(§2.3)。これは「将来の拡張のため」ではなく「テスト容易性という現在の要件」を満たすための分離だからです。YAGNI が適用されるのは Blueprint の細分化であって、ファクトリの導入ではありません。この区別を取り違えないことが、過小設計(テストできない)と過剰設計(割りすぎ)の両方を避ける鍵です。


まとめ:循環 import は「設計」で構造的に消す

大規模 Flask アプリの破綻は、Flask の機能不足ではなく構造不足から来ます。本記事の規律を再掲します。

  1. グローバル app を捨てる。モジュールトップの app = Flask(__name__) は、テスト不能・単一構成・循環 import という三大破綻の根源。
  2. **アプリケーションファクトリ(create_app(test_config=None))**を背骨にする。instance_relative_config=Truetest_config 分岐で、テスト容易性と複数環境を同時に解決する。
  3. 拡張は extensions.py で裸に生成し、init_app(app) で束縛するextensions を最下層に置くことで依存を一方向化し、循環 import を構造から消す。1 つの拡張が複数アプリに再利用できる。
  4. Blueprint で機能を分割するname は endpoint の前綴りで URL ではない、無効 URL の 404 は Blueprint の errorhandler では捕捉できず app_errorhandler が要る、url_prefix のない Blueprint は静的フォルダを配信できない、テンプレートは最初に登録したものが勝つ——この公式仕様の落とし穴を踏まない。
  5. **環境別 Config は from_object(既定値)→ from_prefixed_env(環境上書き)**の二段構え。from_object はクラスをインスタンス化しない点に注意し、秘密はインスタンスフォルダと環境変数でコードから追い出す。
  6. 分割は「3 つ目で抽出」の YAGNI 規律で。Blueprint の細分化は遅らせてよいが、create_appextensions.py の分離だけは最初から入れる。

これらは「規模が大きくなったら導入する」ものではなく、最初の数百行から効く設計です。Flask は「核だけ」を提供し、構造はあなたに委ねます。その自由に責任を持つ最小の規律が、アプリケーションファクトリと Blueprint です。全体像と各論(コンテキスト・テスト・デプロイ・セキュリティ・可観測性)の地図は Flask 本番運用ガイド に戻って俯瞰してください。

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事で解説した技術の適用事例

経済産業大臣賞受賞 | 木材流通業界のDXを実現したB2BサブスクリプションSaaS

ケーススタディを見る