Python UnboundLocalError: local variable referenced before assignment の原因と解決方法【global/nonlocal】

Python UnboundLocalErrorとは?ざっくりと3行で
  • Python UnboundLocalErrorは、関数の中で値を代入する前のローカル変数を参照したときに発生するエラーだ。
  • 関数内で一度でも代入しているとその名前はローカル変数として扱われるのが原因で、外側の同名変数を読んでいるつもりでも実は未代入のローカル変数を読んでいる、というのがほとんどのケースだ。
  • 外側の変数を更新したいならglobalnonlocalを宣言し、分岐で代入し忘れているなら関数の先頭で初期値を入れておけばいい。
Python UnboundLocalErrorで関数内のカウンタ更新が失敗し、globalの1行で解決するまでを描いた4コマ漫画
①カウンタを足すだけの関数が突然真っ赤になり開発者が固まる。②デプロイ太郎が関数内で代入したから局所変数扱いだと指摘する。③globalを1行足すとテストが緑に変わり解決する。④本当は引数と戻り値のほうが安全だと教訓で締める。

この4コマは、Python UnboundLocalErrorが変数の値ではなくスコープ判定で起きるという本質を描いています。カウンタを1つ増やすだけの単純なコードでも、関数の中にcount = count + 1という代入があると、Pythonはコンパイル時にcountを丸ごとローカル変数と決めつけます。右辺で読もうとした瞬間、その局所変数はまだ値を持っていないため、代入前参照として弾かれるわけです。

実務でこのエラーを軽視すると、開発中は問題なく見えても、特定の分岐や例外時にだけ関数が落ちる時限爆弾になりかねません。カウンタや集計値、ステータスフラグのように関数をまたいで持ち回る値で頻発するため、バッチ処理や集計APIで本番障害につながる恐れがあります。4コマの結末が示すように、安易なglobal頼みではなく、引数で受け取り戻り値で返す設計に寄せることが、再発を防ぐ一番の近道になります。

Python UnboundLocalErrorの基本情報

Python UnboundLocalErrorは、関数やメソッドの内部でローカル変数として扱われる名前を、値が束縛される前に参照したときに送出される例外です。NameErrorのサブクラスであり、変数そのものが存在しないわけではなく、そのスコープ内でまだ値が割り当てられていない状態を指します。

項目内容
エラーメッセージUnboundLocalError: local variable ‘x’ referenced before assignment(Python 3.10以前)/ UnboundLocalError: cannot access local variable ‘x’ where it is not associated with a value(Python 3.11以降)
発生する言語・環境Python 2系・3系すべて(CPython 3.8〜3.13で確認)。関数・メソッド・内包表記などローカルスコープを持つ場所で発生
エラーの意味局所変数 ‘x’ が値を割り当てられる前に参照された、という意味。NameErrorのサブクラス
主な原因関数内で代入している名前を、その代入より前に読んでいる。多くは外側の変数を更新するつもりの書き方
解決の基本方針まず、その名前が関数内のどこかで代入されているかを確認する。外側の変数を使うならglobalnonlocal、分岐漏れなら先頭で初期化する
Python UnboundLocalErrorについて解説するITKAGYO運営者デプロイ太郎のアイコン画像

変数に値を入れているのにエラー、という状況はかなり混乱しますよね。現場でもよく聞くエラーなので、落ち着いてスコープの仕組みから追っていきましょう。

なぜPython UnboundLocalErrorは発生するのか?

Python UnboundLocalErrorは値の有無ではなくスコープの決定タイミングで起きます。関数内に代入文が1つでもあると、Pythonはその関数を読み込む段階で名前をローカル変数と確定させ、外側の同名変数は一切見に行きません。

Python UnboundLocalErrorの原因は、発生する場所ごとに分けると理解が早まります。ここでは、外側変数の更新・分岐での代入漏れ・クロージャの3つに分類し、それぞれ発生コードと修正コードを追いかけます。いずれも共通するのは、関数内で名前が代入されているのに、その前に読んでしまっている点です。

原因パターン1:グローバル変数を関数内で更新しようとした

最も多いのが、関数の外にある変数を関数内で増やそうとするケースです。Python 3.12で以下のコードを実行すると、代入前参照になります。

# Python 3.12
count = 0  # グローバル変数

def increment():
    count = count + 1  # 関数内で代入があるため count はローカル変数扱い
    return count       # 右辺の count はまだ未代入 → UnboundLocalError

