Fix Common Errors

IndexError in Python: Causes & Solutions

Learn what causes IndexError in Python, how to read stack traces, and strategies to prevent list index out of range errors in production.

An IndexError list index out of range is one of Python's most common runtime errors. It happens when you try to access a list element at an index that doesn't exist—asking for the 10th item when the list only has 5. While straightforward in principle, it's a beginner trap that sneaks past experienced developers too, especially in loops, conditional logic, or code handling dynamic data from APIs or databases. The error feels simple, but the cause is often buried in subtle logic, off-by-one mistakes, or assumptions about data that doesn't hold.

This guide walks you through what triggers the error, how to diagnose it from a stack trace, real patterns that cause it, and defensive strategies to prevent it entirely. We'll also cover how to safely handle it at runtime and integrate error tracking to catch it in production before users do.

What Causes IndexError: List Index Out of Range

In Python, lists are zero-indexed. The first element is at index 0, the second at 1, and so on. If a list has n elements, valid indices range from 0 to n-1.

fruits = ["apple", "banana", "cherry"]
print(fruits[0])  # "apple" — valid
print(fruits[2])  # "cherry" — valid
print(fruits[3])  # IndexError: list index out of range

The error is raised immediately when you attempt the invalid access. There are no warnings or defaults; Python doesn't return None or skip the line—it stops and raises an exception.

Negative indices work backward: fruits[-1] is "cherry", fruits[-2] is "banana". This is valid and won't raise an IndexError unless the magnitude exceeds the list length. For example, fruits[-4] will raise IndexError.

How to Read the Stack Trace

When you encounter an IndexError in development, the traceback is your map. Learning to read a stack trace is essential for finding the culprit line.

Traceback (most recent call last):
  File "script.py", line 12, in <module>
    value = data[index]
IndexError: list index out of range

The line value = data[index] is where the access happens. The error doesn't tell you what index was used or why it was out of bounds—you must read your code. Print or inspect index and len(data) just before the failing line, or use a debugger.

In production, this traceback should be captured and logged. Error tracking services like LightTrace help you aggregate and debug production errors so you can see how often an IndexError occurs and on which code paths.

Common Scenarios & How to Fix Them

Off-by-One Errors

The classic mistake: iterating one step too far.

# Wrong
items = [10, 20, 30]
for i in range(len(items) + 1):  # 0, 1, 2, 3 — out of bounds!
    print(items[i])

# Right
for i in range(len(items)):  # 0, 1, 2
    print(items[i])

# Better: just iterate the list directly
for item in items:
    print(item)

The fix is usually to iterate over the collection, not its indices. If you need indices, use enumerate():

items = [10, 20, 30]
for idx, item in enumerate(items):
    print(f"{idx}: {item}")

Iterating While Modifying

Removing or inserting elements during iteration can shift indices and cause misses or out-of-bounds access.

# Problematic
numbers = [1, 2, 3, 4, 5]
for i in range(len(numbers)):
    if numbers[i] % 2 == 0:
        numbers.pop(i)  # Shifts remaining elements; next iteration may fail

# Fix: iterate over a copy, modify the original
numbers = [1, 2, 3, 4, 5]
for num in numbers[:]:  # Iterate over a shallow copy
    if num % 2 == 0:
        numbers.remove(num)  # Safe: modify original

# Or use list comprehension (cleaner)
numbers = [num for num in numbers if num % 2 != 0]

Empty Lists or Missing Keys in Nested Structures

Assuming a list has elements when it might be empty is another common source.

# Dangerous
data = fetch_data_from_api()  # What if this returns an empty list?
first_item = data[0]  # IndexError if empty

# Safe
data = fetch_data_from_api()
if data:
    first_item = data[0]
else:
    print("No data available")

# Or use indexing with a default
first_item = data[0] if data else None

For nested structures (lists of dicts), always check depth:

# Risky
user_name = response["users"][0]["name"]  # What if "users" is empty?

# Safer
users = response.get("users", [])
if users:
    user_name = users[0].get("name", "Unknown")

Prevention Strategies

Use built-in methods instead of manual indexing. Methods like first(), last(), pop(), and slicing handle edge cases gracefully:

# Instead of:
if len(items) > 0:
    first = items[0]

# Use:
first = items[0] if items else None

# Or a helper
def safe_first(lst):
    return lst[0] if lst else None

Validate and sanitize user input. If an index comes from a user, request, or external API, validate bounds:

def get_item(items, index):
    if not isinstance(index, int) or index < 0 or index >= len(items):
        raise ValueError(f"Invalid index {index} for list of length {len(items)}")
    return items[index]

Use type hints and assertions during development. They won't prevent the error at runtime but clarify intent:

from typing import List

def process_records(records: List[dict]) -> str:
    assert len(records) > 0, "At least one record is required"
    return records[0]["id"]

Consider using a library like more-itertools or writing helper functions for safe indexing. Many teams build a small utils.py with safe_first(), safe_last(), and safe_get_index() functions to reduce boilerplate.

Catching and Handling IndexError Gracefully

Sometimes an IndexError is expected and recoverable. Catch it explicitly:

try:
    item = items[index]
except IndexError:
    item = default_value
    print(f"Index {index} out of range; using default")

However, overly broad exception handling can hide bugs:

# Avoid this — catches other IndexErrors unintentionally
try:
    item = items[index]
    item.process()  # If process() raises IndexError, you'll catch it
except IndexError:
    item = default_value

Be specific: catch IndexError only at the line where it's expected.

Detecting and Tracking IndexError in Production

In production, an IndexError often signals a bug or unexpected input.

Never silently swallow IndexError without logging. You need visibility into how often it occurs and from which code path.
Integrate an error tracker like LightTrace to capture the full context:

import sentry_sdk

sentry_sdk.init(
    dsn="https://<key>@light-trace.robomiri.com/1",
    traces_sample_rate=1.0
)

try:
    item = items[index]
except IndexError as e:
    sentry_sdk.capture_exception(e)
    # Log context: what index, what list length
    sentry_sdk.set_context("data", {"index": index, "list_length": len(items)})
    raise  # Re-raise so the caller knows

Once captured, LightTrace groups and fingerprints the errors, alerts you to new occurrences, and lets you understand your error tracking strategy across your application.

Summary

IndexError: list index out of range boils down to a mismatch between the index you're using and the list's length. The fix is almost always prevention: iterate the list directly, validate bounds before access, handle empty inputs explicitly, and avoid modifying lists during iteration. When an IndexError does occur in production, instrument it with error tracking so you catch and fix the root cause before it affects more users.

Start tracking errors in minutes

See production errors like IndexError at a glance. Start free with LightTrace—connect any Sentry SDK and get a fast, mobile-first dashboard with full stack traces, breadcrumbs, and source maps.

Fix your next production error faster

Point any Sentry SDK at LightTrace — free up to 5,000 events/month.