閉じる

なぜ危険?Pythonでデフォルト引数にリストや辞書を使うな

Pythonの関数定義におけるデフォルト引数は便利ですが、リストや辞書のような可変オブジェクトを使うと予期せぬバグにつながります。

この記事では、なぜ危険なのかを挙動から理解し、初心者の方でも迷わず書ける安全なパターンを段階的に解説します。

結論として、デフォルト引数にリストや辞書を直接書くのは避け、Noneや不変オブジェクトを使うのがベストプラクティスです。

Pythonのデフォルト引数とリスト/辞書の罠

デフォルト引数は関数定義時に1回だけ評価

Pythonではdefが実行された瞬間にデフォルト引数の式が評価され、その値が関数オブジェクトに保持されます。

呼び出しのたびに評価されるわけではありません。

つまり「デフォルト値は1回だけ作られ、以降は同じものが再利用される」ということです。

確認コード

Python
# デフォルト引数が関数定義時に評価される例
from datetime import datetime
from time import sleep

def stamped(now=datetime.now()):
    # now は def の時点で固定される
    print("captured at:", now)

stamped()  # ここでの時刻と
sleep(1)
stamped()  # ここでの時刻は同一(増えない)
実行結果
captured at: 2025-09-10 12:00:00.123456
captured at: 2025-09-10 12:00:00.123456

リスト/辞書は状態が共有される(意図せぬ副作用)

リストや辞書はミュータブル(変更可能)です。

一度作られたデフォルトのリスト/辞書が呼び出し間で共有され、appendや代入で中身が変化すると、その変化は次回以降の呼び出しにも持ち越されます

オブジェクトIDでの確認

Python
# デフォルトのリストがずっと同じオブジェクトであることを確認
def collect(item, bucket=[]):
    bucket.append(item)
    print("bucket:", bucket, "id:", id(bucket))

collect("A")
collect("B")
collect("C")
実行結果
bucket: ['A'] id: 140123456789456
bucket: ['A', 'B'] id: 140123456789456
bucket: ['A', 'B', 'C'] id: 140123456789456

バグ例: appendでリストが呼び出し間で増え続ける

問題のコード

Python
# 型ヒント付きの例(3.9+)
def add_tag(tag: str, tags: list[str] = []):
    # 期待: tags が省略されたら空リストで始める
    # 実際: 初回に作られた1個のリストが使い回される
    tags.append(tag)
    return tags

print(add_tag("python"))
print(add_tag("tips"))
print(add_tag("mutable-default"))
実行結果
['python']
['python', 'tips']
['python', 'tips', 'mutable-default']

原因の要点

  • デフォルトの[]は関数定義時に1回だけ作られるため、以降の呼び出しでも同じリストが使われ続けます。
  • その結果、意図せず値が蓄積し、呼び出し順によって結果が変わります。

バグ例: 辞書のキーが蓄積してしまう

問題のコード

Python
def add_count(key: str, counter: dict[str, int] = {}):
    # 期待: 呼び出しごとに新しい辞書が使われる
    # 実際: 1つの辞書にカウントが蓄積され続ける
    counter[key] = counter.get(key, 0) + 1
    return counter

print(add_count("A"))
print(add_count("A"))
print(add_count("B"))
実行結果
{'A': 1}
{'A': 2}
{'A': 2, 'B': 1}

なぜ見落とすのか(テストで再現しにくい理由)

テストが小規模で実行順が固定されていると、偶然に「正しいように見える」出力が出ることがあり、状態共有の影響に気づきにくくなります。

特に以下の状況では見落としやすいです。

  • 単体テストで1回だけ呼び出して終わる場合
  • テストの実行順がたまたま副作用の影響を打ち消す場合
  • 並行・並列実行でタイミングにより結果が変化する場合
    呼び出し順で結果が変わるのは強い異常サインです。

なぜ起きるか(ミュータブルと評価タイミング)

ミュータブル(list, dict, set)の性質

リストや辞書、セットはインプレースに変更できるデータ構造です。

参照を共有したまま.appendや代入を行うと、中身が同じオブジェクトに反映されます。

デフォルト引数でこのようなオブジェクトを使うと、1度作ったデータがずっと生き続けてしまいます。

イミュータブル(tuple, str, int)との違い

タプルや文字列、整数はイミュータブル(不変)です。

内容を変更できないため、デフォルトとして共有されても中身が増殖することはありません。

例: タプルのデフォルトは中身が増えない

Python
def add_item_immutable(x: int, bucket: tuple[int, ...] = ()):
    # 不変オブジェクトなので append は存在しない
    # 新しいタプルを作って返す
    return bucket + (x,)

print(add_item_immutable(1))
print(add_item_immutable(2))
実行結果
(1,)
(2,)

イミュータブルのデフォルトはOKか(注意点)

イミュータブル自体をデフォルトにするのは安全ですが、評価タイミングには注意が必要です。

たとえばdatetime.now()uuid4()のような「呼び出しのたびに変わってほしい値」をデフォルトに書くと、関数定義時の1回だけ実行され、以降は固定化されます。

また、イミュータブルをデフォルトにし、内部でミュータブルへ変換する手法(例: tuple→list)は有効ですが、毎回新しいオブジェクトを生成することを忘れないでください。

安全な書き方(対策とベストプラクティス)

Noneをデフォルトにする(items=None)

最も広く使われる安全策は、デフォルトをNoneにして、関数内で新しいオブジェクトを作る方法です。

型ヒントを使うと意図が明確になります。

Python
from typing import Optional

