# Flask Large-App Structure: Extending Without Circular Imports Using the Application Factory (create_app) and Blueprints

> A practical guide for designing a large Flask 3.1-series app structure at production quality. Explained with real code faithful to the official documentation: the breakdown of the global app and circular imports, the create_app application factory, the bare extension → init_app of extensions.py, Blueprints' url_prefix / endpoint naming / nesting / templates and static files / error handlers / CLI, per-environment Config with from_object + from_prefixed_env, and the src/ layout and the YAGNI discipline of splitting.

- Published: 2026-06-25
- Author: 友田 陽大
- Tags: Python, Flask, Blueprint, アーキテクチャ設計, 本番運用, バックエンド
- URL: https://tomodahinata.com/en/blog/flask-application-factory-blueprints-large-app-structure-guide
- Category: Flask in production
- Pillar guide: https://tomodahinata.com/en/blog/flask-production-guide

## Key points

- Place a global app at the module top and you can't swap settings in tests, can't have multiple environments, and views and models circularly import. These are large Flask's three big breakdowns
- The create_app(test_config=None) application factory is the backbone. With instance_relative_config=True and the test_config branch, it solves testability and multiple environments at once
- Generate extensions bare (db = SQLAlchemy()) in extensions.py and bind them with init_app(app) inside the factory. One extension object can be reused across multiple apps, and circular imports disappear
- A Blueprint's name is the prefix of the endpoint, not the URL. There are official-spec pitfalls in url_prefix, nesting, blueprint-local templates / static files, the inability to catch 404s in errorhandler, and CLI groups
- Per-environment Config is a two-stage Base→Production/Test inheritance + from_object→from_prefixed_env. Judge splitting into Blueprints with the 'extract on the third' YAGNI discipline

---

## **Introduction: a Flask app breaks not from "lack of features" but from "lack of structure"**

When a Flask project sinks into technical debt, the cause is almost never a lack of Flask features. It's **a lack of structure.** An app that started by writing `app = Flask(__name__)` at the top of one file, while adding routes, models, and forms, one day stops launching with `ImportError: cannot import name 'app' from partially initialized module` — anyone who has touched Flask has seen this scene at least once.

This breakdown isn't coincidence but a structural inevitability. The moment you place a global `app` at the center, views, models, and extensions are all forced to "import `app`," and that `app` side also imports the views — dependencies draw a circle, and the whole app hangs on the most fragile premise of import order.

This article is a deep-dive guide for resolving this breakdown **faithfully to the official documentation of the Flask 3.1 series (the current stable version, 3.1.3 at the time of writing)** — the **application factory (`create_app`)** and **Blueprints**, and the **`init_app` pattern of `extensions.py`** that connects the two — and assembling it at production quality. This is a spoke that thoroughly develops §1 (app structure) and §3 (the factory) of the [Flask production operation guide](/blog/flask-production-guide) from the angles of avoiding circular imports and the pitfalls of Blueprints. For the overall picture, first refer to the pillar.

The author has **designed and implemented the backend of a B2B SaaS that won the Minister of Economy, Trade and Industry Award in Python / Flask / SQLAlchemy / PostgreSQL, and operated it in production on API Gateway → ALB → ECS (Fargate).** As endpoints grew and authentication, billing, and the admin panel grew as separate concerns, only the discipline of the factory and Blueprints shown here kept the codebase in "an easy-to-change state (ETC)." This article articulates that real-combat design judgment in light of the official spec.

> 💡 **The version covered in this article**: it assumes the **Flask 3.1 series** (the latest stable version 3.1.3). Flask 3.1 requires **Python 3.9 or later**. Note that `Flask.__version__` was removed in Flask 3.0, so when you want to get the version, use `importlib.metadata.version("flask")` (described later). The code in this article is based on the patterns of the official documentation.

---

## **1. Why you go beyond a single file: the breakdown of the global `app` and circular imports**

Before talking about design, let me accurately understand **what breaks.** Flask's minimal form was this.

```python
# hello.py
from flask import Flask

app = Flask(__name__)


@app.route("/")
def index():
    return "Hello, World!"
```

This is enough for learning. The problem occurs the instant features grow and you carve views and models into separate files. Let me reproduce the typical "way it breaks."

