DEV Community

Cover image for IndexError: list index out of range in Python: 10 Causes and Fixes (Tested Examples)
Mahir Faysal
Mahir Faysal

Posted on Originally published at mfaysal.com

IndexError: list index out of range in Python: 10 Causes and Fixes (Tested Examples)

Python raises IndexError: list index out of range when you index a sequence (usually a list) at a position that does not exist. Valid indexes for a list of length n are 0 through n-1 (and -1 through -n). The quickest fixes are a len check before you index, a narrow try/except IndexError, safer loops with enumerate, and never hard-coding a length you already changed with pop, remove or split.

The Python docs list IndexError as the exception raised when a sequence subscript is out of range. That is the whole story. The hard part is working out why the index is wrong: the list was empty, you used len as an index (off-by-one), a split returned fewer fields than you expected, or you shrank the list while still walking the old length.

I reproduced every example below on Python 3.13.5, and I re-ran the empty-list traceback on 3.11.17 and 3.14.8. All three printed the same message and caret underlining. Paths in the tracebacks point at my test scripts under /workspace/seo-posts-nov07/test/python/.

In this guide:

  • What IndexError looks like
  • The 10 causes at a glance
  • 1. Indexing an empty list
  • 2. Off-by-one: using len(lst) as an index
  • 3. Length changed after pop or delete
  • 4. split() or CSV rows shorter than you expected
  • 5. Negative indexes past -len(lst)
  • 6. Mutating a list while walking a fixed length
  • 7. Nested lists: the crash is often the inner index
  • 8. Strings raise a closely related IndexError
  • 9. range() that starts at 1 (or ends past len)
  • 10. Slicing is forgiving; indexing is not
  • Reliable fixes checklist
  • Frequently asked questions

What IndexError looks like

Here is the smallest crash. The list is empty, so there is no index 0:

nums = []
print(nums[0])
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
  File "/workspace/seo-posts-nov07/test/python/c01_empty.py", line 2, in <module>
    print(nums[0])
          ~~~~^^^
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

Since Python 3.11, the traceback underlines the lookup. The tildes sit under nums and the carets under [0], so you know which expression failed. The same traceback printed on 3.11.17 and 3.14.8.

If the variable was None instead of a list, you would see a different exception: TypeError: 'NoneType' object is not subscriptable. That family of crashes is covered in TypeError: NoneType is not subscriptable.

The 10 causes at a glance

# Cause How to spot it Fix
1 Empty list IndexError on [0] Check if lst: / len
2 Off-by-one (lst[len(lst)]) Index equals length Use 0..len-1 or [-1]
3 Length changed after pop Worked before a delete Re-check length; use deque
4 Short split / CSV row Fewer parts than expected Pad or guard len(parts)
5 Negative index beyond -len lst[-n-1] Bound-check negatives too
6 Mutating while indexing a fixed range Crash mid-loop after pop Iterate a copy / reverse
7 Unpacking short rows by index Crash on row[2] Pad fields; validate rows
8 String index out of range Message says string Same rules; check len
9 Nested list, wrong level Carets under inner [] Print each level; check lengths
10 range / slice vs index mix-up Slice works, [i] crashes Remember slices are forgiving

1. Indexing an empty list

An empty list has length 0. There is no valid positive or negative index. nums[0] and nums[-1] both raise.

Fix: test emptiness first, or catch narrowly when absence is exceptional:

items = ["a", "b"]
idx = 5
if 0 <= idx < len(items):
    print(items[idx])
else:
    print("out of range: idx", idx, "len", len(items))

try:
    print(items[5])
except IndexError as e:
    print("caught:", repr(e))

it = iter(items)
print("first:", next(it, None))
print("second:", next(it, None))
print("third:", next(it, None))
Enter fullscreen mode Exit fullscreen mode
out of range: idx 5 len 2
caught: IndexError('list index out of range')
first: a
second: b
third: None
Enter fullscreen mode Exit fullscreen mode

