1. Symptoms
When a Python IndexError occurs, your program terminates immediately with an error traceback. The error message indicates which type of sequence caused the problem and the invalid index that was accessed.
# Example error output
Traceback (most recent call last):
File "example.py", line 5, in <module>
value = my_list[10]
IndexError: list index out of range
---
Common error message variations include:
```python
# List index out of range
numbers = [1, 2, 3]
print(numbers[5])
# Tuple index out of range
coordinates = (10, 20, 30)
print(coordinates[-100])
# String index out of range
text = "Hello"
print(text[10])
# Negative index out of range
items = []
print(items[-1])
The symptoms you might observe include:
- Program crash: The script terminates abruptly without completing execution
- Unexpected termination in loops: Your loop crashes on an iteration you expected to work
- Function returns nothing: A function accessing an index may fail before returning a value
- Partial results: If processing a list, you may get partial output before the crash occurs
2. Root Cause
The IndexError in Python occurs when you attempt to access an index position that does not exist within a sequence object. Python sequences are zero-indexed, meaning the first element is at index 0, not index 1.
Understanding the root causes helps prevent these errors:
1. Index exceeds sequence length
Python indices range from 0 to len(sequence) - 1. Accessing an index at or beyond the length triggers an IndexError.
fruits = ["apple", "banana", "cherry"]
# Valid indices: 0, 1, 2
# Invalid: 3, 4, 5, ...
print(fruits[3]) # IndexError: list index out of range
2. Empty sequence access
Any index access on an empty list, tuple, or string fails because no valid indices exist.
empty_list = []
print(empty_list[0]) # IndexError: list index out of range
3. Off-by-one errors in loops
Using range() with incorrect start, stop, or step values commonly causes this error.
data = [10, 20, 30, 40, 50]
# Wrong: iterating one past the end
for i in range(0, len(data) + 1):
print(data[i]) # Crashes on last iteration
4. Negative index magnitude exceeds length
While negative indices are valid (they count from the end), their absolute value cannot exceed the sequence length.
items = ["a", "b"]
print(items[-3]) # IndexError: list index out of range
5. Modifying list during iteration
Changing a list’s size while iterating invalidates previously computed indices.
numbers = [1, 2, 3, 4, 5]
for i in range(len(numbers)):
if numbers[i] > 2:
numbers.pop(i) # Modifies list, causes index issues
6. Index calculated from dynamic data
When indices come from calculations, user input, or external sources, incorrect values can exceed valid bounds.
def get_middle_element(lst):
middle_index = len(lst) // 2
return lst[middle_index]
get_middle_element([]) # IndexError on empty list
3. Step-by-Step Fix
Here are the primary methods to fix IndexError occurrences:
Method 1: Bounds Checking Before Access
Always verify the index exists before accessing it.
Before:
def get_element(lst, index):
return lst[index]
my_list = [1, 2, 3]
result = get_element(my_list, 10)
After:
def get_element(lst, index):
if index < len(lst) and index >= 0:
return lst[index]
else:
raise ValueError(f"Index {index} out of bounds for list of length {len(lst)}")
my_list = [1, 2, 3]
result = get_element(my_list, 10)
# Now raises descriptive ValueError instead of IndexError
Method 2: Using Exception Handling
Catch the IndexError and handle it gracefully.
Before:
def find_nth_item(items, n):
return items[n] # Crashes if n is invalid
print(find_nth_item(["a", "b", "c"], 5))
After:
def find_nth_item(items, n, default=None):
try:
return items[n]
except IndexError:
print(f"Warning: Index {n} not found, returning default")
return default
print(find_nth_item(["a", "b", "c"], 5, "not found"))
# Output: Warning: Index 5 not found, returning default
# not found
Method 3: Correcting Loop Range
Fix off-by-one errors in loop iterations.
Before:
data = [100, 200, 300, 400]
# Wrong range causes crash
for i in range(0, len(data) + 1):
print(data[i])
After:
data = [100, 200, 300, 400]
# Correct range: 0 to len(data) - 1
for i in range(len(data)):
print(data[i])
# Alternative: use direct iteration
for item in data:
print(item)
# Alternative: enumerate when you need indices
for i, item in enumerate(data):
print(f"Index {i}: {item}")
Method 4: Safe List Access with .get() Equivalent
Use the .get() method on dictionaries, or implement similar safety for lists.
Before:
def get_user_age(users, user_id):
return users[user_id]["age"] # Crashes if user_id invalid
users = [{"id": 1, "age": 25}, {"id": 2, "age": 30}]
print(get_user_age(users, 5))
After:
def get_user_age(users, user_id, default=None):
for user in users:
if user["id"] == user_id:
return user["age"]
return default
users = [{"id": 1, "age": 25}, {"id": 2, "age": 30}]
print(get_user_age(users, 5, default=0)) # Returns 0
Method 5: Using Slice Access
Slices return empty sequences instead of raising errors.
Before:
text = "Hello"
char = text[100] # IndexError
After:
text = "Hello"
# Using slice with default value
char = text[100:101] or None # Returns None instead of crashing
print(char) # None
# More explicit approach
def safe_char(text, index):
if -len(text) <= index < len(text):
return text[index]
return None
print(safe_char(text, 100)) # None
print(safe_char(text, -1)) # 'o'
Method 6: Checking for Empty Sequences
Handle empty sequences explicitly.
Before:
def first_element(items):
return items[0]
print(first_element([])) # IndexError
After:
def first_element(items, default=None):
if items: # Truthiness check - False for empty list
return items[0]
return default
print(first_element([])) # None
print(first_element([], "empty")) # "empty"
print(first_element([42])) # 42
4. Verification
After applying a fix, verify it works correctly across different scenarios:
# Comprehensive test suite for list access functions
def safe_get(lst, index, default=None):
"""Safely retrieve element at index, returning default if invalid."""
try:
return lst[index]
except IndexError:
return default
# Test cases
test_cases = [
([1, 2, 3], 1, 2), # Normal case
([1, 2, 3], 0, 1), # First element
([1, 2, 3], -1, 3), # Last element
([1, 2, 3], 5, None), # Out of bounds
([1, 2, 3], -10, None), # Far negative index
([], 0, None), # Empty list
(["a"], 0, "a"), # Single element
("hello", 4, "o"), # String access
((), 0, None), # Empty tuple
]
for lst, idx, expected in test_cases:
result = safe_get(lst, idx)
status = "✓" if result == expected else "✗"
print(f"{status} safe_get({lst}, {idx}) = {result} (expected {expected})")
Expected output:
✓ safe_get([1, 2, 3], 1) = 2 (expected 2)
✓ safe_get([1, 2, 3], 0) = 1 (expected 1)
✓ safe_get([1, 2, 3], -1) = 3 (expected 3)
✓ safe_get([1, 2, 3], 5) = None (expected None)
✓ safe_get([1, 2, 3], -10) = None (expected None)
✓ safe_get([], 0) = None (expected None)
✓ safe_get(['a'], 0) = 'a' (expected 'a')
✓ safe_get('hello', 4) = 'o' (expected 'o')
✓ safe_get((), 0) = None (expected None)
Additional verification techniques:
# Use assertions for critical code paths
def process_list_item(items, index):
result = safe_get(items, index)
assert result is not None, f"Failed to get item at index {index}"
return result * 2
# Edge case testing
print(process_list_item([10, 20, 30], 1)) # Works: returns 40
5. Common Pitfalls
Avoid these frequent mistakes when working with indices:
Pitfall 1: Confusing length with last index
# Wrong understanding
data = [1, 2, 3]
# Length is 3
# Valid indices: 0, 1, 2
# NOT 0, 1, 2, 3
# Common mistake
last_item = data[len(data)] # IndexError!
Correct approach:
last_item = data[len(data) - 1] # Correct: data[2]
# Or use negative indexing
last_item = data[-1] # Correct and cleaner
Pitfall 2: Assuming sorted input
If your code expects sorted data for index-based operations, validate this assumption.
def binary_search(arr, target):
"""Binary search assumes sorted input."""
left, right = 0, len(arr)
while left < right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid
return -1
# Adding validation
def binary_search_safe(arr, target):
if arr != sorted(arr):
raise ValueError("Array must be sorted for binary search")
return binary_search(arr, target)
Pitfall 3: Forgetting negative indices wrap
Negative indices count from the end, but -len(sequence) - 1 is still invalid.
data = [1, 2, 3]
# These are valid:
print(data[-1]) # 3
print(data[-3]) # 1
# This is invalid:
print(data[-4]) # IndexError
Pitfall 4: Modifying lists during iteration
# Problematic code
numbers = [1, 2, 3, 4, 5]
for i in range(len(numbers)):
if numbers[i] % 2 == 0:
numbers.pop(i) # Modifies list during iteration
# Better approaches:
# Option 1: Iterate backwards
numbers = [1, 2, 3, 4, 5]
for i in range(len(numbers) - 1, -1, -1):
if numbers[i] % 2 == 0:
numbers.pop(i)
# Option 2: List comprehension
numbers = [1, 2, 3, 4, 5]
numbers = [n for n in numbers if n % 2 != 0]
Pitfall 5: Not handling file line access safely
# Risky approach
with open("data.txt") as f:
lines = f.readlines()
print(lines[100]) # May not have 100 lines
# Safer approach
with open("data.txt") as f:
lines = f.readlines()
if len(lines) > 100:
print(lines[100])
else:
print(f"File has only {len(lines)} lines")
Pitfall 6: Over-relying on try-except
While exception handling is useful, excessive use can mask other bugs.
# Poor practice: catching too broadly
try:
result = items[calculated_index]
except Exception:
result = None
# Better: catch specifically what you expect
try:
result = items[calculated_index]
except IndexError:
result = None
6. Related Errors
Several errors are closely related to or commonly confused with IndexError:
KeyError: Occurs when accessing dictionary keys that don’t exist. While IndexError applies to sequences, KeyError applies to mappings.
# KeyError example
users = {"alice": 25, "bob": 30}
print(users["charlie"]) # KeyError: 'charlie'
StopIteration: Raised when an iterator has no more elements. Can occur when manually advancing iterators.
# StopIteration example
it = iter([1, 2, 3])
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
print(next(it)) # StopIteration
TypeError: Occurs when an operation is applied to an inappropriate type. Accessing a non-sequence with indexing produces this error.
# TypeError with index access
number = 42
print(number[0]) # TypeError: 'int' object is not subscriptable
ValueError: While not directly related, often used when validating index values before access.
# ValueError for invalid index
def get_item(items, index):
if index < 0:
raise ValueError("Index cannot be negative")
return items[index]
Understanding these related errors helps in debugging. When you encounter an IndexError, check:
- Is the sequence actually a sequence? (Not
None, notint, etc.) - Is the index within bounds?
- Does the sequence have content?
- Are you accidentally modifying the sequence during iteration?
The IndexError is fundamental to Python’s sequence handling. Mastering index bounds and the various safe access patterns will eliminate most occurrences of this error in your code.