Fix: Python IndexError: list index out of range
Part of: Python Errors
Quick Answer
How to fix Python IndexError list index out of range caused by empty lists, off-by-one errors, wrong loop bounds, deleted elements, and negative indexing mistakes.
The Error
You run a Python script and get:
Traceback (most recent call last):
File "app.py", line 4, in <module>
print(items[5])
IndexError: list index out of rangeOr variations:
IndexError: string index out of rangeIndexError: tuple index out of rangeYou tried to access an element at an index that does not exist. The list (or string, or tuple) does not have that many elements.
Why This Happens
Python sequences are zero-indexed. A list with 5 elements has valid indices 0 through 4. Accessing index 5 is out of range:
items = ["a", "b", "c", "d", "e"] # len = 5
items[0] # "a" — first element
items[4] # "e" — last element
items[5] # IndexError — no 6th elementCommon causes:
- Empty list. The list has no elements, so any index fails.
- Off-by-one error. Using
len(items)as an index instead oflen(items) - 1. - Loop modifying the list. Removing elements while iterating changes the list length.
- Hardcoded index. Assuming data always has a certain number of elements.
- API response with fewer items than expected. The response returned 2 items but you access
response[3].
Most of these come down to one underlying mistake: trusting the shape of upstream data without checking it. A list returned from a database query, an HTTP response, a CSV parse, or a str.split() can all be empty or shorter than expected. Code written for the happy path assumes the data is always there. The day the upstream system has a hiccup — an empty result set, a malformed row, an API change — is the day IndexError shows up.
The error is also one of Python’s more honest failure modes. Unlike languages where reading past the end of an array returns garbage memory, Python raises immediately. That sounds good — you find the bug fast — but it also means the bug surfaces in places far from the real cause. A scheduled batch job that crashes at 3am because yesterday’s data feed dropped a column is not a Python problem; it is a contract problem between two systems. The fix usually lives at the boundary, not at the line that raised.
In Production: Incident Lens
IndexError in production almost always signals an upstream data contract change, not a coding bug. The code worked yesterday. The data shape today is different. Reading the error this way reframes the entire incident.
- How it surfaces: In synchronous request handlers, it surfaces as a 500 error on a single endpoint, often only for some users (the ones whose data hit the bad shape). In batch jobs, ETL pipelines, or Celery/RQ workers, it surfaces as a hard crash that may halt the entire job, leaving partial state. Sentry/Rollbar usually catches it within seconds of the first occurrence with a clean stack trace pointing at the indexing line.
- Blast radius: Per-request for web handlers — only requests that touch the affected code path fail. For batch jobs, blast radius is much wider: a single bad row can poison the entire run if the job is not idempotent or has no per-record error handling. The worst case is silent data corruption upstream: code that should have raised
IndexErrorinstead read a wrong value because someone “fixed” it with[0] if x else Nonewithout validating that the element at index 0 actually meant what they thought. - What catches it: Error tracking (Sentry, Rollbar, Honeybadger) is the fastest signal. SLO error-budget burn alerts fire if the affected endpoint is high-traffic. For batch jobs, watch for job-duration anomalies and partial-output alerts — a job that finished in 30 seconds instead of 30 minutes either succeeded brilliantly or died early.
- Recovery sequence: For web handlers, rollback only if the new code introduced a stricter assumption. If the data shape changed independently of the deploy, rolling back will not help — the same
IndexErrorwill happen on the old code too. Forward-fix with a defensive guard, redeploy, then trace back to fix the upstream contract. For batch jobs, requeue failed records once a fix is shipped. - Postmortem preventive: The durable control is schema validation at the boundary, not bounds checks scattered through business logic. Use Pydantic,
dataclasseswith__post_init__, orjsonschemato validate inputs at the system edge. If the shape is wrong, fail loudly there with a clear message instead of silently propagating a malformed list into code that crashes ten layers deeper. Add contract tests against the upstream API so a breaking change shows up in CI, not production.
Fix 1: Check the List Length First
Before accessing an index, verify the list has enough elements:
items = get_results()
if len(items) > 0:
first = items[0]
else:
first = NoneFor a specific index:
if len(items) > 3:
fourth = items[3]Using a helper function:
def safe_get(lst, index, default=None):
return lst[index] if -len(lst) <= index < len(lst) else default
items = [10, 20, 30]
safe_get(items, 5) # None
safe_get(items, 5, 0) # 0
safe_get(items, 1) # 20Pro Tip: For dictionaries, use
.get()with a default value. Lists do not have.get(), so you need the length check or try/except. If you frequently need safe list access, consider a helper function or usingnext(iter(items), default)for the first element.
Fix 2: Fix Off-By-One Errors
The most common logic error. Lists are zero-indexed, so the last valid index is len(items) - 1:
Broken:
items = [10, 20, 30]
# Wrong — index 3 does not exist
for i in range(len(items) + 1):
print(items[i]) # IndexError when i = 3Fixed:
for i in range(len(items)):
print(items[i])Better — iterate directly:
for item in items:
print(item)Access the last element safely:
items = [10, 20, 30]
last = items[-1] # 30 — negative indexing
last = items[len(items) - 1] # 30 — manual (avoid this)Negative indices count from the end: -1 is the last element, -2 is second to last. But items[-1] still raises IndexError on an empty list.
Fix 3: Handle Empty Lists
Empty lists cause IndexError on any index access:
results = []
first = results[0] # IndexError: list index out of rangeFix with a guard:
results = get_search_results(query)
if results:
first = results[0]
print(f"Top result: {first}")
else:
print("No results found")Fix with a default value:
first = results[0] if results else "No results"Fix with try/except:
try:
first = results[0]
except IndexError:
first = NoneThe if results: check is cleanest for most cases. Use try/except when the empty case is genuinely exceptional.
Fix 4: Fix Loops That Modify Lists
Removing elements while iterating over a list changes the indices:
Broken:
items = [1, 2, 3, 4, 5]
for i in range(len(items)):
if items[i] % 2 == 0:
items.pop(i) # List shrinks, but range doesn't
# IndexError: list index out of rangeWhen you pop index 1 (value 2), the list becomes [1, 3, 4, 5]. Now index 3 is 5, and index 4 does not exist.
Fixed — iterate over a copy:
items = [1, 2, 3, 4, 5]
items = [x for x in items if x % 2 != 0]
# [1, 3, 5]Fixed — iterate in reverse:
items = [1, 2, 3, 4, 5]
for i in range(len(items) - 1, -1, -1):
if items[i] % 2 == 0:
items.pop(i)Iterating in reverse is safe because popping later indices does not affect earlier ones.
Fixed — filter:
items = list(filter(lambda x: x % 2 != 0, items))Common Mistake: Using
for item in items: items.remove(item)to remove elements. This skips elements becauseremove()shifts the remaining elements and the loop’s internal pointer advances past the next item. Always use list comprehension or reverse iteration.
Fix 5: Fix String Indexing
Strings are sequences too, and the same rules apply:
name = "Alice"
name[0] # "A"
name[4] # "e"
name[5] # IndexError: string index out of rangeCommon case — split with fewer parts than expected:
line = "Alice"
parts = line.split(",") # ["Alice"] — only 1 part
name = parts[0] # "Alice"
email = parts[1] # IndexError!Fixed:
parts = line.split(",")
name = parts[0] if len(parts) > 0 else ""
email = parts[1] if len(parts) > 1 else ""Or with unpacking and defaults:
parts = line.split(",")
name, *rest = parts
email = rest[0] if rest else ""If your string parsing involves JSON, see Fix: JSON parse unexpected token for format issues.
Fix 6: Fix Pandas and NumPy Indexing
Pandas DataFrames and NumPy arrays can also raise IndexError:
Pandas — iloc out of range:
import pandas as pd
df = pd.DataFrame({"name": ["Alice", "Bob"]})
df.iloc[5] # IndexError: single positional indexer is out-of-boundsFixed:
if len(df) > 5:
row = df.iloc[5]NumPy:
import numpy as np
arr = np.array([1, 2, 3])
arr[5] # IndexError: index 5 is out of bounds for axis 0 with size 3Fix: Check shape before accessing:
if arr.shape[0] > 5:
value = arr[5]Fix 7: Fix Multithreaded List Access
Multiple threads accessing the same list can cause IndexError if one thread removes elements while another accesses them:
import threading
shared_list = [1, 2, 3, 4, 5]
def worker():
while shared_list:
item = shared_list.pop(0) # Race condition!
process(item)Fixed — use a thread-safe queue:
from queue import Queue
q = Queue()
for item in [1, 2, 3, 4, 5]:
q.put(item)
def worker():
while not q.empty():
item = q.get()
process(item)
q.task_done()Or use a lock:
lock = threading.Lock()
def worker():
with lock:
if shared_list:
item = shared_list.pop(0)
process(item)Fix 8: Debug the Index Error
When the error is not obvious, add debugging:
items = get_data()
index = calculate_index()
print(f"List length: {len(items)}")
print(f"Attempting index: {index}")
print(f"List contents: {items}")
value = items[index]Use enumerate for safe indexed access:
for i, item in enumerate(items):
print(f"Index {i}: {item}")This never raises IndexError because enumerate only yields valid indices.
Still Not Working?
If you have checked all the fixes above:
Check for nested lists. items[0][1] fails if items[0] has fewer than 2 elements, even if items has many elements.
Check for generators vs lists. Generators cannot be indexed:
gen = (x for x in range(10))
gen[0] # TypeError: 'generator' object is not subscriptableConvert to list first: list(gen)[0].
Check for deque maxlen. A collections.deque with maxlen automatically removes old elements, which might make expected indices invalid.
Check for custom __getitem__. If the object is a custom class, its __getitem__ method might raise IndexError for unexpected reasons.
Check for SQLAlchemy result rows. A query that returns zero rows still gives you a Result object. Calling .one() raises NoResultFound, but .first() returns None, and indexing into the result list before checking length raises IndexError. Always check if rows: before rows[0].
Check pagination cursors. If you paginate by index (results[page * size : (page + 1) * size]), slicing past the end is safe — Python returns an empty list. But if you then index into that empty slice (page_results[0]), you crash. Slicing is the right tool; indexing without a length check is not.
Check for protobuf and msgpack deserialization. Binary deserialization formats often produce repeated fields as Python lists. A protobuf repeated field that was empty in the wire format gives you [], not the expected list of items. Code written assuming “this field always has at least one element” breaks the first time the producer sends an empty message.
Check for race conditions on shared mutable state. In a Flask or FastAPI app with a module-level list, two requests can both check if my_list: (both see True), then both call my_list.pop(). The second pop hits an empty list. Use a threading.Lock, switch to collections.deque with appropriate locking, or move state to Redis with atomic operations.
If the error is about dictionary keys rather than list indices, see Fix: Python KeyError. If the value is None and you are trying to subscript it, see Fix: TypeError: ‘NoneType’ object is not subscriptable.
For similar issues with missing attributes on None values, see Fix: AttributeError: ‘NoneType’ has no attribute.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: joblib Not Working — Parallel Backends, Memory Cache, and Pickling Errors
How to fix joblib errors — Parallel n_jobs slower than expected, Memory cache miss, backend loky vs threading vs multiprocessing, pickling lambda not supported, dump load file size, and pytest interference.
Fix: Marshmallow Not Working — Schema Errors, Load vs Dump, and Field Validation
How to fix Marshmallow errors — Schema not validated on dump, ValidationError messages format, unknown field handling, missing vs default, post_load object construction, and Marshmallow 3 to 4 migration.
Fix: Pipenv Not Working — Lock File Generation, Shell Activation, and Dependency Resolution
How to fix Pipenv errors — pipenv lock takes forever, Pipfile.lock not generated, shell activation broken, no virtualenv created, dependency conflict, and migration to uv or Poetry.
Fix: Copier Not Working — Template Updates, Question Conditions, and Migrations
How to fix Copier errors — copier.yml not found, conditional questions not appearing, update breaks generated project, migrations between versions, Jinja vs YAML escaping, and answers file conflict.