You wrote matches = re.findall(pattern, text) then matches[0] and crashed with IndexError because the pattern didn’t match. re.findall returns an empty list when nothing matches, and indexing into an empty list raises IndexError.

📌 Quick answer: If you want “the first match or None,” use re.search(pattern, text) which returns None on no match, instead of re.findall(...)[0]. Check with if m := re.search(...): m.group() using the walrus operator (Python 3.8+).
Pattern 1: Use re.search for “first match or nothing”
re.search returns a Match object on success, None on failure. Much safer than indexing into findall.
import re
m = re.search(r"(\d+)", text)
if m:
number = m.group(1)
else:
number = NonePattern 2: Walrus operator (Python 3.8+)
Cleaner conditional + access in one line.
if m := re.search(r"version: (\d+\.\d+)", text):
version = m.group(1)
else:
version = "unknown"Pattern 3: Default with next()
For “any match from findall” with default.
matches = re.findall(r"\b\w+@\w+\.\w+\b", text)
first_email = next(iter(matches), None)Pattern 4: re.finditer for streaming matches
For pattern matching in a large string, finditer is lazy and yields Match objects.
for m in re.finditer(r"\d+", text):
print(m.group(), m.start(), m.end())
# Naturally handles "no matches" case (loop just doesn't execute)Prevention
- Default to re.search() for “find one”, re.findall() for “find all”
- Never index into re.findall() result without checking the list is non-empty
- Use walrus operator
:=on Python 3.8+ for cleaner conditional matching - Test with empty/non-matching inputs in unit tests
Related Guides
- List index out of range (full guide)
- String index out of range
- All IndexError fixes
- Python Tutorial hub
Quick step-by-step summary (click to expand)
- Check the findall result length. matches = re.findall(pattern, text); if matches: first = matches[0]. Always guard positional access.
- Use re.search for single-match extraction. re.search returns None on no match instead of an empty list. Check with if m and use m.group().
- Verify your regex pattern with re.compile. re.compile shows syntax errors clearly. Test on regex101.com before running in Python.
- Use named groups for structured extraction. For multi-value matches, use (?P
…) named groups and access with m.group(“name”) instead of positional group[0].
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
Frequently Asked Questions
Why does re.findall(…)[0] raise IndexError?
re.findall returns an empty list when nothing matches the pattern. Indexing into an empty list with [0] raises IndexError. Use re.search instead, which returns None on no match.
What’s the difference between re.search, re.findall, and re.finditer?
re.search returns the first Match object (or None). re.findall returns a list of all matches as strings (or tuples if groups). re.finditer returns an iterator of Match objects (lazy, memory-efficient for large texts).
Why does re.findall return tuples sometimes?
When the pattern has multiple capture groups, each match is returned as a tuple of group values. For a single group it’s a list of strings. To always get Match objects, use re.finditer or re.search instead.
How do I check if a regex matched without raising?
result = re.search(pattern, text); if result: … It returns None on no match. Or use re.match for anchored matching (only at string start).
Should I compile my regex with re.compile?
Compile once when the pattern is reused many times (loops, multi-line files). Skip compilation for one-shot uses, re internal caching handles small numbers of recent patterns. Both raise the same errors.