increment()

関数incrementの中にcount = ...という代入があるため、Pythonはcount関数全体でローカル変数と判断します。その結果、右辺のcountは外側の0ではなく未代入のローカル変数を指し、参照した瞬間にエラーになります。外側の変数を書き換えたい意図を明示するにはglobal宣言を使います。

# Python 3.12
count = 0

def increment():
    global count       # グローバル変数 count を使うと宣言
    count = count + 1  # これで外側の count を更新できる
    return count

increment()  # 1 が返る

原因パターン2:if分岐の一部でしか代入していない

条件を満たしたときだけ変数を代入し、満たさない経路で読んでしまう書き方も定番です。次のコードは引数によってはエラーになります。

# Python 3.12
def get_status(score):
    if score >= 60:
        result = "合格"   # 60未満のときは result が作られない
    return result         # score が60未満だと UnboundLocalError

get_status(50)

引数が50のときifブロックを通らないため、resultはどこでも代入されないままreturnに到達します。すべての経路で必ず代入されるように、関数の先頭で初期値を与えておくのが安全な直し方です。

# Python 3.12
def get_status(score):
    result = "不合格"     # 先に初期値を代入しておく
    if score >= 60:
        result = "合格"
    return result

get_status(50)  # "不合格" が返る

原因パターン3:ネスト関数で外側の変数を書き換えた

内側の関数から外側の関数の変数を更新しようとした場合も、同じ理屈で発生します。クロージャでカウンタを持たせる典型例を見てみましょう。

# Python 3.12
def make_counter():
    total = 0
    def add(x):
        total += x      # add 内で代入があるため total はローカル扱い
        return total    # 右辺の total は未代入 → UnboundLocalError
    return add

counter = make_counter()
counter(5)

total += xtotal = total + xの省略形なので、内側のaddにとってtotalはローカル変数になります。外側の関数の変数を書き換えたいときはglobalではなくnonlocalを宣言します。

# Python 3.12
def make_counter():
    total = 0
    def add(x):
        nonlocal total  # 外側の関数の total を使うと宣言
        total += x
        return total
    return add

counter = make_counter()
counter(5)  # 5 が返る
Python UnboundLocalErrorの原因パターンを説明するデプロイ太郎のアイコン画像

パターン1のグローバル更新は本当に多いです。関数の中に代入があるかどうか、まずそこを指差し確認する癖をつけると一気に見つけやすくなりますよ。

Python UnboundLocalErrorはバージョンで表示が変わる?

Python UnboundLocalErrorのメッセージ文言は、Python 3.11で読みやすい表現に刷新されました。コピペ検索でヒットしないときはバージョン差が原因のこともあるため、実行環境の系統を押さえておくと調査が速くなります。

実行環境表示されるエラーメッセージ
Python 3.10 以前UnboundLocalError: local variable ‘x’ referenced before assignment
Python 3.11 以降UnboundLocalError: cannot access local variable ‘x’ where it is not associated with a value

文言は変わっても、送出される例外クラスは同じUnboundLocalErrorであり、NameErrorのサブクラスである点も変わりません。したがってexcept NameError:で捕捉できる挙動はどのバージョンでも共通です。

FlaskやDjangoでPython UnboundLocalErrorが出るのはなぜ?

Djangoでの発生パターン

Django UnboundLocalErrorは、ビュー関数の中でリクエストの種類によって変数の代入が分岐するときに起きがちです。request.methodPOSTのときだけ変数を用意し、GETで同じ変数をrenderに渡すと、代入前参照になります。フォームの初回表示と送信処理を1つの関数にまとめたときの定番パターンです。

# Django 5.x のビュー
from django.shortcuts import render

def profile_view(request):
    if request.method == "POST":
        message = "更新しました"   # POST のときだけ代入
    # GET でアクセスすると message が未定義のまま
    return render(request, "profile.html", {"message": message})  # UnboundLocalError

# 修正:関数の先頭で初期値を用意する
def profile_view_fixed(request):
    message = ""                    # どの経路でも必ず代入される
    if request.method == "POST":
        message = "更新しました"
    return render(request, "profile.html", {"message": message})
Djangoのビューでは、テンプレートに渡す変数を関数の冒頭でまとめて初期化しておくと、GET/POSTの分岐漏れによるUnboundLocalErrorを防げます。

Flaskでの発生パターン

