Python Part II · Module 19 · Regular Expressions for VLSI
Raw Strings and Escaping Special Characters
Every pattern so far has carried an r prefix that changed nothing, and this chapter is why it is there anyway. There are two entirely separate backslash problems in Python regex and almost every explanation blurs them together, which is the main reason the topic has a reputation. One belongs to Python and is solved by the prefix; the other belongs to regular expressions and is solved by a backslash you type on purpose. Separating them takes five minutes and the alternative is years of adding backslashes until something works.
Foundation14 min readPythonPart IIRegexLog ParsingVerification Flow
Part II · Module 19 · Chapter 19.3 · Raw Strings and Escaping Special Characters
1. Problem One: Python Eats Backslashes First
A pattern is written as a Python string, so Python processes it before the regex engine ever sees it. In an ordinary string literal, a backslash starts an escape sequence:
print("newline :", repr("a\nb"))
print("tab :", repr("a\tb"))
print("raw newline :", repr(r"a\nb"))
print("raw tab :", repr(r"a\tb"))newline : 'a\nb'
tab : 'a\tb'
raw newline : 'a\\nb'
raw tab : 'a\\tb'In the first two, \n and \t became one character each — an actual newline and an actual tab. In the raw versions they stayed as two characters: a backslash and a letter.
That matters because regex syntax is built from backslash sequences. \d means "a digit" to the regex engine, and the engine can only see it if Python passes the backslash through.
Python warns when a string contains a backslash sequence it does not recognise:
import warnings
NON_RAW = 'pattern = "\\d+"'
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
compile(NON_RAW, "<example>", "exec")
for w in caught:
print("Python warns:", w.category.__name__)
print("message :", w.message)
with warnings.catch_warnings(record=True) as clean:
warnings.simplefilter("always")
compile('pattern = r"\\d+"', "<example>", "exec")
print("raw version warnings:", len(clean))Python warns: SyntaxWarning
message : invalid escape sequence '\d'
raw version warnings: 0Python itself tells you. \d is not a Python escape, so a non-raw "\d+" produces a SyntaxWarning — and in a future Python version this becomes an error. The raw form produces nothing.
For \d the non-raw version happens to work anyway, because Python leaves unrecognised escapes alone. For \n, \t and \b it does not:
print("\\b non-raw is a backspace character:", repr("\b"))
print("\\b raw is two characters :", repr(r"\b"))
print("they are different :", "\b" != r"\b")\b non-raw is a backspace character: '\x08'
\b raw is two characters : '\\b'
they are different : True\b is regex's word-boundary (Chapter 19.7) and "\b" in Python is a backspace control character. Written without the r, the pattern silently becomes something else entirely.
Write every pattern as
r"...". It costs one character and removes a whole class of bug you would otherwise diagnose one backslash at a time.
2. Problem Two: Regex Reserves Characters
Entirely separate, and the r prefix does nothing for it.
Regex gives special meaning to about a dozen characters. The ones that matter for tool output:
| Character | Means in regex | Appears in |
|---|---|---|
. | any character | drv.sv, 182.4, u_core.reg_q |
( ) | a group | drv.sv(88), (setup) |
[ ] | a character set | [DRV], [RNTST] |
+ | one or more | +incdir+, +define+ |
* | zero or more | *** TEST FAILED *** |
? | optional | rarely |
{ } | repetition count | ${VAR} |
| | alternation | table output |
^ $ | anchors | ^ in some formats |
\ | escape | Windows paths |
Look at that right-hand column. VLSI tool output is unusually dense in reserved characters — a single UVM error line typically contains a dot, a pair of parentheses and a pair of brackets, and a filelist is full of plus signs.
3. The Unescaped Dot
The failure this causes, executed:
import re
print("pattern r'drv.sv' against:")
for subject in ["drv.sv", "drvXsv", "drv sv", "drv.sv(88)"]:
hit = bool(re.search(r"drv.sv", subject))
print(f" {subject:<12} matches: {hit}")pattern r'drv.sv' against:
drv.sv matches: True
drvXsv matches: True
drv sv matches: True
drv.sv(88) matches: TrueAll four match, because . means any character — so the pattern says "d, r, v, anything, s, v". It matches the thing you wanted and three things you did not, and every match looks successful.
Escaping the dot states that you mean a literal one:
import re
print("pattern r'drv\\.sv' against:")
for subject in ["drv.sv", "drvXsv", "drv sv", "drv.sv(88)"]:
hit = bool(re.search(r"drv\.sv", subject))
print(f" {subject:<12} matches: {hit}")pattern r'drv\.sv' against:
drv.sv matches: True
drvXsv matches: False
drv sv matches: False
drv.sv(88) matches: TrueNow only the two subjects that genuinely contain drv.sv match. The fourth still matches because search looks anywhere in the string — the pattern is satisfied by the first six characters.
This is the quietest bug in the module. An unescaped dot does not fail; it over-matches, and the extra matches are usually plausible.
4. Escaping the Rest
The same backslash works for every reserved character:
import re
LINE = "UVM_ERROR drv.sv(88) @ 1420: [DRV] slack -0.142ns"
print("parentheses :", re.search(r"\(88\)", LINE).group())
print("brackets :", re.search(r"\[DRV\]", LINE).group())
print("dot in a num:", re.search(r"0\.142", LINE).group())
print("file and line:", re.search(r"drv\.sv\(88\)", LINE).group())parentheses : (88)
brackets : [DRV]
dot in a num: 0.142
file and line: drv.sv(88)Two notes on what does not need escaping.
Inside a character set (Chapter 19.5) most characters lose their special meaning, so [.] is a literal dot and [()] is a literal parenthesis — which is occasionally more readable than a backslash.
And a / is not a regex metacharacter at all. Engineers coming from sed or Perl escape it out of habit; in Python it is an ordinary character:
import re
PATH = "u_core/regfile/reg_q"
print("unescaped slash:", re.search(r"u_core/regfile", PATH).group())
print("escaped also works but is noise:", re.search(r"u_core\/regfile", PATH).group())unescaped slash: u_core/regfile
escaped also works but is noise: u_core/regfile5. re.escape for Literals You Did Not Write
Hand-escaping is fine for a pattern you type. It is wrong for a literal that arrives as data — a test name, a module path, a value from a config file — because you cannot know what characters it will contain:
import re
LINE = "running test smoke.rand_v2(seed=1234) now"
for name in ["smoke.rand_v2(seed=1234)", "cdc_test", "burst[2]"]:
naive = bool(re.search(name, LINE)) if name != "burst[2]" else "would raise or mis-match"
safe = bool(re.search(re.escape(name), LINE))
print(f"{name:<26} escaped match: {safe}")
print()
print("what re.escape produces:")
print(" ", re.escape("smoke.rand_v2(seed=1234)"))
print(" ", re.escape("+incdir+/proj/rtl"))smoke.rand_v2(seed=1234) escaped match: True
cdc_test escaped match: False
burst[2] escaped match: False
what re.escape produces:
smoke\.rand_v2\(seed=1234\)
\+incdir\+/proj/rtlre.escape inserts a backslash before every character that could be special, so the result matches the literal text and nothing else.
The case that makes it non-optional is a name containing a bracket. burst[2] used directly as a pattern is a character set meaning "b, u, r, s, t, then one of 2" — a completely different pattern that may match something unrelated, or may raise if the brackets are unbalanced:
import re
bad = "burst[2"
try:
re.search(bad, "burst[2] running")
except re.error as e:
print("unescaped, unbalanced:", type(e).__name__, "-", e)
print("escaped :", bool(re.search(re.escape("burst[2"), "burst[2] running")))unescaped, unbalanced: PatternError - unterminated character set at position 5
escaped : TrueIf the literal came from a variable, wrap it in
re.escape. If you typed it yourself, escape by hand and you will see what you are doing.
6. Reading a Pattern With Both Problems
Putting the two halves together on a real line:
import re
LINE = "UVM_ERROR drv.sv(88) @ 1420: slack -0.142ns [DRV]"
pattern = r"drv\.sv\((\d+)\)"
m = re.search(pattern, LINE)
print("pattern as written :", pattern)
print("what Python stores :", repr(pattern))
print("matched :", m.group())
print("the escaped dots are literal, the \\d is regex syntax")pattern as written : drv\.sv\((\d+)\)
what Python stores : 'drv\\.sv\\((\\d+)\\)'
matched : drv.sv(88)
the escaped dots are literal, the \d is regex syntaxThe repr is the useful line. Python stored each \x as two characters, and handed all of them to the regex engine, which then interpreted some as "literal" instructions (\., \(, \)) and one as a class (\d).
Both problems are visible in one pattern, and they are solved by different things: the r prefix got the backslashes through, and the backslashes you typed told regex what to treat literally.
7. Predict, Run, Observe
import re
print("A:", repr("\t"), repr(r"\t"))
print("B:", len("\t"), len(r"\t"))
print("C:", bool(re.search(r"a.c", "abc")))
print("D:", bool(re.search(r"a\.c", "abc")))
print("E:", bool(re.search(r"a\.c", "a.c")))
print("F:", re.escape("a.c"))
print("G:", bool(re.search("\d", "7")))
print("H:", re.search(r"[.]", "a.c").group())A: '\t' '\\t'
B: 1 2
C: True
D: False
E: True
F: a\.c
G: True
H: .A and B — the non-raw tab is one character, the raw version is two. That is Problem One in a single line.
C and D are the same pattern differing by one backslash and giving opposite answers on abc. a.c matched because . is any character; a\.c did not because abc contains no literal dot.
G is True and that is the dangerous one. The non-raw "\d" worked — Python left the unrecognised escape alone — so the bug does not announce itself here. It announces itself on "\b", which Python does recognise and converts to a backspace. Writing r"..." always means never needing to know which escapes Python claims.
H — inside a character set, a dot is literal with no backslash needed, which is Chapter 19.5's subject.
8. Exercises
Exercise 1 — two characters or one. Print the repr and len of "\n", r"\n", "\d" and r"\d". Say which pairs differ.
Exercise 2 — Python's warning. Capture the SyntaxWarning from a non-raw "\d" as Section 1 does. Say what a future Python version will do instead.
Exercise 3 — the backspace. Compare "\b" and r"\b". Explain why a word-boundary pattern written without the prefix silently becomes something else.
Exercise 4 — the over-matching dot. Write r"drv.sv" and find three subjects it matches that you did not intend. Then escape it.
Exercise 5 — a real line. Write a pattern matching drv.sv(88) exactly, escaping every reserved character. Verify it rejects drvXsv[88].
Exercise 6 — from data. Search for a test name held in a variable, where the name contains a dot. Show the unescaped version over-matching and fix it with re.escape.
Exercise 7 — the bracket. Use an unbalanced bracket as a pattern and record the error. Then escape it.
Exercise 8 — no escape needed. Find three characters common in tool output that are not regex metacharacters. Confirm they match unescaped.
9. Summary
- There are two separate backslash problems, and conflating them is why the topic confuses people.
- Python processes a string literal before regex sees it, turning
\nand\tinto single characters. r"..."stops that, passing backslashes through unchanged.- Python warns on an unrecognised escape such as
\d, and a future version will make it an error. "\b"is a backspace character whiler"\b"is regex's word boundary — the case where omitting the prefix silently changes the pattern.- Write every pattern raw. It costs one character.
- Regex reserves about a dozen characters, and VLSI tool output is unusually dense in them.
- An unescaped dot over-matches rather than failing, which makes it the quietest bug in the module.
/is not a metacharacter in Python regex; escaping it is habit from other tools.- Use
re.escapewhenever the literal came from a variable — a test name with a dot or a bracket is enough to cause the bug.
The next chapter stops matching exact text and starts matching shapes.
Where this fits
Part of the Python curriculum.
