Skip to content
VLSI Mentor

Python Part I · Module 4 · Strings

Searching: startswith, endswith, find, replace

Chapter 4.6 gave you one question about text: does it contain this. That is the most-used question but it is not the only one. Real scripts also need to know how a line begins, how a filename ends, where in the line a marker sits, how many times something occurs, and how to produce a version of the text with one part swapped out. This chapter covers all of those, organised by the question rather than by the method, because a list of methods with one-line definitions is a reference page and not a lesson. It also settles a choice Python gives you twice over — two methods that locate a substring and behave completely differently when they fail.

Foundation14 min readPythonPart IFundamentalsStringsSearching

Part I · Module 4 · Chapter 4.10 · Searching: startswith, endswith, find, replace

1. How Does This Line Begin?

in from Chapter 4.6 asks whether a marker appears anywhere. Very often the position matters: a severity marker is meaningful at the start of a line and not in the middle of a message.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "UVM_ERROR: scoreboard mismatch at 1200 ns"
print(line.startswith("UVM_ERROR"))
print(line.startswith("UVM_WARNING"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
True
False

And the mirror question, which is how you test a file's extension:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
path = "runs/axi_smoke/top.sv"
print(path.endswith(".sv"))
print(path.endswith(".v"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
True
False

Note that .sv is True and .v is False, even though the path ends in the letter v. endswith compares the whole piece of text you gave it, so .v would have to be preceded by a dot.

Why not just slice?

You could write this with Chapter 4.5:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "UVM_ERROR: scoreboard mismatch at 1200 ns"
print(line[:9] == "UVM_ERROR")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
True

It works. startswith is better for two reasons. It says what it means without the reader counting characters, and the 9 is not derived from anything — change the marker and the slice silently tests the wrong width. That is the same argument Chapter 4.3 made about derived values, and startswith avoids the problem entirely by taking the marker itself.

Several options at once

Both methods accept a group of alternatives rather than one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
path = "runs/axi_smoke/top.sv"
print(path.endswith((".sv", ".v")))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
True

The extra parentheses group the options together — that kind of grouping is called a tuple and Module 6 covers it. Here it just means "any of these", and the result is True because the path ends in one of them. That is genuinely useful: testing a filename against three possible extensions is one call rather than three.

Still case-sensitive

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "UVM_ERROR: scoreboard mismatch at 1200 ns"
print(line.startswith("uvm_error"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
False

Everything Chapter 4.7 said applies. If the case of the incoming text is not under your control, normalise first: line.lower().startswith("uvm_error").

2. Where Does It Occur?

Knowing that a marker is present is sometimes not enough — you need its position, usually so you can slice around it. find gives you that position:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "UVM_ERROR: scoreboard mismatch at 1200 ns"
print(line.find("scoreboard"))
print(line.find("mismatch"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
11
22

Those are ordinary indexes of the kind Chapter 4.4 established: scoreboard starts at position 11. find returns the position of the first character of the first occurrence.

The position is directly usable in a slice, which is the main reason you would want it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "UVM_ERROR: scoreboard mismatch at 1200 ns"
pos = line.find(": ")
print(pos)
print(line[pos + 2:])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
9
scoreboard mismatch at 1200 ns

find(": ") located the separator at position 9, and slicing from pos + 2 starts just past its two characters — giving the message with the severity marker removed.

If you want the last occurrence rather than the first, rfind searches from the right:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
dotted = "sim.log.log"
print(dotted.find("."))
print(dotted.rfind("."))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3
7

That difference matters for filenames: the extension begins at the last dot, not the first.

3. find() and index(): The Same Job, Different Failures

Python gives you two methods that locate a substring. On success they are identical:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "UVM_ERROR: scoreboard mismatch at 1200 ns"
print(line.find("mismatch"))
print(line.index("mismatch"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
22
22

The entire difference is what happens when the substring is not there.

find returns -1:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "UVM_ERROR: scoreboard mismatch at 1200 ns"
print(line.find("UVM_FATAL"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-1

index raises:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
line = "UVM_ERROR: x"
print(line.index("UVM_FATAL"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ValueError: substring not found

This is not a trivia question — it is a real choice with a real consequence, and it is worth being clear about which you want.

The danger with find is that -1 is a perfectly usable number. Feed it to a slice and nothing complains:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "PASS"
pos = line.find("X")
print(pos)
print(repr(line[pos:]))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-1
'S'

find said "not here" by returning -1, the slice read -1 as "one from the end" exactly as Chapter 4.4 taught, and the result is the last character of the line. No error, and an answer that is completely wrong.

So which should you use? Until Module 9 gives you if, you cannot check a find result anyway, so the honest advice for now is:

  • Use in when you only need to know whether something is there. It is the clearest and it is what you already have.
  • Use index when you need the position and the substring genuinely ought to be present — a failure is then loud and immediate.
  • Use find when you specifically intend to handle absence, which you will be able to do properly from Module 9 onwards.

4. How Many Times?

count answers that, and it never fails — it returns zero for something that is not there:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "UVM_ERROR: scoreboard mismatch at 1200 ns"
print(line.count("s"))
print(line.count("UVM"))
print(line.count("zz"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3
1
0

Three lowercase s characters, one UVM, and no zz. Because a count of zero is a sensible answer rather than a failure, count has none of the -1 awkwardness of find.

It is also an honest way to ask "is this here" when you happen to want the number too, since count and in agree about presence.

5. Producing a Changed Version

replace gives you back a new string with every occurrence of one piece of text swapped for another:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print("a\tb\tc".replace("\t", " "))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a b c

Both tabs became spaces. This is the standard way to normalise a separator you do not want, and it pairs naturally with Chapter 4.7's whitespace handling.

A more realistic example — dropping a marker prefix down to something shorter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "UVM_ERROR: scoreboard mismatch at 1200 ns"
print(line.replace("UVM_ERROR", "ERROR"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: scoreboard mismatch at 1200 ns

Three facts about replace, all of which follow from things you already know.

It replaces every occurrence, not just the first. That is the default and it is usually what you want.

A target that is not there changes nothing — no error, just the original text back:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "UVM_ERROR: scoreboard mismatch at 1200 ns"
print(line.replace("UVM_FATAL", "X") == line)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
True

You can limit how many it replaces with a third argument:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print("a-b-c-d".replace("-", "_", 2))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a_b_c-d

Two hyphens replaced, the third left alone. Useful when only the leading separators are meaningful.

And the fact that matters most:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
orig = "a-b"
orig.replace("-", "_")
print(orig)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a-b

replace returns a new string and leaves the original alone, exactly as every method in this module has. The call on line 2 produced "a_b" and threw it away. Chapter 4.11 — the next chapter — is entirely about this, so if it still feels like a quirk rather than a consequence, that is the chapter that fixes it.

6. Splitting Around a Marker

One more method, because the canonical curriculum names it and because it does something the others do not. partition finds a separator and hands back the part before it, the separator itself, and the part after:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "UVM_ERROR: scoreboard mismatch at 1200 ns"
print(line.partition(": "))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
('UVM_ERROR', ': ', 'scoreboard mismatch at 1200 ns')

Three pieces grouped together — the same kind of grouping as the options in Section 1, and Module 6 names it. What is worth noticing now is that partition does in one step what Section 2 needed find plus arithmetic plus a slice to do, and it does it without any chance of the -1 trap.

Its behaviour when the separator is absent is also well-chosen:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print("PASS".partition(": "))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
('PASS', '', '')

You always get three pieces. The first holds everything, and the other two are empty, which tells you the separator was not found without any special value to check.

7. The Toolkit, Summarised

Now that each has arrived for a reason, here is the whole set in one place. This is a summary of what you have seen, not a reference to learn:

QuestionMethodOn failure
Is it in there?"m" in lineFalse
Does it begin like this?line.startswith("m")False
Does it end like this?line.endswith(".sv")False
Where is it?line.find("m")returns -1
Where is it, and it should be thereline.index("m")raises ValueError
Where is the last one?line.rfind("m")returns -1
How many?line.count("m")0
Give me it with this swappedline.replace("a", "b")unchanged
Split me around this one markerline.partition(": ")all in the first piece

Nine questions. Between them and the previous four chapters, that is the plain-string toolkit complete.

8. When Regex Is Actually Worth It

Chapter 4.1 promised that this module would make the regex decision a judgement rather than a habit. You now have the toolkit, so the criterion can be stated properly.

Every method above matches a piece of text you know. You give startswith the exact marker; you give find the exact substring. That covers a great deal, because a lot of real text has fixed structure — a known prefix, a known extension, a known separator.

A regular expression describes a pattern rather than a piece of text: "some digits", "one or more spaces", "either of these words followed by a colon". You need that when what you are looking for genuinely varies.

Two examples to make the line concrete. "Does this line start with UVM_ERROR" is a known piece of text — startswith, one call, done. "Extract the number from seed = 4210 where there may be any amount of space around the =" is a shape, and while you could do it by combining partition and strip, a pattern says it more directly.

The failure mode to avoid is not using regex. It is reaching for it first, out of habit, on the many problems where in and split were the answer.

9. Predict, Run, Observe

Predict all six before reading on. The fifth is the trap.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "WARNING: hold violation at U12"
print(line.startswith("WARNING"))
print(line.endswith("U12"))
print(line.find("hold"))
print(line.count("o"))
print(line.find("ERROR"))
print(line.replace("WARNING", "WARN"))

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
True
True
9
3
-1
WARN: hold violation at U12

find("hold") is 9 — count the characters of WARNING: and you get nine, so hold starts at index 9. count("o") is 3: one in hold, two in violation.

The fifth is -1, not an error and not False. If you predicted False you were thinking of in; find answers a different question and says "nowhere" with a number. Section 3 is why that number is dangerous.

10. Exercises

Exercise 1 — pick the question. For each task, name the single method or operator you would use, and say why the others are worse: (a) is this a SystemVerilog file (b) does this line report an error, wherever in the line it says so (c) how many commas are in this row (d) get the message that follows the : in a log line (e) turn every tab in a line into a space

Exercise 2 — predict. Given path = "runs/axi_smoke/sim.log", give the result of each without running it, then verify: (a) path.startswith("runs/") (b) path.endswith(".log") (c) path.endswith("log") (d) path.find("/") (e) path.rfind("/") (f) path.count("/") (g) path.find("axi")

Exercise 3 — the two failures. Run both lines below and write down exactly what each does. Then say in two sentences which you would put in a script that must not continue on bad input, and why:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print("PASS".find("X"))
print("PASS".index("X"))

Exercise 4 — the -1 trap. Explain in three sentences why this prints S rather than failing, and what the author probably expected:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "PASS"
pos = line.find("X")
print(line[pos:])

Exercise 5 — first or last. For name = "top.axi.sv", write two expressions: one giving the position of the first dot and one giving the position of the last. Then say which one you would use to find where the extension begins, and why.

Exercise 6 — replace returns. This is meant to clean the tabs out of a line and prints the original. Explain what happened and fix it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
row = "a\tb\tc"
row.replace("\t", " ")
print(row)

Exercise 7 — plain or pattern. For each, say whether the plain toolkit is enough or a pattern is genuinely needed, and name the method if it is plain: (a) lines that start with ERROR or FATAL (b) a filename ending in .sv or .v (c) any line containing a number of at least four digits (d) the text after the first colon (e) a line where a word is repeated twice in a row

Exercise 8 — partition vs find. Produce the message from "ERROR: mismatch" two ways: once with find and a slice, once with partition. Say in one sentence which you would submit for review.

11. Summary

  • startswith and endswith test the ends of a string against a piece of text you name, and are clearer and safer than the equivalent slice comparison.
  • Both accept several alternatives at once, grouped in extra parentheses, meaning "any of these".
  • All of these are case-sensitive. Normalise with lower() first when the case is not under your control.
  • find returns the index of the first occurrence, and rfind the last. The result is directly usable in a slice.
  • find returns -1 when the substring is absent; index raises ValueError: substring not found. That is the only difference between them.
  • -1 is a valid index meaning "the last character", so an unchecked find result can silently operate on the wrong part of the string. Prefer in for presence and index when the substring should be there.
  • count returns the number of occurrences, and 0 for absent — no special value to be careful about.
  • replace returns a new string with every occurrence swapped, changes nothing if the target is absent, and takes an optional limit.
  • partition gives the part before a separator, the separator, and the part after — three pieces always, so absence needs no special check.
  • Every one of these returns a value and modifies nothing.
  • The regex criterion: if you can name the exact text, use these methods. If you can only describe a shape, that is what a pattern is for.

That completes the toolkit for asking about and transforming text. The next chapter explains the single rule that has been sitting underneath every method in this module.

Where this fits

Part of the Python curriculum.