Pythonの関数定義におけるデフォルト引数は便利ですが、リストや辞書のような可変オブジェクトを使うと予期せぬバグにつながります。
この記事では、なぜ危険なのかを挙動から理解し、初心者の方でも迷わず書ける安全なパターンを段階的に解説します。
結論として、デフォルト引数にリストや辞書を直接書くのは避け、Noneや不変オブジェクトを使うのがベストプラクティスです。
- 関連記事:デフォルト引数とは? 引数に初期値を設定する方法
- 関連記事:Noneとは?意味と使い方をやさしく解説
Pythonのデフォルト引数とリスト/辞書の罠
デフォルト引数は関数定義時に1回だけ評価
Pythonではdef
が実行された瞬間にデフォルト引数の式が評価され、その値が関数オブジェクトに保持されます。
呼び出しのたびに評価されるわけではありません。
つまり「デフォルト値は1回だけ作られ、以降は同じものが再利用される」ということです。
確認コード
# デフォルト引数が関数定義時に評価される例
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での確認
# デフォルトのリストがずっと同じオブジェクトであることを確認
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
- 関連記事:ゼロからわかるPythonリスト(list)の作り方と使いどころ
- 関連記事:もうハマらない! list.copyとcopy.deepcopyの正しい使い分け
- copy.deepcopy — 公式ドキュメント
バグ例: appendでリストが呼び出し間で増え続ける
問題のコード
# 型ヒント付きの例(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回だけ作られるため、以降の呼び出しでも同じリストが使われ続けます。 - その結果、意図せず値が蓄積し、呼び出し順によって結果が変わります。
バグ例: 辞書のキーが蓄積してしまう
問題のコード
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)との違い
タプルや文字列、整数はイミュータブル(不変)です。
内容を変更できないため、デフォルトとして共有されても中身が増殖することはありません。
例: タプルのデフォルトは中身が増えない
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
にして、関数内で新しいオブジェクトを作る方法です。
型ヒントを使うと意図が明確になります。
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を生成する
入力引数を変更したくない場合は、関数の冒頭で新しいオブジェクトを必ず作るようにします。
これはコピーの意図を明示し、副作用を局所化できます。
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)
不変のタプルをデフォルトにして、毎回リストへ変換する方法もあります。
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]
共有したい状態は外に出す(グローバルやクラスで管理)
意図的に状態を共有したいなら、デフォルト引数で共有せず、明示的にスコープを分けて管理します。
クラスでの管理が最も分かりやすく、安全です。
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
でも変わりません。
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回だけ評価です。
# ラムダでも同じ罠
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の推奨に従うことで、予測可能で保守しやすいコードになります。
[]や{}をデフォルトに書くのは避けるという原則をチームで共有し、落とし穴を未然に防ぎましょう。