Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re

LINE = "UVM_ERROR drv.sv(88) @ 1420: bad handshake"

m = re.search(r"UVM_ERROR", LINE)
print(m)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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()])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.sv

group() 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
found   : <re.Match object; span=(0, 8), match='UVM_INFO'>
missing : None
type    : Match | NoneType
truthy  : True | False

The important property: a match object is always truthy and None is always falsy, so the idiom is a plain if:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error : UVM_ERROR drv.sv(88) bad handshake
other : UVM_INFO  all good
other : UVM_FATAL env.sv(12) cannot contin

No 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:

Azvya Education Pvt. Ltd.VLSI Mentor
nogroup.py (BROKEN ON PURPOSE)
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())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
matched: UVM_ERROR
no match in: UVM_INFO fine

Or assign and branch in one step, which is the shape most parser loops end up with:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error lines: 2

5. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lines examined: 5
matched       : 2
skipped       : 3
skipping is the normal case, not a problem

Three 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
no line matched -- is the pattern right, or is the log clean?
matched: 0 of 3

Zero 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
with a pattern : True
with in        : True
same answer    : True

For 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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() and span() give positions.
  • The other return is None, and a match object is always truthy while None is always falsy, so if m: is the idiom.
  • Calling .group() on None raises 'NoneType' object has no attribute 'group' — the most common regex runtime error.
  • None is 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 — in is 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.