Run it
The three calls on a hit and on a miss, then the two bugs find invites — including the falsy-zero one, shown giving a confidently wrong answer.
Which one to use
The rule follows from how the two report failure:
Use find when absence is a normal outcome you plan to branch on.
Use index when absence means something is wrong and should not be silently absorbed.
The failure mode of choosing wrongly is specific and worth naming: −1 is a valid index in Python. Forgetting the check does not crash — it silently reads from the end of the string.
A missing check with index raises immediately, at the line that caused it. With find, the wrong answer propagates. That is why index is the safer default when a match is genuinely expected, and it is the opposite of most people's instinct.
The optional start and end arguments
Both accept a search region, and using it avoids slicing:
s.find(sub, i) searches in place; s[i:].find(sub) copies the remainder first and returns an offset relative to the slice, which then needs + i to be meaningful. The in-place form is both faster and less error-prone — and iterating occurrences is the case where it matters:
Advancing by 1 finds overlapping matches; advancing by len(sub) finds only non-overlapping ones. "aaa".find("aa") occurring at both 0 and 1 is the case that distinguishes them, and which behaviour is wanted should be stated rather than assumed.
The same search, three reports
find returns the index of the first occurrence, or -1. index returns the same index, or raises ValueError. in answers only yes or no, and reads better when that is all you need.
All three are the same O(n·m) scan underneath, so this is not a performance choice. There are rfind and rindex for the last occurrence, and all of them take optional start and end bounds — which is how you find the second occurrence without slicing.
The trap in find's return value
-1 is a perfectly valid index in Python, so a find result used without checking silently indexes from the end. Worse:
if s.find("a"): is False when the match is at index 0 — the one case you were most likely to want. The check has to be if s.find("a") != -1:, which is exactly why in exists.
How to choose
Use in when you only need to know whether it is there. Use find when a miss is a normal outcome you will branch on. Use index when a miss means something upstream is already broken and you would rather have a traceback at the real cause than a -1 propagating three functions away.
That last point is the answer interviewers are listening for: it is a question about error handling wearing a string-methods costume.
The other ways to ask the same question
| Question | Best tool |
|---|
| Is it present? | sub in s — clearest and fastest |
| Where is it? | find or index |
| Does it start with this? | startswith — also accepts a tuple |
| Does it end with this? | endswith |
| How many times? | count |
| All positions? | Loop with find(sub, pos), or re.finditer |
| Split around it? | partition or split |
in rather than find(...) != -1 when only presence matters. It reads better and avoids the comparison entirely.
str.partition is the underused one. It splits at the first occurrence and returns three parts, so no index arithmetic is needed:
Checking if sep: is a clean way to distinguish found from not-found, and the split is done in the same call. rpartition splits at the last occurrence, which is how you separate a file extension.
What the complexity actually is
find is not a naive scan. CPython uses a hybrid of Crochemore-Perrin ("two-way") and Boyer-Moore-Horspool, with a simpler loop for short needles.
| Case | Cost |
|---|
| Typical text | ~O(n), often sublinear via skipping |
| Worst case | O(n · m) |
| Single character | O(n), heavily optimised |
| Not found | Full scan — O(n) |
The practical implication is that a hand-written Python loop will not beat it. Even implementing KMP in pure Python is slower than calling find, because the C implementation's constant factor is far smaller. Write the loop in an interview to demonstrate understanding; call find in real code.
Questions people ask
Why does list have index but not find? A historical asymmetry. Use in first, or catch ValueError.
Is in faster than find? Effectively the same algorithm; in avoids constructing the index and is marginally quicker.
Do bytes have both? Yes, with the same semantics — and the argument must be bytes, not str.
What does find("") return? 0. The empty string is found at every position, including in an empty string.
How do I find all overlapping occurrences? Loop with find(sub, pos) and pos += 1. re.finditer skips overlaps unless you use a lookahead pattern.
Which is more Pythonic? "Ask for forgiveness, not permission" favours index with a try; branching on a value favours find. Both are idiomatic — matching the choice to whether absence is exceptional is the point.
Recap in one screen
find returns −1 when absent; index raises ValueError. Nothing else differs.- −1 is a valid Python index, so an unchecked
find silently reads the last character instead of failing. - Use
index when a match is required and find when absence is an expected branch. - Pass
start and end rather than slicing — slicing copies and shifts the returned offset. - Prefer
in for presence, startswith/endswith for edges, and partition to split without index arithmetic.