```python
# 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 する）
```

Launch `app.py` and at the `import models` line, `models.py` runs `from app import app`. But at this point `app.py` is still mid-execution of the `import models` line, and **the module's initialization hasn't completed.** As a result, the name `app` isn't bound yet, and it crashes with `ImportError: cannot import name 'app' from partially initialized module 'app'` (a circular import).

This breakdown has 3 independent symptoms.

| Symptom | What happens | Root cause |
|---|---|---|
| **Can't swap settings in tests** | `app` is settled at import time, so there's no opening to switch to a test DB or test settings | `app` is immediately generated at the module top |
| **Can't have multiple configurations at once** | Can't make apps with different settings for production, staging, and test in parallel | `app` is a singleton within the module |
| **Induces circular imports** | Views/models import `app`, and the `app` side also imports them | The dependency direction has become bidirectional |

> ⚠️ **Anti-pattern**: keep adding features with the global `app` "because it works for now," and when a circular import appears, stopgap it by pushing the `import` statement into a function (a lazy import). This only hides the debt; the fact that dependencies draw a circle doesn't disappear. The application factory in the next section structurally cuts off this circle by **making the dependency direction one-way "from top to bottom."**

---

## **2. The application factory: deep dive into `create_app`**

The solution is the **application factory.** Stop generating `app` at the module top, and **assemble it inside a function and return it.** The caller gains complete control over "when, with which settings, and which parts loaded" the app is made.

### 2.1 The canonical form of the official tutorial

This is the canonical form of the factory the official Flask tutorial (Flaskr) shows. Every element has meaning, so let me read it line by line.

```python
# 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`**: switches the resolution base of config files to the **instance folder** (`app.instance_path`, by default `<project>/instance/`) rather than inside the package. This lets you place settings containing secrets outside the repository (§6.2).
- **② `from_mapping(...)`**: place here only the **safe default values** you can write directly in code. `SECRET_KEY="dev"` is a development dummy, on the premise that it's always overridden in production.
- **③④ the `test_config` branch**: **this branch is the heart of the factory.** If the argument is `None` (normal startup), read production settings from `instance/config.py`, and if an argument is passed (test), override with those settings. Tests can inject settings "without placing a real file."
- **⑤ `os.makedirs(app.instance_path, exist_ok=True)`**: **Flask doesn't auto-generate the instance folder.** If you place SQLite or uploads there, you need to explicitly create it in the factory. The official tutorial also includes this one line.

For startup, the `flask` CLI auto-detects the factory.

```bash
flask --app myapp run --debug
```

### 2.2 Auto-detection of the factory and passing arguments

`flask --app`, if the target module has a function named **`create_app` or `make_app`**, automatically calls it as the factory. When you want to pass arguments, you can write inside the parentheses as a **Python literal.**

```bash
# 引数なしのファクトリを自動検出
flask --app myapp run --debug

# ファクトリに文字列リテラル "dev" を渡す（括弧内は Python リテラルとして解釈）
flask --app 'hello:create_app("dev")' run
```

This format is the same in production Gunicorn. You can point directly at the factory like `gunicorn 'myapp:create_app()'`. The production-deploy details of Gunicorn's worker count, `ProxyFix`, and Dockerization are consolidated in the [production deployment guide](/blog/flask-deployment-gunicorn-docker-production-wsgi-guide).

### 2.3 Why the factory solves testability and multiple environments "at once"

The essential value of the factory is in **"delaying" and "parameterizing" app generation.**

- **Testability**: like `create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:"})`, you can generate an app with clean settings per test. Re-create the app in a pytest fixture and contamination of state between tests doesn't occur either (for testing details, see the [testing practical guide](/blog/flask-testing-pytest-test-client-fixtures-guide)).
- **Multiple environments**: just call the same `create_app` differently — production with environment-variable settings, test with the `test_config` argument. You don't need to branch code per environment.

> 💡 **The factory is not "a future extension" but "a current requirement"**: regarding the application factory as something "to introduce once scale grows" is wrong. **Testability is a requirement needed from the start**, and the factory is the minimal structure that meets it. Even in a small app, put in just the separation of `create_app` and `extensions.py` from the start — this is the starting point that doesn't generate debt (it doesn't contradict §7's YAGNI discipline. What should be split is Blueprints, and introducing the factory is a separate matter).

