Python Part II · Module 19 · Regular Expressions for VLSI
Your First Pattern: Literal Matching With re.search
The smallest useful regular expression contains no special characters whatsoever, which makes it a strange thing to spend a chapter on — it does exactly what the in operator does, more slowly. That is deliberate. Everything genuinely difficult about regular expressions in production code is about what comes back from a search and what happens when nothing does, and both are learnable while the pattern itself is still something you can read at a glance. Add metacharacters before understanding the return value and you have two unfamiliar things failing at once.
Foundation13 min readPythonPart IIRegexLog ParsingVerification Flow
Part II · Module 19 · Chapter 19.2 · Your First Pattern: Literal Matching With re.search
1. The Smallest Pattern
import re
LINE = "UVM_ERROR drv.sv(88) @ 1420: bad handshake"
m = re.search(r"UVM_ERROR", LINE)
print(m)<re.Match object; span=(0, 9), match='UVM_ERROR'>re.search(pattern, text) looks for the pattern anywhere in the text and returns the first place it occurs. The pattern here is nine ordinary characters with no special meaning, so it matches exactly the text UVM_ERROR.
Two things to notice in that output before anything else.
It printed something useful — the span and the matched text — rather than a memory address. Module 16 trained you to expect <object at 0x...> from an unfamiliar type; re.Match defines a proper __repr__ (Chapter 16.8), so printing one during debugging tells you what you need.
And the r before the string is the raw string prefix. It changes nothing for this pattern, and Chapter 19.3 is about why you write it anyway, always.
2. What Comes Back
The match object carries where the match was and what it matched:
import re
LINE = "UVM_ERROR drv.sv(88) @ 1420: bad handshake"
m = re.search(r"drv.sv", LINE)
print("the matched text :", m.group())
print("where it started :", m.start())
print("where it ended :", m.end())
print("both at once :", m.span())
print("the whole subject:", m.string == LINE)
print("slicing it back :", LINE[m.start():m.end()])the matched text : drv.sv
where it started : 10
where it ended : 16
both at once : (10, 16)
the whole subject: True
slicing it back : drv.svgroup() is the one you use constantly. It returns the text that matched — which for a literal pattern is the pattern itself, and which will become genuinely useful the moment Chapter 19.4 lets a pattern match something it does not spell out.
start(), end() and span() give positions, useful for slicing around a match or for reporting where in a line something was found.
3. None Means No Match
The other possible return:
import re
LINE = "UVM_INFO @ 0: reporter [RNTST] Running test smoke_test"
found = re.search(r"UVM_INFO", LINE)
missing = re.search(r"UVM_ERROR", LINE)
print("found :", found)
print("missing :", missing)
print("type :", type(found).__name__, "|", type(missing).__name__)
print("truthy :", bool(found), "|", bool(missing))found : <re.Match object; span=(0, 8), match='UVM_INFO'>
missing : None
type : Match | NoneType
truthy : True | FalseThe important property: a match object is always truthy and None is always falsy, so the idiom is a plain if:
import re
LINES = [
"UVM_ERROR drv.sv(88) bad handshake",
"UVM_INFO all good",
"UVM_FATAL env.sv(12) cannot continue",
]
for line in LINES:
if re.search(r"UVM_ERROR", line):
print("error :", line[:34])
else:
print("other :", line[:34])error : UVM_ERROR drv.sv(88) bad handshake
other : UVM_INFO all good
other : UVM_FATAL env.sv(12) cannot continNo comparison against None is needed. if m: is the conventional spelling and reads correctly.
4. The Most Common Regex Error
Forgetting that None is possible:
import re
LINE = "UVM_INFO all good, nothing to report"
m = re.search(r"UVM_ERROR", LINE)
print("searched; got:", m)
print("the matched text:", m.group())searched; got: None
Traceback (most recent call last):
File "nogroup.py", line 7, in <module>
print("the matched text:", m.group())
^^^^^^^
AttributeError: 'NoneType' object has no attribute 'group''NoneType' object has no attribute is the error Chapter 14.3 Section 8 called the most common in all of Python, and this is its regex form. It means exactly one thing: the pattern did not match and the code assumed it had.
Two shapes that avoid it. Check first:
import re
for line in ["UVM_ERROR drv.sv(88)", "UVM_INFO fine"]:
m = re.search(r"UVM_ERROR", line)
if m:
print("matched:", m.group())
else:
print("no match in:", line[:22])matched: UVM_ERROR
no match in: UVM_INFO fineOr assign and branch in one step, which is the shape most parser loops end up with:
import re
LINES = ["UVM_ERROR drv.sv(88)", "UVM_INFO fine", "UVM_ERROR mon.sv(7)"]
errors = 0
for line in LINES:
m = re.search(r"UVM_ERROR", line)
if not m:
continue
errors += 1
print("error lines:", errors)error lines: 25. None Is an Answer, Not a Failure
Worth separating from Section 4, because the defensive framing is slightly wrong.
A parser runs its pattern against every line of a log, and most lines will not match. That is not an error condition to be guarded against — it is the normal case, and the pattern's job is as much to reject lines as to accept them.
import re
LOG = [
"Chronologic VCS simulator copyright 1991-2024",
"UVM_INFO @ 0: reporter [RNTST] Running test smoke_test",
"UVM_ERROR drv.sv(88) @ 1420: bad handshake",
"UVM_ERROR mon.sv(7) @ 90: missing beat",
"Simulation complete",
]
matched = skipped = 0
for line in LOG:
if re.search(r"UVM_ERROR", line):
matched += 1
else:
skipped += 1
print("lines examined:", len(LOG))
print("matched :", matched)
print("skipped :", skipped)
print("skipping is the normal case, not a problem")lines examined: 5
matched : 2
skipped : 3
skipping is the normal case, not a problemThree of five lines produced None and the parser is working correctly. Chapter 1.8's discipline applies: the states a line can be in are matched and not matched, both are expected, and the code should say which it is doing rather than treating one as an exception.
Where it becomes a problem is when nothing matches — a pattern that returns None for every line of a log is Chapter 18.5's empty discovery in another form, and deserves its own check:
import re
LOG = ["UVM_INFO fine", "UVM_INFO also fine", "Simulation complete"]
hits = [l for l in LOG if re.search(r"UVM_ERROR", l)]
if not hits:
print("no line matched -- is the pattern right, or is the log clean?")
print("matched:", len(hits), "of", len(LOG))no line matched -- is the pattern right, or is the log clean?
matched: 0 of 3Zero matches is ambiguous between a clean run and a broken pattern, and distinguishing them is Chapter 19.17's subject.
6. Do Not Actually Write This
The honest closing, carrying Chapter 19.1's argument:
import re
LINE = "UVM_ERROR drv.sv(88) bad handshake"
print("with a pattern :", bool(re.search(r"UVM_ERROR", LINE)))
print("with in :", "UVM_ERROR" in LINE)
print("same answer :", bool(re.search(r"UVM_ERROR", LINE)) == ("UVM_ERROR" in LINE))with a pattern : True
with in : True
same answer : TrueFor a literal with no metacharacters, re.search is in with an import, a function call and a return value to unpack. It is slower, it is less obvious to a reader, and it buys nothing.
So this chapter's pattern is scaffolding. Its purpose was to make the return value familiar while the pattern was still trivially readable, because from Chapter 19.4 onward the pattern stops being readable at a glance and you will want one of the two unfamiliar things to already be understood.
7. Predict, Run, Observe
import re
LINE = "UVM_ERROR drv.sv(88) @ 1420: UVM_ERROR appears twice"
m = re.search(r"UVM_ERROR", LINE)
print("A:", m.span())
print("B:", m.group())
print("C:", LINE.find("UVM_ERROR"))
print("D:", LINE.rfind("UVM_ERROR"))
print("E:", re.search(r"uvm_error", LINE))
print("F:", bool(re.search(r"ERROR", LINE)))
print("G:", re.search(r"", LINE).span())A: (0, 9)
B: UVM_ERROR
C: 0
D: 29
E: None
F: True
G: (0, 0)A, C and D together show that search finds the first occurrence only — the second UVM_ERROR at index 29 is invisible to it. Finding all of them needs findall or finditer, which is Chapter 19.11.
E is None because matching is case-sensitive by default. Chapter 19.14's IGNORECASE flag changes that; LINE.lower() is often simpler.
F is True — a pattern matches anywhere, including inside a longer word, which is Chapter 19.7's false-positive problem arriving early.
G is the empty pattern, which matches at position zero and succeeds. An empty pattern is a real hazard when a pattern is built by joining pieces and one piece turns out to be an empty string: the search succeeds, group() returns '', and nothing looks wrong.
8. Exercises
Exercise 1 — the smallest search. Search for a literal in a line and print the match object, its group() and its span().
Exercise 2 — slice it back. Use start() and end() to slice the matched text out of the original string and confirm it equals group().
Exercise 3 — the None error. Call .group() on a search that does not match. Record the error and name the chapter that first described it.
Exercise 4 — the guard. Rewrite the same code two ways: with if m: and with an early continue. Say which fits a parser loop better.
Exercise 5 — skipping is normal. Run a pattern over a ten-line log and report matched and skipped counts. Confirm most lines do not match.
Exercise 6 — zero matches. Make a pattern that matches nothing and add the check that notices. Say what the two possible explanations are.
Exercise 7 — first only. Put the same literal twice in a line and show that search finds only the first. Find the second with find and an offset.
Exercise 8 — the empty pattern. Search with r"" and inspect the span. Describe how a joined pattern could end up empty by accident.
9. Summary
re.search(pattern, text)finds the pattern anywhere and returns the first occurrence.- A literal pattern has no special characters and matches exactly the text it spells.
- A match object prints usefully — span and matched text, with no memory address.
group()returns the matched text;start(),end()andspan()give positions.- The other return is
None, and a match object is always truthy whileNoneis always falsy, soif m:is the idiom. - Calling
.group()onNoneraises'NoneType' object has no attribute 'group'— the most common regex runtime error. Noneis an answer, not a failure: most lines of a log will not match, and that is the normal case.- Zero matches across a whole file is ambiguous between a clean log and a broken pattern, and deserves its own check.
- Search finds only the first occurrence; matching is case-sensitive; a pattern matches inside longer words.
- Do not use a literal pattern in real code —
inis faster and clearer. This one was scaffolding for the return value.
The next chapter explains the r prefix that has been on every pattern so far, and how to match the characters regex reserves for itself.
Where this fits
Part of the Python curriculum.