Flask UnboundLocalErrorは、tryブロックの中だけで変数へ代入し、例外時にexceptを通ってから同じ変数を返すルートで多発します。外部APIやデータベースへのアクセスが失敗した経路で、結果を格納する変数が未代入のまま参照されるためです。

# Flask 3.x のルート
from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/data")
def get_data():
    try:
        result = fetch_from_api()      # 例外が起きると result は未代入
    except Exception:
        app.logger.error("API 呼び出しに失敗")
    return jsonify(result)             # except を通ると UnboundLocalError

# 修正:try の前に初期値を置くか、except 内で return する
@app.route("/data-fixed")
def get_data_fixed():
    result = None                      # 先に初期化
    try:
        result = fetch_from_api()
    except Exception:
        app.logger.error("API 呼び出しに失敗")
    return jsonify(result)
例外処理では、成功時にしか代入しない変数を try の外側で初期化しておくのが鉄則です。ログだけ出して処理を続ける設計のときに特に効いてきます。

変数名別に見るPython UnboundLocalErrorの対処法は?

Python UnboundLocalErrorは、メッセージに含まれる変数名から状況を推測できます。実務で検索されやすい2つの型について、原因と直し方を絞って示します。

referenced before assignment がループ内で出る場合

集計用の変数を用意する前に、ループの中で累算してしまうケースです。合計を求めるコードでよく起こります。関数内での代入位置が原因なので、ループに入る前に初期値を置けば解消します。

# Python 3.12
def total_price(items):
    for item in items:
        subtotal += item     # subtotal を初期化する前に加算 → UnboundLocalError
    return subtotal

# 修正:ループの前で 0 を代入
def total_price_fixed(items):
    subtotal = 0
    for item in items:
        subtotal += item
    return subtotal

組み込み名を上書きして出る場合

listsumのような組み込み名を関数内で変数として代入し、その代入より前に組み込みとして呼ぶと発生します。組み込みを使っているつもりでも、代入によってローカル変数へ格上げされてしまうためです。変数名を組み込みと衝突しないものに変えるのが確実です。

# Python 3.12
def build():
    print(sum([1, 2, 3]))  # 組み込みの sum を呼んだつもり
    sum = 0                # 同じ関数内で sum に代入 → sum はローカル扱い
    return sum             # 上の print 行が UnboundLocalError になる

# 修正:組み込みと衝突しない名前にする
def build_fixed():
    print(sum([1, 2, 3]))
    total = 0
    return total

Python UnboundLocalErrorはどうデバッグすればいい?

Python UnboundLocalErrorのデバッグは、トレースバック末尾の行番号と変数名を起点に、その名前が関数内のどこで代入されているかを探すのが基本です。代入が見つかれば、その名前は関数全体でローカル変数だと確定できます。あとは参照位置が代入位置より前になっていないかを確認します。

# Python 3.12 デバッグの着眼点
def calc(flag):
    print(locals())      # 参照直前の locals() で束縛済みの名前を確認
    if flag:
        answer = 10
    return answer        # answer が locals() に無ければ未代入

# ある名前がローカル扱いか調べる
# calc.__code__.co_varnames にローカル変数名の一覧が入る
print(calc.__code__.co_varnames)  # ('flag', 'answer') と表示される

__code__.co_varnamesにその名前が含まれていれば、Pythonはコンパイル時点でローカル変数と判断しています。外側の変数を読んでいるつもりでも一覧に載っていたら、代入によってローカル化していると判断できます。

Python UnboundLocalErrorのデバッグのコツを伝えるデプロイ太郎のアイコン画像

デバッグのコツは、変数の値を疑う前に代入の位置を疑うことです。co_varnames を一度覗くと、ローカル化していたのかが一目で分かって腑に落ちますよ。

Python UnboundLocalErrorを未然に防ぐには?

Python UnboundLocalErrorの予防は、関数の入口で使う変数を初期化する習慣と、静的解析ツールの導入が両輪になります。pyflakesflake8pylintは代入前参照を実行前に警告してくれます。特にpylintE0601: used-before-assignmentとして検出します。

# 予防の書き方:使う変数は先頭でまとめて初期化する
def summarize(records):
    total = 0        # 集計値は先に 0
    latest = None    # 見つからない場合に備えて None
    for r in records:
        total += r["amount"]
        latest = r
    return total, latest

