リストの中身を取り出そうとしたら、IndexError: list index out of range と表示されてプログラムが止まってしまった。そんな経験はありませんか?
リストを使い始めると、ほぼ全員が一度は出会うのがこのIndexErrorです。英語のメッセージは少し怖く見えますが、言っていることはとてもシンプルです。
この記事では、IndexErrorが出る仕組みから、よくある原因と直し方、そして再発を防ぐ書き方までを順番に見ていきます。サンプルの実行結果は、手元のPython 3.11、3.12、3.13で確認したものです。
IndexErrorとはどんなエラーなのか¶
まずは、IndexErrorがどんなときに出るのかを押さえておきましょう。ひとことで言えば、用意されていない番号の席を指定したときに出るエラーです。
リストや文字列、タプルのように、データが順番に並んだものをPythonではシーケンスと呼びます。シーケンスの中身には、0から始まる番号が振られています。この番号がインデックスです。
公式ドキュメントでは、IndexErrorはシーケンスの添字が範囲外のときに送出される例外だと説明されています。つまり、IndexErrorは、存在しない番号で中身を取り出そうとした合図なのです。
辞書でよく見るKeyErrorとは兄弟のような関係にあります。どちらもLookupErrorという共通の親クラスを持っていて、探したものが見つからなかったことを表しています。
【関連記事】PythonでKeyErrorが出る理由とは?辞書dictで初心者がハマるポイント
エラーメッセージの読み方¶
次に、実際のエラー表示を見てみましょう。3つしか要素がないリストから、4番目を取り出そうとしたときの例です。
fruits = ["apple", "banana", "cherry"]
print(fruits[3])
実行すると、次のようなメッセージが出ます。
Traceback (most recent call last):
File "main.py", line 2, in <module>
print(fruits[3])
~~~~~~^^^
IndexError: list index out of range
最後の行がいちばん大事な情報です。list index out of range は、リストのインデックスが範囲の外にあるという意味になります。
その上には、エラーが起きたファイル名と行番号が出ています。Python 3.11以降では、問題の箇所に~や^の印が付くので、どの式で失敗したのかが一目でわかります。
メッセージの前半は、対象のデータの種類によって変わります。どんな表示があるのか、表にまとめておきました。
| 操作の例 | 表示されるメッセージ | 意味 |
|---|---|---|
[1, 2][5] |
list index out of range | リストの範囲外を読もうとした |
"ab"[5] |
string index out of range | 文字列の範囲外を読もうとした |
(1,)[3] |
tuple index out of range | タプルの範囲外を読もうとした |
range(3)[5] |
range object index out of range | rangeの範囲外を読もうとした |
[].pop() |
pop from empty list | 空のリストから取り出そうとした |
[1].pop(5) |
pop index out of range | pop()に範囲外の番号を渡した |
lst[3] = 0 |
list assignment index out of range | 範囲外の番号へ代入しようとした |
どの表示でも、out of range か empty という言葉が入っていれば、番号か中身の数を疑えばよいわけです。
【関連記事】Pythonのエラー文はどこを読めばいい?初心者向けtracebackの見方
IndexErrorのよくある原因と直し方¶
ここからは、IndexErrorが出る典型的なパターンを見ていきます。原因の多くは、番号の数え方の思い込みにあります。
0から数えることを忘れている¶
もっとも多いのが、インデックスを1から数えてしまうケースです。Pythonでは最初の要素が0番、2番目が1番になります。
要素が3つのリストなら、使える番号は0、1、2の3つだけです。つまり、最後の要素の番号は、要素の数から1を引いた値になります。
このずれは、プログラミングの世界でoff-by-oneエラー、つまり1つずれの誤りと呼ばれています。私は10年ほどエンジニアとして開発に関わってきましたが、経験を積んだ今でも、境界の1つずれはレビューで指摘されることがあるくらい根深いミスです。
len()をそのまま番号に使っている¶
0始まりのルールを知っていても、len()と組み合わせたときにうっかり間違えることがあります。len()が返すのは要素の数であって、最後の番号ではありません。
scores = [80, 65, 92]
# NG: len(scores) は 3 なので、scores[3] を読もうとしてしまう
# print(scores[len(scores)])
# OK: 最後の番号は len(scores) - 1
print(scores[len(scores) - 1]) # 92
# もっと簡単なのはマイナスのインデックス
print(scores[-1]) # 92
Pythonでは、マイナスの番号を使うと後ろから数えられます。-1が最後、-2が後ろから2番目です。
最後の要素がほしいときは、len()を使った計算より-1を使うほうが読みやすく、ずれも起きません。ただし、マイナスの番号にも範囲があり、要素が3つのリストで-4を指定するとIndexErrorになります。
空のリストとループで起きるIndexError¶
番号の数え方を正しく理解していても、IndexErrorが出ることがあります。データの中身が想定と違うときです。
空のリストから先頭を取り出している¶
ファイルの読み込みやWebからのデータ取得では、結果が0件になることがあります。そのときに先頭の要素を決め打ちで取り出すと、IndexErrorで止まってしまいます。
def first_item(items):
"""先頭の要素を返す。空なら None を返す。"""
if not items:
return None
return items[0]
print(first_item(["red", "green"])) # red
print(first_item([])) # None
Pythonでは、空のリストを条件式に書くとFalseとして扱われます。if not items と書けば、中身が空かどうかを手軽に確かめられます。
実務では、この空のデータが原因の障害にたびたび出会いました。テスト環境では必ずデータが入っていたのに、本番では検索結果が0件の日があり、夜間のバッチ処理がIndexErrorで止まっていたことがあります。
それ以来、外から受け取ったデータについては、まず空の場合にどう動くべきかを決めてから書くようにしています。皆さんも、件数が0のときのことを一度想像してみてください。
rangeの範囲がリストの長さとずれている¶
for文でrange()を使って番号を回すときにも、ずれは起きやすくなります。たとえば、隣どうしの要素を比べたいときに、range(len(nums))のまま i + 1 番を読むと、最後の周回で範囲外にはみ出します。
nums = [3, 8, 5, 10]
# NG: i が 3 のとき nums[4] を読んでしまう
# for i in range(len(nums)):
# print(nums[i + 1] - nums[i])
# OK: 隣どうしを比べるなら zip() で2つずつ組にする
for current, nxt in zip(nums, nums[1:]):
print(nxt - current) # 5, -3, 5
そもそも、番号を使わずに済む書き方を選ぶのがいちばんの予防策です。要素を順番に処理するだけなら、for item in nums と書けばインデックスは登場しません。
番号も一緒にほしい場合は、enumerate()を使うと安全です。zip()とenumerate()は、IndexErrorを遠ざけてくれる心強い道具だと覚えておきましょう。
【関連記事】Pythonのenumerate関数の使い方とは?インデックス管理を簡単にする書き方
ループ中にリストを削除して起きるIndexError¶
少し気づきにくいのが、ループの途中でリストの中身を減らしてしまうケースです。番号の範囲は最初に決まったまま、中身だけが短くなるからです。
次のコードは、0点のテストを取り除こうとしています。一見正しそうですが、途中で範囲外の番号を読んでしまいます。
scores = [0, 70, 0, 85]
# NG: 削除するたびにリストが短くなり、後半で範囲外になる
# for i in range(len(scores)):
# if scores[i] == 0:
# del scores[i]
# OK: 条件に合うものだけを集めた新しいリストを作る
scores = [s for s in scores if s != 0]
print(scores) # [70, 85]
range(len(scores))は、ループが始まった時点の長さ4で番号を作ります。ところが削除のたびにリストは短くなるので、後半の番号はもう存在しません。
こうした場面では、元のリストを書き換えずに、残したい要素だけで新しいリストを作るのが定番です。内包表記を使うと1行で書けて、意図も読み取りやすくなります。
pop()や代入でも出るIndexError¶
IndexErrorは、要素を読むときだけに出るわけではありません。取り出しや書き込みでも同じように起きます。
リストのpop()は、末尾の要素を取り出して返すメソッドです。空のリストに対して呼ぶと、pop from empty list というメッセージで止まります。
ほかの言語を経験した人が驚きやすいのが、範囲外への代入です。Pythonでは、lst[10] = 0 のように存在しない番号へ代入しても、リストは自動で伸びません。
公式のリファレンスでも、添字への代入でリストに新しい要素を追加することはできないと説明されています。要素を増やしたいときは、append()やinsert()を使いましょう。
ちなみに、insert()は範囲外の番号を渡してもエラーになりません。指定した番号がリストより大きいときは、末尾に追加されます。
スライスではIndexErrorが出ない理由¶
ここで、ちょっと不思議な性質を紹介しておきます。範囲外の番号を指定しても、スライスならエラーにならないのです。
たとえば、要素が2つのリストに対して [1, 2][5:9] と書くと、エラーではなく空のリストが返ってきます。公式ドキュメントでは、スライスの開始や終了がシーケンスの長さより大きい場合は、長さに置き換えて扱うと説明されています。
つまり、スライスは範囲外を自動で切り詰めてくれるのです。
この性質を使うと、先頭から最大3件だけ取り出したいときに、items[:3] と書くだけで済みます。要素が3件未満でも、あるだけの要素が返ってくるからです。
一方で、エラーにならないことが逆に問題を隠すこともあります。スライスの番号を間違えていても静かに空のリストが返るので、結果がおかしいときはスライスの範囲も疑ってみてください。
【関連記事】Pythonのスライスとは?リストや文字列の一部を切り出す書き方を初心者向けに解説
IndexErrorをtryで受け止める前に考えたいこと¶
IndexErrorは、try文とexceptで受け止めることもできます。けれども、何でもtryで包めばよいというわけではありません。
def get_or_default(items, index, default=None):
"""範囲外なら default を返す。"""
try:
return items[index]
except IndexError:
return default
print(get_or_default([10, 20], 1)) # 20
print(get_or_default([10, 20], 5, 0)) # 0
この書き方が向いているのは、範囲外になることが仕様として起こりうる場面です。たとえば、ユーザーが入力した番号でデータを選ぶ場合などがあてはまります。
反対に、本来は起きるはずのないIndexErrorを黙って握りつぶすと、バグの発見が遅れます。ループの範囲ミスのような原因は、tryで隠すのではなくコードそのものを直すべきです。
迷ったときは、そのエラーが起きたら正しい動きは何かを考えると判断しやすくなります。決まった代わりの値を返せるならtry、そもそも起きてはいけないなら原因の修正、という分け方です。
【関連記事】Pythonのtry・except・else・finallyとは?例外処理の基本の流れ
IndexErrorを未然に防ぐ習慣¶
最後に、日ごろから意識しておきたいポイントを振り返ります。どれも少しの工夫で取り入れられるものです。
| 場面 | おすすめの書き方 |
|---|---|
| 要素を順番に処理したい | for item in items で番号を使わない |
| 番号と要素の両方がほしい | enumerate(items) を使う |
| 隣どうしを比べたい | zip(items, items[1:]) で組にする |
| 最後の要素がほしい | items[-1] を使う |
| 先頭の数件だけほしい | items[:3] のようにスライスを使う |
| 空かもしれないデータを扱う | if not items で先に確かめる |
| 条件に合う要素を消したい | 内包表記で新しいリストを作る |
表を見るとわかるように、多くの対策は番号を直接扱わないことにつながっています。インデックスを手で計算する回数が減るほど、IndexErrorに出会う機会も減っていきます。
それでもエラーが出たときは、慌てずにprint()でリストの長さと指定した番号を表示してみましょう。len(items)とindexの値を並べて見るだけで、原因がすぐに見つかることがほとんどです。
まとめ¶
IndexErrorは、リストや文字列などのシーケンスに対して、存在しない番号でアクセスしたときに出るエラーです。メッセージに out of range や empty が含まれていたら、番号の数え方とデータの件数を確かめましょう。
原因として多いのは、0始まりの数え忘れ、len()をそのまま番号に使うミス、空のリスト、ループ中の削除です。どれも、番号を手で計算する場面で起きやすいという共通点があります。
for文で要素を直接回す、enumerate()やzip()を使う、マイナスのインデックスやスライスを活用する。こうした書き方を身につけると、IndexErrorそのものが起きにくいコードに近づいていきます。
エラーは失敗ではなく、Pythonからのていねいな道案内です。メッセージを読む習慣がつけば、次に出会ったときにはきっと落ち着いて直せるはずです。
ここまでお読みいただきありがとうございました。