List index out of range in Python: causes and fixes
List index out of range means you asked for a position the list does not have. Six causes, including the one that prints most of your output first.
IndexError: list index out of range means you asked a list for a position it does not have. Python lists start at 0, so a list of three items has positions 0, 1 and 2. Position 3 does not exist, and asking for it raises rather than returning nothing.
nums = [10, 20, 30]
print(nums[3])
# IndexError: list index out of range
The single most useful consequence of zero-indexing: the last valid index is always len(list) - 1, so len() itself is never a valid index.
nums = [10, 20, 30]
print(nums[len(nums)])
# IndexError: list index out of range
Every output on this page came from Python 3.13 running under Pyodide 0.28.3.
The message names the type
Worth noticing, because it tells you what you are actually holding:
s = 'abc'
print(s[5])
# IndexError: string index out of range
t = (1, 2)
print(t[9])
# IndexError: tuple index out of range
If you expected a list and the message says string index out of range, your variable is a string. That is often the real bug, and the index is just where it surfaced.
Six causes
1. Off by one in range(len(...)). The classic, and it has a distinctive symptom covered in the next section.
for i in range(len(nums) + 1):
print(nums[i])
2. The list is empty. An empty list has no valid index at all, not even 0.
rows = []
print(rows[0])
# IndexError: list index out of range
3. A filter matched nothing. This is the empty-list case wearing a disguise, and it passes every test where at least one item matches.
nums = [1, 3, 5]
evens = [n for n in nums if n % 2 == 0]
print(evens[0])
# IndexError: list index out of range
The list comprehension is correct. There simply are no even numbers in [1, 3, 5], so evens is [].
4. Removing items while looping over the list. The list shrinks underneath the loop, and the indexes computed at the start stop being valid.
nums = [1, 2, 3, 4]
for i in range(len(nums)):
if nums[i] % 2 == 0:
nums.remove(nums[i])
# IndexError: list index out of range
range(len(nums)) was fixed at 0, 1, 2, 3 when the loop began. By the time i reaches 3, two items have been removed and the list only has positions 0 and 1. Build a new list instead:
nums = [n for n in nums if n % 2 != 0]
5. Data that is shorter than you assumed. Splitting a line and reading parts[2] works until one row has fewer fields. Files with a blank final line cause this constantly.
6. A negative index on an empty list. -1 normally means "last item", which is a safe way to reach the end. It is not safe when there is no end:
nums = [10, 20, 30]
print(nums[-1]) # 30
rows = []
print(rows[-1])
# IndexError: list index out of range
The one that prints first, then fails
The off-by-one case behaves in a way that misleads people, and it is worth seeing the actual output:
nums = [10, 20, 30]
for i in range(len(nums) + 1):
print(nums[i])
10
20
30
IndexError: list index out of range
Every value printed correctly. The loop did all of its real work, and only then crashed on an iteration that should never have run.
This matters because it changes what the error is telling you. Output that looks complete followed by an IndexError usually means the loop ran one step too many, not that your data or your logic is wrong. Nothing before the error is suspect.
range(len(nums)) produces 0, 1, 2 and is correct. Adding + 1 makes it 0, 1, 2, 3, and 3 is one past the end.
Print the length before you guess
When the cause is not obvious, two values settle it immediately:
print(len(nums), i)
The list's length and the index you used. If the index is greater than or equal to the length, you have found the bug without reading any more code.
Put that line immediately before the failing access rather than at the top of the function. Lists change as a program runs, and the length at the top may not be the length at the moment of failure, which is exactly the situation in the remove-while-looping case above.
For data that arrived from a file or an API, print the whole thing once:
print(repr(rows))
repr() rather than print(rows), because it shows quotes and escapes. That distinguishes an empty list from a list holding one empty string, and it makes a trailing blank line in a parsed file visible instead of invisible. Those two cases produce identical-looking output otherwise and have different fixes.
The fixes, in the order to prefer them
Loop over the list, not over indexes. If you do not need the position, do not create one:
for n in nums:
print(n)
This cannot go out of range. There is no index to get wrong.
Use enumerate() when you need the position too.
for i, n in enumerate(nums):
print(i, n)
# 0 10
# 1 20
# 2 30
enumerate counts as it goes, so the index always matches a real item. It is the direct replacement for range(len(...)) and the reason that pattern is rarely needed. There is more on it in how to loop with an index.
Check before you reach in, when the index comes from outside your code:
if len(rows) > 0:
print(rows[0])
Catch it when an empty result is a normal outcome rather than a bug:
try:
first = rows[0]
except IndexError:
first = None
Use this one sparingly. A try around a genuine off-by-one hides the mistake instead of fixing it.
Nested lists hide which index failed
With a grid, the message is no help at all. These two mistakes are different, and Python reports them identically:
grid = [[1, 2], [3, 4]]
print(grid[5][0]) # IndexError: list index out of range
print(grid[0][5]) # IndexError: list index out of range
The first asked for row 5 of a two-row grid. The second asked for column 5 of a two-column row. Same message, different bug, and no indication of which index was the problem.
Split the access to find out:
row = grid[5] # fails here if the row is missing
value = row[0] # fails here if the column is missing
Whichever line raises tells you which index is wrong. This is the fastest way through any chained subscript, and it applies equally to data parsed from JSON where data["items"][3]["name"] can fail at several points.
Ragged data makes this worse, because the rows are not all the same length:
for row in grid:
print(len(row))
Printing the lengths first is often quicker than reasoning about where the data came from.
The same mistake on other types
Reaching for something that is not there raises a different exception depending on what you reached into. Recognising which one you got tells you what kind of container you are holding:
| Code | Exception |
|---|---|
nums[3] on a 3-item list |
IndexError: list index out of range |
rows.pop() on [] |
IndexError: pop from empty list |
user['age'] on a dict without it |
KeyError: 'age' |
nums.index(9) when 9 is absent |
ValueError: 9 is not in list |
pop from empty list is worth noticing because it names the operation rather than the index, which makes it easier to find than the generic message.
KeyError is the dictionary equivalent, and it has a clean fix that lists do not have:
user = {'name': 'Ada'}
print(user.get('age')) # None
print(user.get('age', 0)) # 0
.get() returns None, or a default you choose, instead of raising. There is no list method that does this, which is why the list version needs a length check, a slice, or a try.
ValueError from .index() catches people who expect it to return -1 the way some other languages do. It does not. If you need to know whether an item is present, test with in first:
if 9 in nums:
print(nums.index(9))
Slices never raise
This is the most useful thing on this page, and it is missing from most explanations:
nums = [10, 20, 30]
print(nums[1:99]) # [20, 30]
print(nums[99:]) # []
Neither of those is an error. A slice that runs past the end is silently trimmed to whatever exists, and a slice starting past the end gives an empty list.
So nums[0] on an empty list raises, and nums[0:1] on the same list returns []. When you want "the first item if there is one", a slice expresses it without a length check:
first = nums[:1] # [] or [10]
The flip side is that slices will not tell you when your assumptions are wrong. An index is loud and a slice is quiet, so pick the one that matches what an empty result means in your program. If missing data is a bug, let it raise.
Want to build the intuition on real lists? Start with the Python track and get the off-by-one errors somewhere they cost nothing.
More from the blog
Each child in a list should have a unique key prop
The unique key prop warning in React still renders your list. Here is the bug it predicts, shown with real output from two runs.
Read moreToo many re-renders in React: the one real cause
Too many re-renders always means a state update running during render. Here is how to spot which of the four shapes you have written.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.