# 静的解析で事前検出する例(ターミナル)
# pip install pylint
# pylint your_script.py   → E0601: used-before-assignment を報告
外側の変数を更新したいときは、まず引数で受け取り戻り値で返す設計にできないかを検討し、それが難しい場合に限ってglobalやnonlocalを使うと保守性が保てます。

globalとnonlocalはどう使い分ける?

Python UnboundLocalErrorの解決でglobalとnonlocalのどちらを使うか迷う場面は多いものです。両者は参照先のスコープが異なる点が決定的な違いになります。globalはモジュール直下のグローバル変数を、nonlocalは1つ外側の関数のローカル変数を対象にします。

宣言対象スコープ使う場面
globalモジュール直下のグローバル変数関数の外で定義した変数を関数内で書き換えたいとき
nonlocal1つ外側の関数のローカル変数ネストした内側の関数から外側の関数の変数を書き換えたいとき

どちらも乱用するとどこで値が変わるか追いにくくなり、テストもしづらくなります。関数を小さく保ち、値は引数と戻り値で受け渡す設計を基本にしておけば、そもそもこの使い分けで悩む場面自体が減っていきます。

globalとnonlocalの使い分けをまとめるデプロイ太郎のアイコン画像

最初は難しく感じますが、代入の位置とスコープさえ意識できれば必ず克服できます。一つずつ確認していきましょう、応援しています!

よくある質問

Q
変数にちゃんと値を入れているのに、なぜUnboundLocalErrorが出るのですか?
A

関数の中でその名前に代入している行があると、Pythonはその名前を関数全体でローカル変数と決めつけるからです。外側の変数に値が入っていても、関数内では別物のローカル変数として扱われ、代入より前に読むと未代入と判定されます。

Q
globalとnonlocalはどちらを使えばよいですか?
A

更新したい変数がモジュール直下にあるならglobal、1つ外側の関数の中にあるならnonlocalを選びます。ネストした関数からさらに外の関数の変数を書き換えるときはnonlocalが正解で、globalではグローバル変数を探しに行くため意図と食い違います。

Q
try/exceptの中だけで代入すると、なぜこのエラーが起きるのですか?
A

tryブロックが例外で中断されると、代入行に到達しないまま変数が未代入で残るためです。そのあとexcept経由で同じ変数を参照すると代入前参照になります。tryの前に初期値を置くか、except内でreturnして参照そのものを避けると防げます。

Q
実行する前にUnboundLocalErrorを見つける方法はありますか?
A

静的解析ツールで事前に検出できます。pyflakesやflake8は代入前参照を警告し、pylintはE0601(used-before-assignment)として報告します。CIに組み込んでおけば、本番デプロイ前に分岐漏れの代入を自動でチェックできます。

Q
UnboundLocalErrorとNameErrorの違いは何ですか?
A

UnboundLocalErrorは、名前は存在するがローカルスコープでまだ値が束縛されていないときに起きるNameErrorのサブクラスです。一方のNameErrorは、そもそもどのスコープにも定義がない名前を参照したときに発生します。前者は代入位置の問題、後者は定義の有無の問題という点が違います。

この記事と一緒に知っておきたいエラー解決

関連エラー この記事との関連
Python AttributeError: has no attribute 同じくPythonで名前や属性の解決に失敗する代表的なエラーで、原因の切り分け方が似ています。
Python KeyError: キーが存在しません 存在しない対象を参照して落ちるという構造が共通し、初期化と防御的な書き方で防げる点が重なります。
JavaScript ReferenceError: is not defined 未定義・未束縛の変数を参照するという同種の問題で、スコープ理解の重要性を裏返しで学べます。
Ruby NameError: undefined local variable or method ローカル変数の未定義で起きる他言語版のエラーで、スコープ判定の考え方を比較して理解できます。
Python IndexError: list index out of range 関数内の変数やデータの状態を確認せず参照して起きるという、デバッグ観点が共通するPythonエラーです。

【出典】参考URL

https://docs.python.org/3/library/exceptions.html :UnboundLocalErrorの定義とNameErrorのサブクラスである根拠
https://docs.python.org/3/faq/programming.html :グローバル変数を関数内で更新する際の挙動とglobal宣言の必要性
https://friendly-traceback.github.io/docs/compare_exceptions.html :Python 3.10以前と3.11以降のエラーメッセージ文言の差

コメント

デプロイ太郎のSNSを見てみる!!