Finding every occurrence of a specific sequence of characters within a larger string is a fundamental task in text processing, data cleaning, and parsing. In Python, there are several idiomatic ways to achieve this, ranging from simple built-in methods to powerful regular expressions and modern standard library additions. Understanding the nuances of each approach—specifically regarding performance, readability, and the type of data returned—allows developers to choose the right tool for their specific use case.
The Classic Approach: Using str.find() in a Loop
The most basic mechanism for locating a substring is the str.Think about it: find(sub[, start[, end]]) method. Because of that, it returns the lowest index where the substring is found, or -1 if it is absent. To find all instances, you must wrap this in a loop, updating the start index after every match.
text = "abracadabra"
substring = "abra"
indices = []
start = 0
while True:
index = text.find(substring, start)
if index == -1:
break
indices.append(index)
start = index + 1 # Move past the current match
print(indices) # Output: [0, 7]
Key Considerations:
- Overlapping Matches: The snippet above uses
start = index + 1. This allows finding overlapping matches (e.g., finding "ana" in "banana" yields indices 1 and 3). If you want non-overlapping matches, usestart = index + len(substring). - Performance: This is implemented in C and is reasonably fast for single-pass scanning, but the Python-level
whileloop introduces overhead for strings with a massive number of matches.
The Pythonic Generator: str.index() and Exceptions
While find returns -1 on failure, str.Day to day, index() raises a ValueError. Some developers prefer the "Easier to Ask for Forgiveness than Permission" (EAFP) style using a try/except block. Still, for finding all instances, this is generally considered less readable and slower than the find loop due to the cost of exception handling in a tight loop.
Modern Standard Library: re.finditer() for Power and Flexibility
The re module is the heavy lifter for string searching. end()), the matched string (match.Even so, the re. Consider this: this is often the preferred "professional" approach because match objects contain rich metadata: the start index (match. finditer(pattern, string) function returns an iterator yielding **match objects** for all non-overlapping matches. In real terms, start()), end index (match. group()), and captured groups.
No fluff here — just what actually works.
import re
text = "The rain in Spain falls mainly on the plain."
pattern = r"ain"
matches = [(m.Here's the thing — start(), m. group()) for m in re.
### Handling Overlapping Matches with Regex
Standard regex engines consume characters as they match, preventing overlapping matches by default. To find overlapping instances (e.g., finding "aba" in "ababa" at indices 0 and 2), you must use a **positive lookahead assertion** `(?=...)`. This asserts a match without consuming characters.
```python
import re
text = "ababa"
pattern = r"(?=aba)" # Lookahead for overlapping matches
indices = [m.start() for m in re.finditer(pattern, text)]
print(indices) # Output: [0, 2]
Performance Note on re
Compiling the pattern once using re.compile() is significantly faster if you are searching the same pattern across multiple strings or in a tight loop Not complicated — just consistent. Turns out it matters..
compiled_pattern = re.compile(r"\d+") # Compile once
# ... later in code ...
for match in compiled_pattern.finditer(large_text):
process(match)
The New Contender: str.finditer() (Python 3.11+)
Introduced in **Python 3.finditer(sub[, start[, end]])brings the convenience of an iterator interface to the built-in string type without importing theremodule. 11**,str.It returns an iterator yielding slice objects (or tuples in some implementations, specifically slice objects representing the span) for non-overlapping occurrences.
Note: As of Python 3.12, str.finditer yields slice objects.
# Python 3.11+
text = "hello world, hello universe"
substring = "hello"
# Returns an iterator of slice objects
for s in text.finditer(substring):
print(f"Found at [{s.start}:{s.stop}] -> '{text[s]}'")
# Output:
# Found at [0:5] -> 'hello'
# Found at [13:18] -> 'hello'
Why use this over re.finditer?
- Zero Dependencies: No
import rerequired. - Literal String Search: It searches for the exact substring, not a regex pattern. You don't need to escape special characters like
.,*,?,[,]. - Speed: For literal substring searching, the highly optimized C implementation (often using algorithms like Two-Way or Boyer-Moore-Horspool) is significantly faster than the regex engine.
Limitation: It does not support overlapping matches natively (it advances past the match) and lacks the advanced capture group features of regex.
Functional Style: List Comprehension with enumerate
For developers who prefer a functional, one-liner style without importing modules, combining enumerate with slicing or startswith is a clever trick.
text = "mississippi"
sub = "issi"
# Using startswith (cleaner than slicing manually)
indices = [i for i in range(len(text)) if text.startswith(sub, i)]
print(indices) # Output: [1, 4]
Pros: Extremely concise; handles overlapping matches naturally.
Cons: O(N * M) complexity in the worst case (where N is text length, M is substring length) because startswith checks the substring at every single index. For long strings or long substrings, this is orders of magnitude slower than find or re which use optimized search algorithms (O(N+M)).
Third-Party Powerhouses: regex and more-itertools
When the standard library isn't enough, two packages are industry standards.
The regex Module (Drop-in re Replacement)
The third-party regex module (install via pip install regex) supports overlapping matches natively via the overlapped=True flag in finditer. It also supports fuzzy matching and variable-width lookbehinds Not complicated — just consistent..
# pip install regex
import regex
text = "ababa"
# Native overlapping support
matches = [m.start() for m in regex.finditer("aba", text, overlapped=True)]
print(matches) # Output: [0, 2]
The more-itertools Library
This library provides locate and windowed utilities that can be composed for complex stream processing, though for simple substring indexing, regex or re are usually more direct.
Comparing Performance: A Quick Mental Model
| Method | Overlapping Support | Speed (Literal Substring) | Returns | Best For |
|---|---|---|---|---|
while + str.find |
Manual (+1 vs +len) |
Fastest (C-loop) | List of int (indices) |
Simple scripts, max speed, no imports. |