---

## **3. The `extensions.py` pattern: bare extensions → bind with `init_app`**

The key by which the factory cuts off circular imports is in **how you handle extension objects.** Generate an extension like Flask-SQLAlchemy or Flask-Migrate together with `app`, and you're back in the circular-import trap. The solution the official docs show is clear — **generate the extension in "a bare state unbound to any app," and bind it only by calling `init_app(app)` inside the factory.**

### 3.1 The structure of two-stage initialization

```python
# src/myapp/extensions.py — どのアプリにも束縛されていない「裸」の拡張
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()      # この時点では app を知らない
migrate = Migrate()    # この時点では app を知らない
```

```python
# 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
```

The point is that `db = SQLAlchemy()` is generated in `extensions.py` **independent of the app.** A model definition imports `db` with `from myapp.extensions import db`, but this `db` knows nothing of `app`, so it doesn't need to import `app`. **The dependency direction becomes one-way, `models → extensions`**, and the circle disappears.

### 3.2 Why this structurally resolves circular imports

Looking at the dependency graph, the power of two-stage initialization becomes clear.

| Approach | Dependency direction | Result |
|---|---|---|
| Global `app` + `db = SQLAlchemy(app)` | `models → app → models` | Circular (can't launch) |
| `extensions.py` + `db.init_app(app)` | `models → extensions`, `__init__ → extensions`, `__init__ → models` (in-function import) | One-way (sound) |

By `extensions.py` becoming "the bottom-most layer that depends on neither the app, the models, nor the views," all modules can safely import `extensions`. This is also a naive implementation of dependency inversion — **depending on an abstraction (the bare extension object) rather than a concrete (a specific app).**

### 3.3 One extension object can be reused across multiple apps

This design has one more important consequence beyond avoiding circular imports. **A single extension object `db` can be bound to multiple apps through `init_app`.** The production app and the test app can use the same `db` object, each with a different DB setting. This is why the factory (§2) and the extension pattern mesh — the factory makes multiple apps, and binds the same extension to each app with `init_app`.

> 💡 **`current_app` completes this design**: make the app in the factory and a situation arises where "you can't import `app` from the module top" (because it doesn't exist at import time). For an extension or view to access the current app's settings, use the `current_app` proxy instead of dragging `app` around. `current_app.config["..."]` resolves dynamically to "the app currently being processed," so it's fully consistent with the factory configuration. The mechanism of `current_app` / `g` / the application context is dug into in the [context thorough explanation](/blog/flask-application-request-context-g-current-app-guide). For the design of the persistence layer (SQLAlchemy 2.0's typed ORM), also read the [SQLAlchemy 2.0 practical guide](/blog/sqlalchemy-2-typed-orm-production-guide).

---

## **4. Blueprint thorough explanation: the unit of splitting and the pitfalls of the official spec**

Once the application factory has solved "how to make the app," next is "how to split features." That's the **Blueprint.** A Blueprint is "a registerable feature unit bundling routes, templates, static files, error handlers, and CLI commands." The Blueprint API was added in Flask 0.7.

### 4.1 Definition and registration: `name` is not the URL but the prefix of the endpoint

Let me crush the most important misconception first. **A Blueprint's `name` (the first argument) doesn't decide the URL. It prefixes the endpoint (the name used in `url_for`).**

```python
# 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")
```

```python
# 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")
```

The crux here is how `url_for` changes.

| Specification | endpoint | Generated URL (`url_prefix="/pages"`) |
|---|---|---|
| `url_for("simple_page.show", page="about")` | `simple_page.show` | `/pages/about` |

**The `name` (`simple_page`) is prefixed to the endpoint with a `.` separator, and the URL is prefixed with `url_prefix`** — these two are different things. Change `name` and the URL doesn't change, and change `url_prefix` and the endpoint doesn't change. Without understanding this separation, you'll endlessly get stuck emitting a `BuildError` with `url_for`.

> ⚠️ **Anti-pattern**: writing a URL path in `name`, like `Blueprint("/admin", __name__)`. `name` is the endpoint's namespace, not the URL. A `name` containing a slash is invalid. If you want to attach a URL, use `app.register_blueprint(admin, url_prefix="/admin")`. Similarly, omit the Blueprint name like `url_for("show")` and resolution fails when the view is in a different Blueprint. **Within the same Blueprint, you can reference relatively with `url_for(".show")` (a leading dot).**

### 4.2 Nesting Blueprints

Blueprints can be nested. Register a child to a parent Blueprint and register the parent to the app, and **the child inherits both the parent's `name` and `url_prefix`.**

```python
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)     # アプリに親を登録
```

As a result, the endpoint becomes `parent.child.create` and the URL becomes `/parent/child/create`.

```python
url_for("parent.child.create")   # → "/parent/child/create"
```

Note that both `name` and `url_prefix` are concatenated. Nesting with a subdomain using `subdomain=` works similarly too. Nesting straightforwardly expresses a hierarchy like "the admin panel has user management and billing management," but make it too deep and the endpoint name becomes huge. **Use 2 levels of nesting as a guideline**, and beyond that, it's realistic to consider splitting into a separate app (microservice).

### 4.3 Blueprint-local templates and static files: a treasure trove of pitfalls

A Blueprint can have its own templates and static files, but **the official-spec pitfalls are concentrated here.**

**Templates**: with `Blueprint("x", __name__, template_folder="templates")`, you can add a Blueprint-specific template folder. But you need to be careful about priority.

- The Blueprint's template folder is added at **a lower priority than the app's template folder.** That is, if there's a template with the same relative path, **the app side can override the Blueprint side** (a design leaving room for customization).
- If multiple Blueprints provide a template with the same relative path, **the first-registered Blueprint wins.**

> ⚠️ **The template-collision trap**: if multiple Blueprints each have a `templates/index.html`, the `index.html` of a later-registered Blueprint is **never resolved** (because the first-registered wins). To avoid this, namespace each Blueprint with a subfolder — like `templates/admin/index.html`, `templates/auth/index.html`. Namespacing the template's relative path with the Blueprint name is the only sure way to prevent collisions.

**Static files**: here is even more of a trap. With `Blueprint("admin", __name__, static_folder="static")`, you can have a static folder, but **a Blueprint without a `url_prefix` can't serve its own static folder.**

- The Blueprint's static files are served at `url_prefix + /static`, and the endpoint becomes `admin.static`.
- In a Blueprint without `url_prefix`, the app's `/static` wins, and **the Blueprint's static folder isn't even searched as a fallback.**

```python
# 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"
```

In practice, rather than scattering static files per Blueprint, **consolidating them into the app's single `/static` and bundling with a build tool (Vite, etc.)** is simpler to operate. Blueprint-local static is best limited to special cases like "distributing a fully independent feature unit plugin-style."

### 4.4 Blueprint error handlers and the "can't catch 404" exception

A Blueprint can register an error handler with `@bp.errorhandler(404)`, but **the most easily misunderstood pitfall is here.**

```python
@simple_page.errorhandler(404)
def page_not_found(e):
    # これは「無効な URL アクセス」では呼ばれない！
    return render_template("pages/404.html"), 404
```

**A Blueprint's `errorhandler(404)` is called only when `abort(404)` or `raise` occurs *inside that Blueprint's own view function*.** It's not called for access to a nonexistent URL (a routing failure). The reason is clear — **a Blueprint doesn't "own" the URL space.** Flask can't judge which Blueprint a request for `/nonexistent` should belong to, so an invalid-URL 404 can only be caught by an **app-level handler.**

When you want to register a handler effective for the whole app from a Blueprint, use `app_errorhandler`.

```python
# 自分の 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
```

| Decorator | Scope | Catches invalid-URL 404? |
|---|---|---|
| `@bp.errorhandler(404)` | Only `abort`/`raise` inside that Blueprint's own views | ❌ No |
| `@bp.app_errorhandler(404)` | The whole app (including invalid URLs) | ✅ Yes |
| `@app.errorhandler(404)` | The whole app | ✅ Yes |

The error-handler resolution order, bulk handling of `HTTPException`, and the details of JSON error design in an API are split into the [error-handling / observability guide](/blog/flask-error-handling-logging-observability-guide).

### 4.5 Blueprint CLI commands

A Blueprint can define `flask` CLI commands tied to that Blueprint with `@bp.cli.command(...)`. By default, they go under a group named after the Blueprint.

```python
# 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}")
```

```bash
# 既定では Blueprint 名（admin）のグループ配下に入る
flask admin create alice
```

The group name can be changed. With `Blueprint("admin", __name__, cli_group="other")` it goes under a different-named group, and with `cli_group=None` it's promoted to the top level as `flask create alice`. Giving independent operational command groups like `db migrate` per Blueprint keeps the CLI organized by feature and easier to operate.

---

## **5. The recommended package structure: a concrete example of the `src/` layout**

Let me land the design elements so far — the factory, `extensions.py`, Blueprints, Config — into one directory structure. This is the "pattern" of production Flask. Adopt the `src/` layout (placing the package under `src/`) and at test time it's imported as "an installed package," preventing the accident of "carelessly importing a file in the current directory."

```text
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
```

Let me make explicit the dependency direction in this structure — **all one-way, "from top to bottom."**

```text
__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）
```

As long as you keep this shape — `extensions.py` at the bottom layer, and no one imports `__init__.py` (`create_app`) — circular imports structurally don't occur. The "layer separation of `Router → Schema (the boundary) → Model`" in the API Blueprint's `schemas.py` is detailed in [designing a production REST API with marshmallow × Flask × SQLAlchemy](/blog/marshmallow-flask-sqlalchemy-rest-api-production-guide). This article's Blueprint handles "where," and that article handles "what inside."

> 💡 **Make `models/` and `blueprints/` packages (a directory + `__init__.py`)**: starting with a single `models.py` file is fine, but once models grow, promote it to a `models/` package and re-export each model in `models/__init__.py`. This keeps the import path `from myapp.models import User` stable while freely changing the internal file split. The stability of the import path directly connects to refactoring freedom.

---

## **6. Per-environment Config: a two-stage `from_object` + `from_prefixed_env`**

Combined with the factory's `test_config` branch (§2.1), prepare a mechanism to **switch settings per environment.** There's one principle you must not waver on in production — **don't write secrets (`SECRET_KEY`, DB passwords, API keys) in code.** This is the core of 12-factor and the minimum line of security.

### 6.1 The inheritance hierarchy of Config classes

`app.config.from_object(...)` loads **only uppercase attributes** as settings. Using this, express environment differences with `Base → Production / Test` inheritance.

```python
# 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:"
```

```python
# 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
```

> ⚠️ **The `from_object` trap: a class reference is "not instantiated"**: `from_object("myapp.config.ProductionConfig")` **reads the class's uppercase attributes as-is** and **does not instantiate the class.** That is, a setting value computed with `@property` isn't loaded (it's ignored as a `property` object). If you need a dynamically computed setting, **pass an instance rather than a class** (`from_object(ProductionConfig())`), or flow it in from an environment variable with `from_prefixed_env()`.

### 6.2 `from_prefixed_env` and the instance folder

`from_prefixed_env()` is a method **added in Flask 3.0**, optimal for the container / 12-factor era. It automatically loads environment variables starting with `FLASK_`, and the value is **typed with `json.loads`.**

```bash
# 本番の環境変数（コンテナの 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` is loaded not as a string but as `int 10485760`, and you can also assign to nested dictionary keys with `__` (a double underscore). Flask resolves the typical worry of "environment variables are all strings and hard to handle."

There are other loading interfaces for settings. Besides the `from_object` / `from_prefixed_env` / `from_pyfile` used in this article, there are `from_file(path, load=tomllib.load, text=False)` to read JSON/TOML, and `from_envvar` to read a path from a single environment variable. The overall picture of settings management is summarized in §4 of the [Flask production operation guide](/blog/flask-production-guide).

The **instance folder** (`instance/`, added in Flask 0.8) is "a settings location not committed to the repository" that becomes the resolution base of `from_pyfile("config.py")` when `instance_relative_config=True` (§2.1). **Always put `instance/` in `.gitignore`.** For the deep dive into security settings around Cookies, CSRF, and `SECRET_KEY`, see the [security implementation guide](/blog/flask-security-sessions-csrf-secure-cookies-guide).

### 6.3 Getting Flask's version

You sometimes want to output the framework's version in settings or a health check. **`Flask.__version__` was removed in Flask 3.0.** Instead, use the standard library's `importlib.metadata`.

```python
from importlib.metadata import version

flask_version = version("flask")   # "3.1.3" のような文字列
```

---

## **7. When to split: the "extract on the third" YAGNI discipline**

I've explained Blueprints and the src layout up to here, but **the most important is the judgment of "when to split."** Design skill is not the power to increase structure but **the power to discern the moment structure should be increased.**

You don't need to split finely into `blueprints/auth/`, `blueprints/api/`, and `blueprints/admin/` from the start. This is typical too-early abstraction, an act of prepaying cognitive load for requirements that don't exist yet. The discipline the author has held to in production is simple.

| Stage | Structure | Judgment |
|---|---|---|
| 1st feature | Write routes directly in `create_app` | No Blueprint needed yet |
| 2nd feature | Bundle into one Blueprint | "Want to split," but still hold |
| 3rd feature | Extract into Blueprints per concern | **Split for the first time here** |

This "**extract on the third**" is the same philosophy as DRY's "twice is coincidence, three times is a pattern." Two similar routes might be a coincidental match, but with three lined up you can have conviction that "this is an independent concern." By delaying the split until you have conviction, you avoid the risk of **splitting at the wrong boundary** (ending up re-splitting later).

> 💡 **Decide the unit of splitting by "the reason to change (SRP)"**: split Blueprints not by "URL prefix" but by "**what changes together when something changes.**" Things that change together when the auth logic changes go in `auth/`, and things that change together when the API schema changes go in `api/`. Gather code that changes for the same reason into one Blueprint, and separate code that changes for different reasons — this is the application of the single responsibility principle at the Blueprint granularity. The URL is a result, not the criterion for splitting.

But there's an exception. **Put in just the separation of `create_app` and `extensions.py` from the start, even with one feature** (§2.3). This is a separation to meet "the current requirement of testability," not "for a future extension." YAGNI applies to the subdivision of Blueprints, not to the introduction of the factory. Not mistaking this distinction is the key to avoiding both under-design (can't test) and over-design (split too much).

---

## **Summary: eliminate circular imports structurally with "design"**

The breakdown of a large Flask app comes not from a lack of Flask features but from a lack of structure. Let me re-list this article's discipline.

1. **Throw away the global `app`.** A module-top `app = Flask(__name__)` is the root of the three big breakdowns of untestable, single-configuration, and circular imports.
2. **Make the application factory (`create_app(test_config=None)`) the backbone.** With `instance_relative_config=True` and the `test_config` branch, solve testability and multiple environments at once.
3. **Generate extensions bare in `extensions.py` and bind them with `init_app(app)`.** Placing `extensions` at the bottom layer makes dependencies one-way and eliminates circular imports structurally. One extension is reusable across multiple apps.
4. **Split features with Blueprints.** Don't step on these official-spec pitfalls: `name` is the endpoint prefix, not the URL; an invalid-URL 404 can't be caught by a Blueprint's `errorhandler` and needs `app_errorhandler`; a Blueprint without `url_prefix` can't serve its static folder; for templates, the first-registered wins.
5. **Per-environment Config is a two-stage `from_object` (defaults) → `from_prefixed_env` (environment override).** Be careful that `from_object` doesn't instantiate the class, and drive secrets out of code with the instance folder and environment variables.
6. **Split with the "extract on the third" YAGNI discipline.** You may delay the subdivision of Blueprints, but put in just the separation of `create_app` and `extensions.py` from the start.

These aren't things "to introduce once scale grows" but **a design that pays off from the first few hundred lines.** Flask provides "only the core" and entrusts the structure to you. The minimal discipline that takes responsibility for that freedom is the application factory and Blueprints. For the map of the overall picture and the topics (context, testing, deployment, security, observability), go back to the [Flask production operation guide](/blog/flask-production-guide) for the overview.
