The str.find(sub[, start[, end]]) method searches for a substring within a string and returns the lowest 0-based index where the substring begins. If the substring is not present, it returns -1 rather than raising an exception.
find() vs. index() & the "in" Operator: Use the in operator for simple boolean membership checks ("cat" in text). Use find() when you need the integer character offset without wrapping calls in try...except ValueError blocks required by str.index().
The find() method accepts the search target and optional slice offsets:
index_pos = string.find(sub, start=0, end=len(string))
| Parameter | Type | Description |
|---|---|---|
sub |
str |
Required. The substring to search for. |
start |
int |
Optional (default=0). Starting index (inclusive) of slice. Negative values count from end. |
end |
int |
Optional (default=len). Ending index (exclusive) of slice. Negative values count from end. |
| Return Value | int |
Returns lowest 0-based index where sub starts; returns -1 if not found. |
Indices in Python are 0-based. The returned index points to the first character of the matching substring:
phrase = "Welcome to plus2net.com"
# 1. Substring present at beginning/middle
print("Index of 'Welcome':", phrase.find("Welcome")) # 0
print("Index of 'plus': ", phrase.find("plus")) # 11
print("Index of 'co': ", phrase.find("co")) # 3 ('co' in 'Welcome')
# 2. Substring not found returns -1
print("Index of 'Python': ", phrase.find("Python")) # -1
# 3. Case sensitivity
print("Index of 'PLUS': ", phrase.find("PLUS")) # -1 (case mismatch)
Output:
Index of 'Welcome': 0
Index of 'plus': 11
Index of 'co': 3
Index of 'Python': -1
Index of 'PLUS': -1
Using start and end restricts searching to a substring slice string[start:end] without allocating a new string in memory:
data = "apple pie, apple cider, apple crisp"
# Search whole string: returns first occurrence at index 0
print("First 'apple': ", data.find("apple")) # 0
# Start search after index 5: finds second occurrence
print("Second 'apple': ", data.find("apple", 5)) # 11
# Restrict search between index 5 and 15:
print("Apple in [5:15]: ", data.find("apple", 5, 15)) # -1 ('apple' ends at index 16)
print("Apple in [5:16]: ", data.find("apple", 5, 16)) # 11
# Negative slice offsets count from the end
print("Apple in last 15: ", data.find("apple", -15)) # 24
Output:
First 'apple': 0
Second 'apple': 11
Apple in [5:15]: -1
Apple in [5:16]: 11
Apple in last 15: 24
While find() returns the first (lowest index) match from the left, str.rfind() searches from right to left, returning the last (highest index) occurrence:
filepath = "/var/log/nginx/access.2026.log"
first_slash = filepath.find("/")
last_slash = filepath.rfind("/")
extension_dot = filepath.rfind(".")
print("First slash offset: ", first_slash) # 0
print("Last slash offset: ", last_slash) # 14
print("File extension offset:", extension_dot) # 26
print("File extension: ", filepath[extension_dot:]) # '.log'
Output:
First slash offset: 0
Last slash offset: 14
File extension offset: 26
File extension: .log
Choosing between Python's substring search tools depends on your architectural requirements:
| Tool | If Found | If Not Found | Best Used For |
|---|---|---|---|
str.find() |
Lowest Index (0+) | -1 |
When you need index position and absence is expected. |
str.index() |
Lowest Index (0+) | Raises ValueError |
When substring presence is mandatory. |
in operator |
True |
False |
When you only need a boolean existence check. |
str.rfind() |
Highest Index (from right) | -1 |
Finding the last occurrence of a delimiter (e.g. extension dot). |
Because find() is strictly case-sensitive, pair it with str.casefold() for international case-insensitive lookups:
def find_caseless(haystack, needle, start=0, end=None):
if end is None:
end = len(haystack)
return haystack.casefold().find(needle.casefold(), start, end)
text = "Learn Python Programming at Plus2Net"
print("Search 'python':", find_caseless(text, "python")) # 6
print("Search 'PYTHON':", find_caseless(text, "PYTHON")) # 6
Output:
Search 'python': 6
Search 'PYTHON': 6
While find() only returns the first matching offset, an idiomatic while loop advances the start parameter to collect all occurrences:
def find_all_occurrences(text, sub):
positions = []
start = 0
while True:
pos = text.find(sub, start)
if pos == -1:
break
positions.append(pos)
start = pos + len(sub) # Advance past match
return positions
sample = "banana bandana band"
print("All 'an' positions:", find_all_occurrences(sample, "an"))
Output:
All 'an' positions: [1, 3, 8, 10, 16]
In HTML and template parsing, extracting text enclosed by opening and closing tags uses find() to avoid regex engine overhead:
def extract_tag_content(html, open_tag, close_tag):
start_idx = html.find(open_tag)
if start_idx == -1:
return None
content_start = start_idx + len(open_tag)
end_idx = html.find(close_tag, content_start)
if end_idx == -1:
return None
return html[content_start:end_idx]
snippet = "<title>Python str.find() Guide</title>"
print("Extracted title:", extract_tag_content(snippet, "<title>", "</title>"))
Output:
Extracted title: Python str.find() Guide
Splitting email addresses into local-part and domain components using the '@' index offset:
def parse_email_components(email):
at_pos = email.find("@")
if at_pos == -1 or at_pos == 0 or at_pos == len(email) - 1:
return None
return email[:at_pos], email[at_pos + 1:]
print("Valid email: ", parse_email_components("admin@plus2net.com"))
print("Invalid email:", parse_email_components("no_at_sign.com"))
Output:
Valid email: ('admin', 'plus2net.com')
Invalid email: None
The following production script implements an offset finder, tag extractor, and safe sub-slice searcher with automated unit test assertions:
class StringSearchEngine:
@staticmethod
def find_all(text, sub):
# Finds all non-overlapping occurrences of sub in text
if not sub:
return []
indices = []
start = 0
while True:
pos = text.find(sub, start)
if pos == -1:
break
indices.append(pos)
start = pos + len(sub)
return indices
@staticmethod
def extract_between(text, start_delim, end_delim):
# Extracts text strictly between two boundary delimiters
p1 = text.find(start_delim)
if p1 == -1:
return None
content_start = p1 + len(start_delim)
p2 = text.find(end_delim, content_start)
if p2 == -1:
return None
return text[content_start:p2]
# --- Unit Assertions and Verification ---
engine = StringSearchEngine()
# 1. Basic find checks
text = "The quick brown fox jumps over the lazy dog"
assert text.find("quick") == 4
assert text.find("cat") == -1
assert text.find("The", 1) == -1 # 'The' is at 0, searching from 1 returns -1
assert text.find("the") == 31 # Lowercase 'the'
# 2. Find all occurrences
seq = "abc-abc-abc-abc"
assert engine.find_all(seq, "abc") == [0, 4, 8, 12]
assert engine.find_all(seq, "xyz") == []
assert engine.find_all(seq, "") == []
# 3. Delimited text extraction
html = "<header>Welcome to Python</header><p>Body content</p>"
assert engine.extract_between(html, "<header>", "</header>") == "Welcome to Python"
assert engine.extract_between(html, "<p>", "</p>") == "Body content"
assert engine.extract_between(html, "<footer>", "</footer>") is None
assert engine.extract_between(html, "<header>", "</footer>") is None
print("Extracted header:", engine.extract_between(html, "<header>", "</header>"))
print("All 'abc' offsets:", engine.find_all(seq, "abc"))
print("All str.find() unit assertions passed successfully!")
Open in Google Colab View on GitHub
Run all substring search examples interactively, evaluate start/end slice windows, benchmark find() vs. index(), and extract delimited tags in Google Colab.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.