next(iterator, default) is a clean way to pull the first item without indexing. For optional "first element" reads, prefer that or an explicit if items: guard over a bare items[0].

2. Off-by-one: using len(lst) as an index

A list of three items has indexes 0, 1 and 2. Index 3 is one past the end - the classic off-by-one:

items = ["a", "b", "c"]  # len 3, valid indexes 0..2
print("len:", len(items))
print(items[3])
Enter fullscreen mode Exit fullscreen mode
len: 3
Traceback (most recent call last):
  File "/workspace/seo-posts-nov07/test/python/c02_off_by_one.py", line 3, in <module>
    print(items[3])
          ~~~~~^^^
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

Fix: the last item is items[-1] or items[len(items) - 1], never items[len(items)]. When you loop with indexes, use range(len(items)) (which stops before len) or, better, enumerate. Sequence indexing rules live in the Python built-in types docs.

3. Length changed after pop or delete

You computed an index when the list was longer. After pop, that index is gone:

queue = ["one", "two", "three"]
print("before pop:", queue, "len", len(queue))
queue.pop()
print("after pop:", queue, "len", len(queue))
print(queue[2])
Enter fullscreen mode Exit fullscreen mode
before pop: ['one', 'two', 'three'] len 3
after pop: ['one', 'two'] len 2
Traceback (most recent call last):
  File "/workspace/seo-posts-nov07/test/python/c03_after_pop.py", line 5, in <module>
    print(queue[2])
          ~~~~~^^^
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

Fix: re-check len after every mutation, or use a structure that matches the access pattern. For queues, collections.deque gives cheap left pops without leaving stale indexes around:

from collections import deque
q = deque(["one", "two", "three"])
print("popleft:", q.popleft())
print("remaining:", list(q))
Enter fullscreen mode Exit fullscreen mode
popleft: one
remaining: ['two', 'three']
Enter fullscreen mode Exit fullscreen mode

Lists, tuples, sets and dicts each behave differently under indexing and membership; if you are choosing a structure, see list vs tuple vs set vs dict.

4. split() or CSV rows shorter than you expected

str.split returns only the parts that exist. A line with two fields is length 2, so index 2 raises:

line = "Alice,admin"
parts = line.split(",")
print("parts:", parts, "len", len(parts))
print(parts[2])  # expected email at index 2
Enter fullscreen mode Exit fullscreen mode
parts: ['Alice', 'admin'] len 2
Traceback (most recent call last):
  File "/workspace/seo-posts-nov07/test/python/c04_split_short.py", line 4, in <module>
    print(parts[2])  # expected email at index 2
          ~~~~~^^^
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

The same pattern shows up when you pull fields by index from a short CSV row:

row = "id,name".split(",")  # only 2 fields; expected 3
print("row:", row)
uid, name, email = row[0], row[1], row[2]
Enter fullscreen mode Exit fullscreen mode
row: ['id', 'name']
Traceback (most recent call last):
  File "/workspace/seo-posts-nov07/test/python/c07_csv_row.py", line 4, in <module>
    uid, name, email = row[0], row[1], row[2]
                                       ~~~^^^
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

Fix: pad missing fields, or refuse the row early:

line = "Alice,admin"
parts = line.split(",")
if len(parts) < 3:
    parts = parts + [""] * (3 - len(parts))
name, role, email = parts[0], parts[1], parts[2]
print("name:", name, "role:", role, "email:", repr(email))

first, *rest = "a,b".split(",")
print("first:", first, "rest:", rest)
Enter fullscreen mode Exit fullscreen mode
name: Alice role: admin email: ''
first: a rest: ['b']
Enter fullscreen mode Exit fullscreen mode

5. Negative indexes past -len(lst)

Negative indexes count from the end. -1 is the last item, -len(lst) is the first. One step further raises:

letters = ["x", "y", "z"]
print("letters[-1]:", letters[-1])
print("letters[-3]:", letters[-3])
print(letters[-4])
Enter fullscreen mode Exit fullscreen mode
letters[-1]: z
letters[-3]: x
Traceback (most recent call last):
  File "/workspace/seo-posts-nov07/test/python/c05_negative.py", line 4, in <module>
    print(letters[-4])
          ~~~~~~~^^^^
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

Fix: bound-check negatives the same way you check positives:

letters = ["x", "y", "z"]
n = -4
if -len(letters) <= n < len(letters):
    print(letters[n])
else:
    print("negative index out of range:", n)

def at(seq, i, default=None):
    try:
        return seq[i]
    except IndexError:
        return default

print("at(letters, 99):", at(letters, 99))
print("at(letters, -1):", at(letters, -1))
Enter fullscreen mode Exit fullscreen mode
negative index out of range: -4
at(letters, 99): None
at(letters, -1): z
Enter fullscreen mode Exit fullscreen mode

A tiny at() helper keeps call sites clean when a missing position should become a default instead of a crash. For missing keys on dicts you want a different exception and fix path - see KeyError in Python.

6. Mutating a list while walking a fixed length

Hard-coding the old length (or caching len once) and then pop-ing inside the loop is a reliable way to walk off the end:

nums = [10, 20, 30, 40]
i = 0
while i < 4:  # hard-coded old length
    print("i", i, "value", nums[i])
    if nums[i] % 20 == 0:
        nums.pop(i)
    else:
        i += 1
Enter fullscreen mode Exit fullscreen mode
i 0 value 10
i 1 value 20
i 1 value 30
i 2 value 40
Traceback (most recent call last):
  File "/workspace/seo-posts-nov07/test/python/c06_mutate_while_iter.py", line 5, in <module>
    print("i", i, "value", nums[i])
                           ~~~~^^^
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

After popping 20, the list is shorter, but the loop still aims for index 3. Fix: iterate indexes in reverse when deleting, or build a new list:

nums = [10, 20, 30, 40]
for i in range(len(nums) - 1, -1, -1):
    if nums[i] % 20 == 0:
        nums.pop(i)
print("after reverse pop:", nums)

items = ["red", "green", "blue"]
for i, value in enumerate(items):
    print(i, value)
Enter fullscreen mode Exit fullscreen mode
after reverse pop: [10, 30]
0 red
1 green
2 blue
Enter fullscreen mode Exit fullscreen mode

7. Nested lists: the crash is often the inner index

Chained indexes fail at the first bad level. The caret underline tells you which [] broke:

matrix = [[1, 2], [3, 4]]
print("matrix[0]:", matrix[0])
print(matrix[1][2])
Enter fullscreen mode Exit fullscreen mode
matrix[0]: [1, 2]
Traceback (most recent call last):
  File "/workspace/seo-posts-nov07/test/python/c09_nested.py", line 4, in <module>
    print(matrix[1][2])
          ~~~~~~~~~^^^
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

matrix[1] exists ([3, 4]), but that inner list only has indexes 0 and 1. Fix: print each level, check len(matrix[r]) before matrix[r][c], and treat ragged rows as data you must validate. When you are pasting sample matrices while debugging, the helpers on mfaysal.com/free-tools are handy for a quick look.

8. Strings raise a closely related IndexError

Strings are sequences too. Index past the end and you get a sibling message:

word = "hi"
print("len:", len(word))
print(word[2])
Enter fullscreen mode Exit fullscreen mode
len: 2
Traceback (most recent call last):
  File "/workspace/seo-posts-nov07/test/python/c08_str_index.py", line 4, in <module>
    print(word[2])
          ~~~~^^^
IndexError: string index out of range
Enter fullscreen mode Exit fullscreen mode

Same rules: valid indexes are 0..len-1. Slicing a string is forgiving (word[10:20] returns ''); indexing is not. Prefer slicing when you want a tolerant read, and indexing when a missing character should be an error.

9. range() that starts at 1 (or ends past len)

