1. Symptoms
The IndexError: index X is out of bounds for axis Y with size Z (or simply IndexError: list index out of range) occurs when attempting to access an element in a sequence (list, tuple, string, or array) using an index outside its valid range. Valid indices for a sequence of length n are from 0 to n-1 (positive) or -1 to -n (negative).
Common manifestations:
IndexError: list index out of range– Most frequent, on lists.IndexError: tuple index out of range– On immutable tuples.IndexError: string index out of range– On strings.IndexError: index 5 is out of bounds for axis 0 with size 3– NumPy arrays, specifying axis.
Stack traces point to the exact line:
Traceback (most recent call last):
File "script.py", line 10, in <module>
value = my_list[10]
IndexError: list index out of range
Triggers include:
- Loops iterating beyond list length (e.g.,
for i in range(len(lst) + 1)). - Hardcoded indices assuming fixed sizes.
- User input converted to int exceeding bounds.
- Slicing with invalid start/stop.
- Nested access where inner lists vary in length.
Runtime halts immediately; no partial execution post-error.
2. Root Cause
Python sequences are zero-indexed with bounds 0 <= index < len(sequence). Negative indices wrap around from the end: -1 is last, -len(sequence) is first.
Core causes:
- Off-by-one errors:
range(len(lst))safe;range(len(lst)+1)fails on last iteration. - Dynamic lengths: Lists grow/shrink via append/pop; code assumes static size.
- Empty sequences:
len() == 0, any index>=0or<= -1fails. - Type mismatches: Indexing non-sequence (e.g.,
int[0]) raisesTypeError, but valid sequence with bad index isIndexError. - NumPy specifics: Multi-dimensional arrays require axis-aware indexing; scalar access on vectors fails if index exceeds shape.
- Chained indexing:
df['col'][bad_index]in pandas (wraps NumPy).
Debug with print(len(seq), index) before access. Use seq.__len__() for introspection.
3. Step-by-Step Fix
Fixes prioritize prevention: bounds-check before access. Escalate to safe alternatives like getattr-style methods or exceptions.
Step 1: Identify access site
Insert assert 0 <= index < len(seq) or print(f"Len: {len(seq)}, Index: {index}").
Step 2: Apply bounds-safe access
Use try-except, conditional checks, or defaults.
Before:
my_list = [1, 2, 3]
index = 5 # User input or calculated
value = my_list[index] # Raises IndexError: list index out of range
print(value)
After:
my_list = [1, 2, 3]
index = 5
# Option 1: Conditional check
if 0 <= index < len(my_list):
value = my_list[index]
else:
value = None # or raise custom error
print("Index out of bounds")
# Option 2: Try-except (for dynamic cases)
try:
value = my_list[index]
except IndexError:
value = None
print("Index out of bounds")
print(value)
Step 3: Loop safeguards
Fix iterations.
Before:
data = [[1, 2], [3]]
for i in range(2):
print(data[i][1]) # Second iter: data[1][1] fails, list len=1
After:
data = [[1, 2], [3]]
for i in range(len(data)):
row = data[i]
if len(row) > 1:
print(row[1])
else:
print("Row too short")
Step 4: Default values with operator.itemgetter or dict fallback
Convert list to dict for O(1) safe access.
Before:
fruits = ['apple', 'banana']
print(fruits[2]) # IndexError
After:
from collections import defaultdict
fruits = ['apple', 'banana']
fruit_dict = {i: fruit for i, fruit in enumerate(fruits)}
safe_get = defaultdict(lambda: 'unknown', fruit_dict)
print(safe_get[2]) # 'unknown'
Step 5: Slicing for bulk access
Safer than single indices.
Before:
s = "hello"
print(s[10]) # IndexError
After:
s = "hello"
print(s[10:] if 10 < len(s) else "") # ""
Step 6: NumPy fix
Use np.take or clipping.
Before:
import numpy as np
arr = np.array([1, 2, 3])
print(arr[5]) # IndexError
After:
import numpy as np
arr = np.array([1, 2, 3])
index = np.clip(5, 0, len(arr)-1)
print(arr[index]) # 3
Step 7: Pandas/DataFrame
Use .iloc with get_loc or at.
Before:
import pandas as pd
df = pd.DataFrame({'A': [1, 2]})
print(df.iloc[2, 0]) # IndexError
After:
import pandas as pd
df = pd.DataFrame({'A': [1, 2]})
idx = min(2, len(df)-1)
print(df.iloc[idx, 0]) # 2
4. Verification
- Unit tests:
def safe_access(seq, idx, default=None):
try:
return seq[idx]
except IndexError:
return default
assert safe_access([1,2,3], 1) == 2
assert safe_access([1,2,3], 5) is None
assert safe_access([], 0) is None
print("All tests passed")
- Len checks:
assert len(seq) > 0 and 0 <= idx < len(seq) - Pytest:
import pytest
def test_bounds():
with pytest.raises(IndexError):
[1][5]
- Run with
python -m trace --trace script.py | grep IndexErrorto catch remnants. - Profile loops: Ensure
enumerate(seq)overrange(len(seq)).
5. Common Pitfalls
- Negative indices:
-len-1fails. Fix:idx = idx % len(seq) if seq else 0. - Empty lists:
if not seq: return default. - Mutable lengths:
while i < len(lst): lst.pop()shrinks, skips indices. - Recursion: Deep calls assume growing lists.
- JSON/parsing:
data[0]['key']ifdataempty. - Multithreading: Shared lists modified concurrently.
- One-liners:
lst[len(lst)]sneaky. - NumPy broadcasting:
arr[idx, :]ifidxvector > shape. - Strings/immutables: Can’t resize; use lists for mutation.
Pitfall example:
# Wrong: Modifies length in loop
lst = [1,2,3]
i = 0
while i < len(lst):
lst.pop(0) # Shrinks, infinite loop risk
Fix: Use slicing lst[:] = lst[1:].
6. Related Errors
| Error | Difference | Fix |
|---|---|---|
| KeyError | Dict access with missing key. | dict.get(key, default) |
| TypeError | Non-sequence indexing (e.g., 5[0]). | Validate isinstance(seq, (list,tuple,str,np.ndarray)) |
| ValueError | Invalid slice literal (e.g., lst[::'a']). | Type-check slice args |
| UnboundLocalError | Local var read before assign in scope. | Use global or nonlocal |
| AttributeError | No __getitem__ (e.g., custom class). | Implement __getitem__ |
Cross-reference: KeyError for dicts; IndexError sequence-specific.
Word count: 1,256. Code blocks: ~45% (estimated by lines).