導入:current_app を初めて使うと、必ず一度はこのエラーに出会う
Flask を本番で扱うエンジニアが、ほぼ全員一度は踏むエラーがあります。
RuntimeError: Working outside of application context.
あるいは、
RuntimeError: Working outside of request context.
スクリプトの中で current_app.config["DATABASE"] を読もうとした、テストの外で request.json に触れた、バックグラウンドスレッドに request を渡した——いずれも「コンテキストが無い場所で、コンテキストに依存するオブジェクトへ触れた」という、たった一つの原因から来ています。逆に言えば、Flask の2 つのコンテキスト(アプリケーションコンテキストとリクエストコンテキスト)の仕組みを正確に理解すれば、このエラーは「なぜ起きたか」と「どう直すか」が即座に分かるようになります。
この記事は、Flask 本番運用ガイド の §5「コンテキスト」を、本番で必要な深さまで掘り下げるスポークです。current_app / g / request / session という 4 つのプロキシが「いつ使えて」「いつ使えないか」、その背後の contextvars + LocalProxy の仕組み、teardown_appcontext による資源の後始末、手動でコンテキストを push する場面、そしてバックグラウンドタスクとテストでの正しい扱いまでを、Flask 3.1 系の公式仕様に忠実に解説します。
筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを Python / Flask / SQLAlchemy / PostgreSQL で設計・実装し、API Gateway → ALB → ECS(Fargate) 上で本番運用してきました。マルチテナント SaaS では「いまのリクエストはどのテナントのものか」「DB セッションをリクエストの寿命にどう縛るか」がコンテキスト設計そのものであり、ここで示すパターンはその実戦で磨いたものです。
💡 この記事で扱うバージョン:Flask 3.1 系(現行安定版)を前提とします。コンテキストの実装はバージョン間で安定していますが、
gがリクエストコンテキストからアプリケーションコンテキストへ移った(0.10)、teardown_appcontextが追加された(0.9)、copy_current_request_context/appcontext_pushedが追加された(0.10)といった「いつ何が変わったか」も要所で触れます。コードは公式ドキュメントのパターンに基づきます。
1. メンタルモデル:Flask には「2 つのコンテキスト」がある
最初に、この記事全体の地図を頭に入れます。Flask が 1 つのリクエストを処理する間、内部では 2 種類のコンテキストが積まれています。
| コンテキスト | 保持するもの | このコンテキストに属するプロキシ | いつ push されるか |
|---|---|---|---|
| アプリケーションコンテキスト(app context) | アプリ単位のデータ | current_app / g | リクエスト処理時・CLI コマンド実行時に自動 |
| リクエストコンテキスト(request context) | リクエスト単位のデータ | request / session | リクエスト処理時に自動(その際 app context も先に push) |
公式の定義はこうです——アプリケーションコンテキストは「リクエスト、CLI コマンド、その他のアクティビティの間、アプリケーションレベルのデータを追跡する。アプリを引き回す代わりに、current_app と g プロキシ経由でアクセスする」。リクエストコンテキストは「リクエストレベルのデータを追跡し、request と session プロキシを提供する」。
1.1 なぜ「コンテキスト」などという仕組みが必要なのか
ここがすべての出発点です。素朴に考えれば、ビュー関数から設定を読みたいなら app を import すればよさそうに見えます。
from myapp import app # ← これが本番で破綻する
@some_blueprint.route("/")
def index():
return app.config["SOMETHING"]
しかしこれは 2 つの理由で破綻します。
- 循環 import を起こす。
appは普通ビューやモデルを import して組み立てます。そのビューがappを import し返すと、依存が循環します。 - app-factory 構成では、そもそも import できる
appが存在しない。アプリケーションファクトリではappはcreate_app()の中で生成されるため、モジュールトップに「import 可能なグローバルapp」がありません。再利用可能な Blueprint も、どのアプリに登録されるか定義時には分かりません。
この 2 つを同時に解決するのが current_app です。公式の言葉では——「current_app はいま処理中のアクティビティを担当しているアプリを指す。これはプロキシであり、アプリケーションコンテキストが push されているときだけ利用できる」。app という名前のグローバルを撲滅し、「いま動いているアプリ」を実行時に解決する。これがコンテキストの存在理由です。
💡 寿命のイメージ:リクエスト処理の開始時に、Flask はリクエストコンテキストと(必要なら)アプリケーションコンテキストを push し、終了時にリクエストコンテキストを pop してからアプリケーションコンテキストを pop します。だから「アプリケーションコンテキストの寿命 ≒ リクエストの寿命」です。この入れ子構造(外側が app context、内側が request context)が、後で
teardownの順序を理解する鍵になります。
2. current_app:アプリへのプロキシを正しく使う
current_app は、flask から import するプロキシオブジェクトです。アクセスした瞬間に「いま push されているアプリケーションコンテキストのアプリ」へ転送されます。
from flask import current_app
@bp.get("/version")
def version():
# app を import せずに、いま動いているアプリの設定へ届く
return {"version": current_app.config["API_VERSION"]}
押さえるべき性質は 3 つです。
- プロキシである:
current_app自体は本物のFlaskインスタンスではなく、その時々のアプリへの「窓口」です。 - コンテキストが push されているときだけ使える:app context が無い場所(モジュールのトップレベル、別スレッド、コンテキスト外のスクリプト)で触れると
RuntimeError: Working outside of application context.になります(§6 で対処)。 - 手動で制御できる:リクエスト外でアプリ参照が必要なら
with app.app_context():で自分で push します(§6.1)。
2.1 _get_current_object():プロキシの「中身」を取り出す
プロキシは普段は透過的ですが、プロキシのままでは困る場面が 2 つあります。isinstance() での型チェックと、シグナルの送信者として実体を渡したいときです。プロキシは Flask のサブクラスではないので isinstance(current_app, Flask) は意図通りになりません。実体が必要なら _get_current_object() を呼びます。
from flask import current_app
# プロキシではなく、本物の Flask インスタンスが欲しいとき
app = current_app._get_current_object()
some_signal.send(app) # シグナルの sender には実体を渡す
assert isinstance(app, Flask) # 実体なので型チェックも通る
⚠️ アンチパターン:
current_appを変数に代入して、コンテキストが切れた後(別スレッド・別タスク)で使い回す。current_appは「いまのコンテキスト」に束縛されるプロキシなので、コンテキストを越えて持ち出すと壊れます。コンテキストを越えてアプリを渡したいなら、current_app._get_current_object()で実体を取り出してから渡してください。
3. g:アプリケーションコンテキストに縛られた名前空間
g は公式の定義で「アプリケーションコンテキストの間、データを保存できる名前空間オブジェクト」です。current_app と同じくプロキシで、同じアプリケーションコンテキストに属します。
ここで最も誤解されるのが g の名前です。公式はわざわざ注記しています(そのまま引用します)。
The
gname stands for "global", but that is referring to the data being global within a context. The data ongis lost after the context ends, and it is not an appropriate place to store data between requests. Use thesessionor a database to store data across requests.
つまり——g の "g" は global だが、それは「コンテキスト内でグローバル」という意味に過ぎない。g のデータはコンテキスト終了で失われ、リクエストをまたぐ保存場所としては不適切である。リクエスト間で保存したいなら session か DB を使え、と。
⚠️
gをキャッシュ/グローバル変数だと思った瞬間に事故る:「ログイン中のユーザーをg.userに入れておけば次のリクエストでも使える」——これは間違いです。gは各コンテキスト(≒各リクエスト)ごとに新品で、リクエストが終われば消えます。gに適しているのは「このリクエストの中で何度も使う、リクエストスコープの値」(DB 接続・解決済みの現在ユーザー・現在テナント)であって、リクエスト間で共有したい状態ではありません。
3.1 正準パターン:get_db() + teardown_appcontext
g の最も重要な用途が、「リクエスト中は同じ DB 接続を使い回し、コンテキスト終了時に必ず閉じる」パターンです。公式ドキュメントのコードをそのまま示します。
from flask import g
def get_db():
if 'db' not in g:
g.db = connect_to_database()
return g.db
@app.teardown_appcontext
def teardown_db(exception):
db = g.pop('db', None)
if db is not None:
db.close()
公式の説明はこうです——「リクエストの間、get_db() の呼び出しは毎回同じ接続を返し、リクエストの終わりに自動的に閉じられる」。仕組みを分解すると:
if 'db' not in g:で初回だけ接続を生成し、g.dbに保存する。'x' in gはgの便利なメンバ判定です。- 2 回目以降の
get_db()は、すでにg.dbがいるのでそれを返す。リクエスト内で接続が1 本に保たれる。 teardown_appcontextに登録したteardown_dbが、コンテキスト pop 時に呼ばれ、g.pop('db', None)で取り出して閉じる。
g のメンバ操作は dict に似た API を持ちます。本番で多用するのは次の 3 つです。
| 操作 | 意味 | 用途 |
|---|---|---|
'db' in g | g に db 属性があるか | 初回生成の判定 |
g.get('db') | 無ければ None(既定値も指定可) | 「あれば使う」読み取り |
g.pop('db', None) | 取り出して削除(無ければ既定値) | teardown での後始末 |
3.2 LocalProxy で get_db() を「変数のように」見せる(任意)
get_db() を毎回呼ぶのが煩わしければ、Werkzeug の LocalProxy でラップして、関数呼び出しを変数アクセスのように見せられます。
from werkzeug.local import LocalProxy
db = LocalProxy(get_db) # db に触れるたび get_db() が呼ばれる
# 以降は current_app のように db を使える
def find_user(user_id):
return db.execute("SELECT * FROM users WHERE id = ?", (user_id,))
これは「あってもなくてもよい」糖衣ですが、current_app / g / request がすべて同じ LocalProxy 機構で実装されていることを理解するのに役立ちます。次節でその機構そのものを見ます。
4. request / session:コンテキストローカルの正体
request(受信リクエスト)と session(署名付き Cookie セッション)は、リクエストコンテキストに属するプロキシです。
from flask import request, session
@bp.post("/login")
def login():
email = request.json["email"] # 受信ボディ
session["user_id"] = authenticate(email) # 次のリクエストへ持ち越す(Cookie)
return {"ok": True}
ここで多くの記事が「request はスレッドローカルだ」と説明しますが、それは Flask 3.1 では正確ではありません。この区別が本番の async / 並行設計を左右するので、正確に押さえます。
4.1 「context local」であって「thread local」ではない
公式の説明はこうです——「ワーカー(サーバーに応じてスレッド・プロセス・コルーチン)は一度に 1 つのリクエストしか処理しないため、リクエストデータはそのワーカーにとってそのリクエストの間グローバルとみなせる。Flask はこれを "context local" と呼ぶ」。そして決定的な一文:
Context locals are implemented using Python's
contextvarsand Werkzeug'sLocalProxy.
つまりコンテキストローカルは、Python 標準の contextvars と Werkzeug の LocalProxy で実装されています。これを「スレッドローカル」と呼んではいけません。contextvars こそが、async def ビューやコルーチンでも request が正しいリクエストを指す理由だからです。スレッドローカルだと、1 スレッド上で複数コルーチンが走る async 環境では「どのリクエストの request か」が混線します。contextvars はコンテキスト(コルーチン)単位で値を持つので、それが起きません。
[プロキシ] [機構] [実体]
request ──→ LocalProxy ──┐
session ──→ LocalProxy ├─ contextvars ──→ いまのコンテキストの
current_app ─→ LocalProxy │ RequestContext / AppContext
g ──→ LocalProxy ──┘
4.2 だから「request を別スレッドへ渡す」と壊れる
この仕組みの直接的な帰結が、request を別スレッドに渡してはいけないという鉄則です。公式は明言します——「request を別スレッドに渡すことはできない。別スレッドは異なるコンテキストを持つ」。
import threading
from flask import request
@bp.get("/bad")
def bad():
def worker():
# ❌ 別スレッドには「このリクエストのコンテキスト」が無い → 壊れる
print(request.path)
threading.Thread(target=worker).start()
return "started"
worker スレッドは別の contextvars コンテキストで動くため、request は「このリクエスト」を指しません。バックグラウンドにリクエスト情報を持ち出したい正しい方法は §7 の copy_current_request_context です。
💡 push されるのは request だけではない:リクエストコンテキストが push されるとき、そのアプリ用のアプリケーションコンテキストがまだトップに無ければ、Flask は先にアプリケーションコンテキストを push します。だから「リクエストを処理しているなら
current_appもgも必ず使える」のです。逆は成り立ちません——CLI コマンドや手動 push では app context だけがあり、requestはありません。
5. teardown_appcontext と teardown_request:資源を確実に後始末する
g に積んだ DB 接続のように、コンテキストの終わりで必ず解放したい資源があります。Flask はそのためのフックを 2 つ提供します。
| フック | 呼ばれるタイミング | 典型用途 |
|---|---|---|
teardown_request(f) | リクエストコンテキスト pop 時 | リクエスト固有の後始末 |
teardown_appcontext(f) | アプリケーションコンテキスト pop 時(各リクエストの request ctx の後、CLI コマンド終了時、手動 push の終了時) | DB 接続など「リクエストでも CLI でも閉じたい」資源 |
5.1 呼ばれる順序と「例外があっても呼ばれる」保証
レスポンスを返した後、コンテキストは pop され、teardown_request() → teardown_appcontext() の順で呼ばれます(外側=app context が後)。重要なのは、上のコードで未処理例外が送出されても、これらの teardown は呼ばれるという保証です。だから「接続を確実に閉じる」場所として信頼できます。
ただし注意があります——公式いわく「他のリクエストディスパッチの処理が先に実行されたという保証は無い」。teardown は「何があっても最後に資源を返す」場所であって、「正常系の後処理を書く」場所ではありません。
5.2 teardown 関数は「絶対に例外を投げてはいけない」
公式の最重要ルール(そのまま守ってください):
- teardown 関数は例外を送出してはならない。teardown 中の例外は後始末の連鎖を壊します。
- teardown 関数の戻り値は無視される。何かを return しても意味はありません。
- teardown 関数は引数として例外(あれば)を受け取りますが、それは「ハンドリングのため」ではなく「ログ等の参考のため」です。
@app.teardown_appcontext
def teardown_db(exception):
# exception は teardown のトリガになった例外(無ければ None)
db = g.pop("db", None)
if db is not None:
try:
db.close()
except Exception:
# ⚠️ teardown の中で例外を外へ漏らさない。ログに留める
current_app.logger.exception("failed to close db connection")
💡 CLI コマンドでも
teardown_appcontextは走る:@app.cli.command()で登録したコマンドの実行時、Flask はアプリケーションコンテキストを push し、コマンド終了時に pop します。このときteardown_appcontextも呼ばれるので、get_db()パターンは Web リクエストでも CLI バッチでも同じように動きます。これが「teardown_requestではなくteardown_appcontextに DB 後始末を置く」理由です。リクエストにしか無いteardown_requestだと、CLI バッチで接続が閉じられません。
6. 手動でコンテキストを push する:スクリプト・初期化・テスト
Flask が自動で push してくれるのは「リクエスト処理時」と「CLI コマンド実行時」だけです。それ以外——初期化スクリプト・cron バッチ・テストの一部——では、自分でコンテキストを push する必要があります。ここを理解すると、冒頭の 2 つの RuntimeError が完全に制御下に入ります。
6.1 app.app_context():アプリケーションコンテキストを手動 push
DB 初期化やスタンドアロンスクリプトのように「リクエストは無いがアプリ参照(current_app / g)が要る」場面では、with app.app_context(): で push します。公式の正準形:
with app.app_context():
init_db()
このブロックの中でなら current_app も g も使えます。ブロックを抜けると pop され、teardown_appcontext も呼ばれます。これを忘れて、コンテキスト外で current_app に触れると:
RuntimeError: Working outside of application context.
**正しい対処は「その処理を with app.app_context(): で囲む」**ことです。flask shell を使うと、対話セッションで自動的に app context が push されるので、デバッグ時に便利です。
6.2 app.test_request_context():リクエストコンテキストを手動 push(テスト用)
request や session に依存するコードを、実サーバーを立てずにテストしたいときは test_request_context() を使います。これはダミーのリクエストコンテキストを push します。
def test_login_reads_email():
app = create_app({"TESTING": True})
with app.test_request_context("/login", method="POST", json={"email": "a@example.com"}):
# このブロック内では request / session が使える
assert request.json["email"] == "a@example.com"
リクエストコンテキストが無い場所で request に触れると:
RuntimeError: Working outside of request context.
公式の指針はこうです——「これは通常、アクティブなリクエストを前提とするコードをテストするときにだけ起きるべきだ」。だから対処は文脈で分かれます。
RuntimeError: Working outside of request context. の発生場所 | 正しい対処 |
|---|---|
| テストコードの中 | test_request_context() で push する。または test_client() 経由でリクエストする |
| テスト以外(通常のアプリコード) | そのコードをビュー関数の中へ移す。リクエスト外で request を読もうとしているのが設計の誤り |
公式も「テスト以外でこのエラーを見たら、そのコードをビュー関数の中へ移せ」と述べています。test_client / test_request_context を使ったテストの組み立て方は、テスト実践ガイド で詳説しています。
💡 2 つの
RuntimeErrorの見分け方:エラーがapplication contextならcurrent_app/gに触れた、request contextならrequest/sessionに触れた、というだけのことです。前者はapp_context()、後者はtest_request_context()(テスト)かビュー関数への移動(本番コード)で直ります。エラーメッセージがどちらのコンテキストを指しているかを読むだけで、原因の半分は特定できます。
7. バックグラウンドタスク:copy_current_request_context
§4.2 で見たとおり、request を素のスレッドに渡すと壊れます。では、リクエスト処理の途中で重い処理をバックグラウンドへ逃がしつつ、その中で request / session を参照したい場合はどうするか。Flask は copy_current_request_context(0.10 で追加)を用意しています。これは「いまのリクエストコンテキストを、バックグラウンドで動く関数のためにコピーして紐付ける」デコレータです。公式の例(gevent を使ったもの):
import gevent
from flask import copy_current_request_context
@app.route('/')
def index():
@copy_current_request_context
def do_some_work():
# ここでは flask.request / flask.session にアクセスできる
...
gevent.spawn(do_some_work)
return 'Regular response'
@copy_current_request_context を付けないと、gevent.spawn で起動した do_some_work はアプリ/リクエストオブジェクトを一切見られません(別コンテキストだから)。付けることで、起動時点のリクエストコンテキストがコピーされ、バックグラウンド側でも request が正しいリクエストを指します。
⚠️ 公式の警告:コンテキストのコピーより「データを渡す」を優先する。
copy_current_request_contextは便利ですが、公式は明確に注意しています。
- 可能なら、必要なデータを引数で渡す方が安全(コンテキスト全体を持ち回らない)。
- バックグラウンドに渡す前にリクエストボディを読み終えておくこと(後から読もうとすると、親リクエストが既に閉じている可能性がある)。
sessionの操作は親(ビュー)側で行うこと。つまり
copy_current_request_contextは「どうしてもコンテキストごと必要な場合の最後の手段」であり、第一選択は「requestから必要な値を抜き出して、プレーンな引数として渡す」設計です。マルチテナント SaaS でも、バックグラウンドのインポート処理にはtenant_idのような値を渡すのが基本で、コンテキストのコピーは例外的にしか使いません。
8. シグナル:appcontext_pushed でテストに資源を仕込む(簡潔に)
Flask は Blinker ベースのシグナルで、コンテキストのライフサイクルにフックできます。コンテキスト関連のシグナルは 3 つです。
| シグナル | タイミング |
|---|---|
appcontext_pushed | アプリケーションコンテキストが push された直後(0.10 で追加) |
appcontext_tearing_down | コンテキストが pop される直前 |
appcontext_popped | コンテキストが pop された後 |
最も実用的なのが appcontext_pushed です。ユニットテストで、コンテキストが立ち上がった瞬間に g へリソース(テスト用ユーザーなど)を仕込むのに使えます。公式の例:
from contextlib import contextmanager
from flask import appcontext_pushed, g
@contextmanager
def user_set(app, user):
def handler(sender, **kwargs):
g.user = user
with appcontext_pushed.connected_to(handler, app):
yield
with user_set(app, some_user): のブロックの中では、新しく push される各コンテキストで g.user が some_user にセットされます。本番コードを汚さずに、テスト時だけ g の状態を差し替えられるのが利点です。可観測性の文脈では、has_request_context() をログフォーマッタで使い「リクエスト中なら request.url を、そうでなければ省く」といった分岐も定番です(エラー処理・可観測性ガイド を参照)。
💡
has_request_context()/has_app_context()は「いまコンテキストがあるか」を例外を出さずに確かめる関数です。ログフォーマッタやユーティリティのように「リクエスト中にも CLI からも呼ばれうる」コードで、requestに触れる前にガードするのに使います。if has_request_context(): ...と書けば、CLI 実行時にWorking outside of request contextを踏みません。
9. 本番例:リクエストスコープの DB セッションと「現在テナント」を g に載せる
ここまでの部品を、実際のマルチテナント B2B SaaS の形に組み上げます。やりたいことは 2 つです。
- DB セッションをリクエストの寿命に縛る(リクエスト中は 1 セッション、終了時に確実にクローズ)。
- 「いまのリクエストはどのテナントか」を一度だけ解決し、
gに載せて以降使い回す。
# db.py — リクエストスコープの DB セッション
from flask import g, current_app
from werkzeug.local import LocalProxy
from sqlalchemy.orm import Session
def _get_session() -> Session:
if "db_session" not in g:
# current_app 経由でエンジンを取得(app を import しない)
g.db_session = current_app.config["SESSION_FACTORY"]()
return g.db_session
# LocalProxy で「変数のように」使えるセッション
db_session = LocalProxy(_get_session)
def init_teardown(app):
@app.teardown_appcontext
def close_session(exception):
session = g.pop("db_session", None)
if session is None:
return
try:
# 例外で終わったならロールバック、正常ならコミットは
# ビュー側で済ませる方針なら、ここは確実な close に徹する
if exception is not None:
session.rollback()
finally:
session.close() # ← teardown は「閉じる」を最優先で確実に
# tenancy.py — 現在テナントを g に解決する
from flask import g, request, abort
def current_tenant():
if "tenant" not in g:
tenant_id = request.headers.get("X-Tenant-ID")
if not tenant_id:
abort(400, description="X-Tenant-ID header is required")
tenant = db_session.get(Tenant, tenant_id) # 上の db_session を利用
if tenant is None:
abort(404, description="unknown tenant")
g.tenant = tenant # このリクエスト内で 1 回だけ解決し、以降使い回す
return g.tenant
ビュー側はこれらを「変数のように」使うだけです。app の import も、セッションの引き回しも、テナント解決の重複もありません。
@bp.get("/orders")
def list_orders():
tenant = current_tenant() # 初回だけ解決、2 回目以降は g からヒット
orders = db_session.scalars(
select(Order).where(Order.tenant_id == tenant.id)
).all()
return {"data": [o.to_dict() for o in orders]}
この設計が効く理由を、コンテキストの観点で整理します。
gがリクエストスコープであることが、「テナントをリクエスト中 1 回だけ解決する」を自然に表現する。リクエストが終わればg.tenantは消えるので、別テナントのリクエストへ漏れない。teardown_appcontextが例外時にも呼ばれることが、「どんな終わり方をしてもセッションを必ず閉じる」を保証する。接続リークは本番で最も静かに効いてくる障害なので、ここを teardown に置くのは設計上の要です。current_app経由でエンジンを取ることが、このdb.pyをどのアプリにも import 依存させず、テストでは別のセッションファクトリを持つアプリに差し替え可能にする。
⚠️
gにテナントを載せる設計の落とし穴:g.tenantはリクエスト間で共有されないことが安全性の根拠です。もし「パフォーマンスのため」とモジュールグローバルにテナントをキャッシュしようものなら、別テナントのリクエストに前のテナントが漏れる——マルチテナントで最悪のデータ混線になります。テナントやユーザーのようなリクエスト固有・かつ取り違えが許されない値は、必ずコンテキストスコープ(g)に置いてください。「速くするためのグローバルキャッシュ」がセキュリティ境界を壊す典型例です。SQLAlchemy 側のセッション/エンジン設計の詳細は SQLAlchemy 2.0 実践ガイド を併読してください。
まとめ:コンテキストは「アプリを引き回さない」ための装置である
Flask の 2 つのコンテキストは、突き詰めれば**「app というグローバルを撲滅し、いま動いているアプリ/リクエストを実行時に解決する」**ための装置です。この記事の要点を再掲します。
- 2 つのコンテキスト——
current_app/gはアプリケーションコンテキスト、request/sessionはリクエストコンテキスト。リクエスト処理時と CLI 実行時に Flask が自動 push する。 current_appは循環 import と app-factory 問題を同時に解く唯一の正しいアプリ参照手段。型チェックやシグナルには_get_current_object()で実体を取り出す。gはコンテキスト内グローバルであってリクエスト間の保存先ではない(公式の Note を厳守)。get_db()+teardown_appcontextがリクエストスコープ資源の正準パターン。- コンテキストローカルは
contextvars+LocalProxyで実装され、thread-local ではない。だから async でも正しく動き、requestを別スレッドへ渡すと壊れる。 teardown_appcontextは例外時にも呼ばれ、戻り値は無視され、自身は例外を投げてはならない。CLI でも走るので DB 後始末はここに置く。Working outside of application/request contextは「コンテキストの無い場所でプロキシに触れた」サイン。app_context()(スクリプト/初期化)、test_request_context()(テスト)、あるいはビュー関数への移動で直す。- バックグラウンドは
copy_current_request_context——ただし第一選択は「必要なデータを引数で渡す」設計。
コンテキストを正しく理解すると、Flask アプリの「どこから何が使えるか」が一枚の地図になります。current_app で設定へ、g でリクエストスコープ資源へ、request / session で入力とセッションへ——アプリを一切引き回さずに届く。この明示性こそ、適切に設計された Flask の強さです。全体像と他の設計対象(構成・設定・エラー処理・デプロイ・テスト)は Flask 本番運用ガイド に戻って俯瞰してください。