People sometimes write range(1, len(names) + 1) because they want "human" numbering, then use i as an index:

names = ["Ada", "Lin"]
for i in range(1, len(names) + 1):
    print(i, names[i])
Enter fullscreen mode Exit fullscreen mode
1 Lin
Traceback (most recent call last):
  File "/workspace/seo-posts-nov07/test/python/c10_range_vs_len.py", line 4, in <module>
    print(i, names[i])
             ~~~~~^^^
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

Index 1 printed Lin; index 2 does not exist. Fix: keep display numbers separate from indexes:

names = ["Ada", "Lin"]
for i, name in enumerate(names, start=1):
    print(i, name)  # i is 1-based for display
Enter fullscreen mode Exit fullscreen mode

Or loop with range(len(names)) and add 1 only when you print. Mixing 1-based counting with 0-based indexing is one of the most common IndexError sources in beginner code and in quick scripts.

10. Slicing is forgiving; indexing is not

Slices clamp to the ends of the sequence. A single index does not:

data = [1, 2, 3]
print("slice data[5:]:", data[5:])
print("slice data[1:10]:", data[1:10])
print(data[5])
Enter fullscreen mode Exit fullscreen mode
slice data[5:]: []
slice data[1:10]: [2, 3]
Traceback (most recent call last):
  File "/workspace/seo-posts-nov07/test/python/c11_slice_vs_index.py", line 5, in <module>
    print(data[5])
          ~~~~^^^
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

That difference surprises people who "tested" with a slice in the REPL and then shipped a bare index. Fix: know which one you mean. Use a slice when an empty result is fine. Use an index (and a guard) when a missing position is a real error. The data structures tutorial covers list operations in the official Python tutorial.

Reliable fixes checklist

  • Read the caret line: which index expression failed?
  • Print len(seq) and the index you asked for.
  • Prefer enumerate over hand-rolled range(len(...)) when you need both index and value.
  • After pop/remove/split, re-check length before indexing.
  • Pad or reject short CSV / split rows instead of assuming field counts.
  • Use reverse iteration (or a new list) when deleting by index.
  • Remember: slices forgive, indexes do not.
  • Catch IndexError narrowly at boundaries; do not swallow Exception.

deque, used earlier for queue-style pops, lives in the collections module. Reach for it when you need cheap left-side operations; keep a plain list when random access by index is the main job.

Frequently asked questions

What does IndexError: list index out of range mean?

It means you used square brackets to read a position that does not exist on a list (or another sequence). For a list of length n, valid indexes are 0 through n-1 and -1 through -n.

Why does my loop raise IndexError after a few iterations?

Usually the list shrank (pop/remove) while the loop still used the old length, or you started range at 1 and used those numbers as indexes. Print len and the index each time the body runs.

How do I safely get the first or last item?

Check if the list is non-empty, then use lst[0] or lst[-1]. Alternatively use next(iter(lst), default) for the first item without indexing.

Why does a slice work but lst[i] crash?

Slices clamp to the sequence bounds and can return []. A single index must be in range or Python raises IndexError.

Is IndexError the same as string index out of range?

Same exception class, different message. Strings, lists, tuples and other sequences all raise IndexError when a subscript is out of range.

Should I catch IndexError or check len first?

If missing indexes are normal, check len or use a helper with a default. If they should never happen at that layer, let IndexError propagate or catch it narrowly at a boundary.

How is IndexError different from KeyError?

IndexError is for out-of-range sequence positions. KeyError is for missing keys on mappings (dicts). They look similar in a traceback but the fixes differ.

Does enumerate prevent IndexError?

It prevents the common off-by-one from hand-rolled ranges, because it only yields real indexes. It will not save you if you later index with a different, invalid number.

Last checked: 7 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.


Originally published on mfaysal.com.

I'm learning cybersecurity on TryHackMe — follow my progress at tryhackme.com/p/mfaysal.dev.

Top comments (0)