def add_tag_safe(tag: str, tags: Optional[list[str]] = None) -> list[str]:
    if tags is None:
        tags = []  # 呼び出しごとに新しいリストを生成
    tags.append(tag)
    return tags

print(add_tag_safe("python"))
print(add_tag_safe("tips"))
print(add_tag_safe("immutable"))
実行結果
['python']
['tips']
['immutable']

関数内で新しいlist/dictを生成する

入力引数を変更したくない場合は、関数の冒頭で新しいオブジェクトを必ず作るようにします。

これはコピーの意図を明示し、副作用を局所化できます。

Python
def merge_user_prefs(base: dict[str, str] | None = None) -> dict[str, str]:
    if base is None:
        base = {}
    # 新しい辞書にデフォルト値を載せる
    prefs = {**base, "theme": "light", "lang": "ja"}
    return prefs

print(merge_user_prefs())
print(merge_user_prefs({"theme": "dark"}))
実行結果
{'theme': 'light', 'lang': 'ja'}
{'theme': 'dark', 'lang': 'ja'}

不変のデフォルトを使い内部で変換する(tuple→list)

不変のタプルをデフォルトにして、毎回リストへ変換する方法もあります。

Python
def collect_items(item: int, seed: tuple[int, ...] = ()) -> list[int]:
    bucket = list(seed)  # ここで常に新しいリスト
    bucket.append(item)
    return bucket

print(collect_items(1))
print(collect_items(2))
実行結果
[1]
[2]

共有したい状態は外に出す(グローバルやクラスで管理)

意図的に状態を共有したいなら、デフォルト引数で共有せず、明示的にスコープを分けて管理します。

クラスでの管理が最も分かりやすく、安全です。

Python
class TagCollector:
    def __init__(self) -> None:
        self.tags: list[str] = []

    def add(self, tag: str) -> None:
        self.tags.append(tag)

    def snapshot(self) -> list[str]:
        # 外部に渡す際はコピーすると安心
        return list(self.tags)

collector = TagCollector()
collector.add("python")
collector.add("class")
print(collector.snapshot())
実行結果
['python', 'class']

PEP8/公式ガイドの推奨に従う

PEP 8は「デフォルト引数にミュータブルな値を使わない」ことを推奨しています。

実務ではitems=Noneパターンが事実上の標準であり、レビューでも強く求められます。

例外的な意図がない限り、[]や{}をデフォルトに書かない方針をチーム規約に含めると安全です。

よくある落とし穴と勘違い

[]や{}をデフォルトに書くのはアンチパターン

デフォルトに[]{}を書くのはアンチパターンです。

短く書けてもバグの温床になります。

可読性・保守性の両面でNoneを使いましょう。

deepcopyでの回避はデフォルト引数には効かない

評価が1回しか行われないという本質はdeepcopyでも変わりません。

Python
import copy

def f(x=copy.deepcopy([])):  # def 時に1回だけ deep コピーされる
    x.append(1)
    return x

print(f())
print(f())
実行結果
[1]
[1, 1]

期待と違って毎回新しいリストにはなりません。

deepcopyを使った式自体が定義時に1回だけ評価されるためです。

正しい対策はNoneセンチネルや毎回生成することです。

ラムダやメソッドのデフォルト引数も同じ挙動

関数だけでなく、lambdaやインスタンスメソッドのデフォルト引数も同じく1回だけ評価です。

Python
# ラムダでも同じ罠
f = lambda x, acc=[]: (acc.append(x), list(acc))[1]  # acc が共有される
print(f("a"))
print(f("b"))
実行結果
['a']
['a', 'b']

可変デフォルトを意図的に共有する特殊ケースのリスク

「高速化のために敢えて共有する」設計も可能ですが、バグを招きやすくテストも難しくなります

スレッド安全性や再入可能性、予測可能性を損なうため、共有は構造的に表現(クラスや外部ストア)すべきです。

テストで気づくサイン(呼び出し順で結果が変わる)

  • 同じテストを単体で走らせる時と、スイート全体で走らせる時で結果が変わる
  • 個別実行すると通るが、一括実行だと失敗する
  • 失敗が順序や回数に依存して再現したりしなかったりする
    このようなときは可変デフォルト引数やグローバルな状態共有を疑ってください。

参考: ミュータブルとイミュータブルの安全性まとめ

種別代表例デフォルト引数としての可否補足
ミュータブルlist, dict, set非推奨Noneセンチネルや毎回生成で回避
イミュータブルtuple, str, int, float概ね安全動的値(now, uuid, random)は定義時固定に注意
ユーザー定義dataclassのインスタンスなど原則非推奨可変フィールドがあれば共有される

「毎回新しいリストを作ると遅いのでは」と心配されがちですが、関数呼び出しのオーバーヘッドに比べて小さいことが多く、正しさと可読性を優先すべきです。

必要ならベンチマークし、実測で判断しましょう。

まとめ

Pythonのデフォルト引数は関数定義時に1回だけ評価され、ミュータブルな値を使うと状態が呼び出し間で共有されるという仕様が、リストや辞書の罠の正体です。

appendや代入で中身が増え続け、テストの実行順で結果が変わるなど、発見が難しいバグを生みます。

安全に書くにはNoneセンチネルを使い、関数内で新しいリストや辞書を生成し、必要に応じて不変のデフォルトを内部で変換します。

状態を共有したい場合はクラスや外部のスコープで管理し、PEP8の推奨に従うことで、予測可能で保守しやすいコードになります。

[]や{}をデフォルトに書くのは避けるという原則をチームで共有し、落とし穴を未然に防ぎましょう。

Python 実践TIPS - コーディング効率化・Pythonic
この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!