Locating substrings within text is fundamental to parsing data, validating input, and processing logs. Python provides two primary methods for finding substring positions: .find() and .index(). While both return the starting index when the substring exists, they behave very differently when it doesn't: a distinction that significantly impacts how you structure your error handling and program flow. This guide clarifies when to use each method for more robust string processing.
The Safe Approach: find()
The .find() method returns the index of the first occurrence of a substring, or -1 if not found. This makes it ideal for conditional checks where the substring may or may not exist:
log_entry = "2026-05-15 INFO: Connection established"
error_pos = log_entry.find("ERROR")
if error_pos == -1:
print("No errors detected in log entry")
else:
print(f"Error found at position {error_pos}")
info_pos = log_entry.find("INFO")
print(f"INFO tag found at index: {info_pos}") Output:
No errors detected in log entry
INFO tag found at index: 11You can also specify start and end positions to search within a specific range:
text = "banana"
result = text.find("a", 2)
print(result) # Output: 3
result = text.find("a", 2, 4)
print(result) # Output: 3The Strict Approach: index()
The .index() method also returns the substring's position, but raises a ValueError when the substring isn't found. Use this when the substring's presence is required for your program logic:
config_line = "database_host: localhost"
try:
separator_pos = config_line.index(":")
key = config_line[:separator_pos].strip()
value = config_line[separator_pos + 1:].strip()
print(f"Config: {key} = {value}")
except ValueError:
print("Invalid configuration format: missing separator")The exception-based approach makes code intent clearer: if execution continues past .index(), you're guaranteed the substring exists:
def parse_email(email):
"""Extract username and domain from email address."""
try:
at_position = email.index("@")
username = email[:at_position]
domain = email[at_position + 1:]
return username, domain
except ValueError:
raise ValueError(f"Invalid email format: {email}")
user, domain = parse_email("user@example.com")
print(f"User: {user}, Domain: {domain}")Output:
User: user, Domain: example.comMethod Comparison
| Aspect | .find() |
.index() |
|---|---|---|
| When found | Returns index | Returns index |
| When not found | Returns -1 |
Raises ValueError |
| Works on | Strings only | Strings, lists, tuples |
| Best for | Optional searches | Required substrings |
| Error handling | Conditional check | Try/except block |
Choosing the Right Method
def highlight_keyword(text, keyword):
"""Highlight keyword if present, otherwise return original."""
pos = text.find(keyword)
if pos != -1:
return f"{text[:pos]}**{keyword}**{text[pos + len(keyword):]}"
return text
def extract_value(data, prefix):
"""Extract value after prefix: prefix must be present."""
start = data.index(prefix) + len(prefix)
return data[start:].split()[0]Finding All Occurrences
Both methods find only the first occurrence. To find all positions:
def find_all(text, substring):
"""Find all occurrences of substring in text."""
positions = []
start = 0
while True:
pos = text.find(substring, start)
if pos == -1:
break
positions.append(pos)
start = pos + 1
return positions
text = "banana"
print(find_all(text, "a")) # Output: [1, 3, 5]The in Operator Alternative
When you only need to check existence without needing the position, use the in operator for clearer, more Pythonic code:
log_message = "ERROR: Database connection failed"
if "ERROR" in log_message:
print("Error detected!")
status = "critical" if "ERROR" in log_message else "normal"By understanding the behavioral difference between silent failure (-1) and explicit exceptions (ValueError), you can write string processing code that clearly communicates its intent and handles edge cases appropriately.