IndexError: pop from empty list happens when you call list.pop() on an empty list. This is one of the most common Python bugs in queues, stacks, work-stealing loops, and item-by-item processors. Here are 4 patterns to handle it cleanly.

Minimal reproducer
queue = []
item = queue.pop() # IndexError: pop from empty list
Fix 1: Check length before popping
while queue:
item = queue.pop()
process(item)
# Or as a one-liner with conditional expression
item = queue.pop() if queue else None
The truthiness check if queue is the most Pythonic way to test if a list is non-empty. Same applies to dicts, sets, and strings.
Fix 2: try/except for race conditions
try:
item = queue.pop()
except IndexError:
item = None # or break, or sleep and retry
Use this pattern in multi-threaded code where another thread might pop between your length-check and your pop. The try/except is atomic; the check-then-pop is not.
Fix 3: collections.deque for safe popping with timeout
from collections import deque
queue = deque([1, 2, 3])
# Pop from left (FIFO) or right (LIFO)
while queue:
item = queue.popleft() # O(1), unlike list.pop(0) which is O(n)
process(item)
For producer-consumer patterns, switch to collections.deque (O(1) popleft) or queue.Queue (thread-safe with blocking get).
Fix 4: Sentinel default with no exception
SENTINEL = object()
def safe_pop(lst, default=SENTINEL):
return lst.pop() if lst else default
item = safe_pop(queue)
if item is SENTINEL:
print("Queue empty, nothing to do")
else:
process(item)
When to use each
| Scenario | Best pattern |
|---|---|
| Single-threaded loop | while queue: queue.pop() |
| Multi-threaded queue | queue.Queue with blocking get |
| FIFO from front | collections.deque.popleft() |
| Concurrent producer/consumer | asyncio.Queue or queue.Queue |
Debugging checklist before applying the fix
When your Python script raises IndexError: pop from empty list, run through this quick checklist to find the real cause before jumping to a fix.
- Print the list right before the pop line. If it is
[], you already know the source of the error. - Check any loop that consumes the list. If two consumers pop from the same queue, one always wins and the other hits an empty list.
- Look at your list initialization. A function that returns
[]when input is missing will silently produce an empty list downstream. - Add
assert my_list, "list should not be empty here"during dev to surface the earliest empty state.
Safe pop patterns you can copy
These patterns handle the empty case explicitly. Pick the one that fits your control flow.
# Pattern 1: guard before pop
if items:
latest = items.pop()
else:
latest = None # or a default, or raise a domain error
# Pattern 2: try/except (idiomatic when empty is rare)
try:
latest = items.pop()
except IndexError:
latest = None
# Pattern 3: collections.deque with default
from collections import deque
q = deque(items)
latest = q.pop() if q else NoneCommon mistakes we see with pop() in real code
- Popping inside a while loop that also feeds the list. If the feeder runs slower than the consumer, the consumer will eventually pop from empty.
- Using pop() as a queue.
list.pop(0)is O(n) and shifts every element. Usecollections.dequefor FIFO work. - Assuming pop() on an empty list returns None. It does not. It raises
IndexError. This is a common bug for developers coming from JavaScript.
How to verify the fix worked
Add a unit test that covers the empty case so this bug does not come back:
import unittest
class TestPopSafe(unittest.TestCase):
def test_empty_returns_none(self):
items = []
result = items.pop() if items else None
self.assertIsNone(result)
def test_populated_returns_last(self):
items = [1, 2, 3]
result = items.pop() if items else None
self.assertEqual(result, 3)
if __name__ == "__main__":
unittest.main()Real-world example: task queue worker
Here is a common production scenario. A worker process pops tasks from a queue and processes them. Two consumers race for the same queue, so at any given moment one of them can hit an empty list.
from collections import deque
import time
task_queue = deque()
def producer():
for i in range(100):
task_queue.append({"id": i, "payload": f"work_{i}"})
time.sleep(0.01)
def safe_worker(name):
while True:
try:
task = task_queue.pop()
except IndexError:
time.sleep(0.05)
continue
print(f"{name} processing {task['id']}")
# In a real deployment you would guard the outer loop with
# a shutdown flag or a total-timeout so workers can exit cleanly.The try/except IndexError pattern lets the worker sleep briefly when the queue is empty instead of crashing. Combined with collections.deque, you get O(1) pop performance and clean shutdown semantics.
Related Python IndexError patterns
- IndexError: list index out of range from accessing
items[i]wherei >= len(items). Same guard pattern applies. - IndexError: string index out of range when slicing empty strings. Use
text[0]only afterif text. - IndexError: pop index out of range when
list.pop(i)receives an out-of-range index. Validateiagainstlen(items)first.
Why IndexError happens
List index out of range means you accessed my_list[i] beyond the list’s actual length. Python lists are indexed from 0 to len(list)-1.
Common triggers
- Off-by-one.
my_list[len(my_list)]fails — uselen(my_list) - 1. - Empty container.
my_list[0]fails when the list is empty. - Wrong data source. CSV had fewer columns than expected.
- Loop range wrong.
for i in range(len(my_list) + 1)— off-by-one. - API returned empty result. Unhandled empty response.
Diagnostic pattern
# BAD — accessing first element without check
def get_first(items):
return items[0] # IndexError if items is empty
# GOOD — guard for empty
def get_first(items):
if not items:
return None
return items[0]
# BETTER — use Optional and let caller handle
from typing import Optional, Sequence, TypeVar
T = TypeVar("T")
def get_first(items: Sequence[T]) -> Optional[T]:
return items[0] if items else None
# For pandas, use .iloc with .empty check
import pandas as pd
def first_row(df: pd.DataFrame) -> Optional[dict]:
if df.empty:
return None
return df.iloc[0].to_dict()
# For enumerate-based loops, this is safe
for i, item in enumerate(items):
print(i, item) # never IndexError
# Never write: for i in range(len(items) + 1)
Best practices
- Prefer enumerate over range(len()). Never off-by-one.
- Guard empty containers. Return None or default before accessing.
- Use slicing.
items[:5]is safe even if items has fewer than 5 elements. - Use type hints with Optional. Communicates that the value may not exist.
- Use pytest with edge cases. Test empty lists, single-element lists, off-by-one boundaries.
Official documentation
Quick step-by-step summary (click to expand)
- Check list emptiness before pop. Use if mylist: value = mylist.pop() to guard against empty list access.
- Use pop with a default via dict.pop(). For dict-like APIs, use mydict.pop(key, None). Lists do not support this so use the length check.
- Wrap pop in try/except IndexError. For queue-like patterns, wrap pop in try except and break the loop when the list is empty.
- Use collections.deque with popleft. For queue behavior, deque raises IndexError on empty popleft too. Same guard pattern applies.
Frequently Asked Questions
What is the difference between list.pop() and list.pop(0)?
list.pop() removes the last item (O(1)). list.pop(0) removes the first item (O(n), because all remaining items shift left). For FIFO queues, use collections.deque.popleft() instead, which is O(1).
Why does my pop() fail intermittently in multi-threaded code?
Race condition. Thread A checks “if queue”, sees items, then Thread B pops the last item, then Thread A tries to pop and gets IndexError. Use try/except IndexError (atomic) or queue.Queue (thread-safe with blocking).
Is list.pop() equivalent to del list[-1]?
Almost. Both remove the last item. pop() returns it, del does not. Both raise IndexError on empty list. Use pop() when you need the value, del when you only need to remove.
How do I implement a thread-safe stack in Python?
For LIFO stacks, queue.LifoQueue is thread-safe with blocking put/get. For LIFO with non-blocking, wrap a list in a threading.Lock. For async code, use asyncio.LifoQueue.
Can I catch IndexError without try/except in modern Python?
For checking-not-handling, just use truthy test (if queue: …). For handling, contextlib.suppress(IndexError) is a one-line alternative to try/except pass. Python 3.10+ structural pattern matching can also handle it with case [first, *rest]: cases.
