Python Part I · Module 4 · Strings
Building Text With f-Strings
Almost everything a script produces is text with values in it — a log path built from a test name and a seed, a run directory, a command to hand to a tool, a line of a report. Chapter 4.3 showed how to do that with concatenation and str(), which works and is worth knowing. It is also verbose enough that people get it wrong, and the wrongness is usually a missing space or a forgotten conversion. Python has a better way, added to the language in version 3.6 and now the standard approach: put an f in front of the quotes and write the values directly into the text where they belong. This chapter covers it properly, including the mistake that produces no error at all.
Foundation12 min readPythonPart IFundamentalsStringsf-strings
Part I · Module 4 · Chapter 4.12 · Building Text With f-Strings
1. The Problem
You have a test name and a seed, and you need the path to that run's log file:
test_name = "axi_smoke"
seed = 4210
print("runs/" + test_name + "_" + str(seed) + "/sim.log")runs/axi_smoke_4210/sim.logThat is correct, and it is exactly what Chapter 4.3 taught. It is also five operands, three separator literals typed by hand, and one str() call that is only there because Python refuses to concatenate text with a number. Count the ways it can go wrong: a missing slash, a missing underscore, a forgotten str(), a slash in the wrong operand.
Here is the same result:
test_name = "axi_smoke"
seed = 4210
print(f"runs/{test_name}_{seed}/sim.log")runs/axi_smoke_4210/sim.logThe string now looks like the thing it produces. That is the whole argument, and it is a strong one — you can see the result in the source, so a missing slash is visible rather than deduced.
2. How It Works
Two pieces of syntax:
status = "PASS"
print(f"status={status}")status=PASS- The
fbefore the opening quote marks this as a formatted string. It is part of the syntax, not a comment or a convention. - Braces inside the text mark a place where a value goes.
{status}means "put the value ofstatushere".
Everything outside the braces is ordinary text and appears exactly as written. Everything inside is evaluated and its value put in place.
Note what did not happen: no str() anywhere. Python converts whatever the value is to text for you as it builds the string, so a number needs no conversion:
seed = 4210
print(f"seed is {seed}")
print(type(f"seed is {seed}"))seed is 4210
<class 'str'>That second line is worth registering. The result of an f-string is an ordinary string — type str, like any other. It is not a special kind of value, and everything in Chapters 4.4 through 4.11 applies to it. Nothing is deferred until printing; the string is fully built the moment the line runs.
Several values in one string is the normal case:
test_name = "axi_smoke"
seed = 4210
status = "PASS"
runtime_s = 182.4
print(f"{test_name} seed={seed} status={status} runtime={runtime_s}")axi_smoke seed=4210 status=PASS runtime=182.4Four values, and the spacing between them is visible in the source rather than assembled from separator literals.
3. What Can Go Inside the Braces
Not just names. Any expression, in the full Module 3 sense:
test_name = "axi_smoke"
seed = 4210
print(f"{test_name.upper()}")
print(f"{len(test_name)}")
print(f"{seed + 1}")AXI_SMOKE
9
4211A method call, a function call, an arithmetic expression. The brace contents are evaluated first, then the result is placed into the text — which is exactly the evaluation model Chapter 3.1 established, happening in a new place.
Slices work too, which is occasionally handy:
line = "UVM_ERROR: x"
print(f"severity={line[:9]}")severity=UVM_ERROR4. Quotes and Braces Inside the Text
Everything Chapter 4.2 said about quotes still applies. Use the quote style the text does not contain, or escape it:
status = "PASS"
print(f'the tool said "{status}"')
print(f"the tool said \"{status}\"")the tool said "PASS"
the tool said "PASS"Both work. The single-quoted version is cleaner and is the reason having two quote characters is useful.
Triple quotes work as well, giving multi-line text with values in it:
test_name = "axi_smoke"
seed = 4210
banner = f"""test: {test_name}
seed: {seed}"""
print(banner)test: axi_smoke
seed: 4210And if you need a literal brace in the output — which is rare but does happen — double it:
status = "PASS"
print(f"{{not a field}} {status}"){not a field} PASS{{ produces one {, on the same principle as \\ producing one backslash in Chapter 4.2: two characters in the source, one in the value.
5. Broken on Purpose
Forgetting the f. This is the most important example in the chapter, because it does not raise anything:
seed = 4210
print("seed={seed}")seed={seed}No error. Without the f, that is an ordinary string, and {seed} is seven ordinary characters — a brace, four letters, a brace. Python printed exactly what you wrote.
This is a silent failure of the same family as the discarded method result in Chapter 4.11, and it has the same diagnosis: if you see braces in your output, you forgot the f. Once you know the symptom it takes two seconds to spot.
A name that does not exist. In contrast, this one is loud:
print(f"{missing_name}")NameError: name 'missing_name' is not definedWhich confirms Section 3's claim: the brace contents are real code, evaluated when the line runs. A misspelled name inside braces fails exactly as it would anywhere else, and Chapter 1.11 taught you to read this message.
The contrast between these two failures is the thing to take away. With the f, mistakes inside braces are reported. Without it, there are no braces — only text.
6. A Debugging Form Worth Knowing
Put an = immediately after the expression and the f-string prints the expression and its value:
seed = 4210
runtime_s = 182.4
print(f"{seed=}")
print(f"{runtime_s=}")seed=4210
runtime_s=182.4The name, the equals sign and the value, all from three extra characters. When you are working out why a script is doing the wrong thing, this is the fastest way to print several values without typing out labels — and the labels cannot drift out of step with the values, because Python writes them.
7. Which Form to Use
You have now seen three ways to combine text and values, and they are not equally good.
| Form | Verdict |
|---|---|
f"runs/{name}_{seed}/sim.log" | Use this. The output is visible in the source and no conversion is needed. |
"runs/" + name + "_" + str(seed) + "/sim.log" | Correct, verbose, easy to get wrong. Know how to read it — plenty of existing code uses it. |
" ".join(pieces) | The right tool when the pieces are uniform and there are many of them, as Chapter 4.9 showed. Not a competitor for this job. |
You may also meet "runs/{}/{}".format(name, seed) and "runs/%s" % name in older code. Both predate f-strings and do the same job less clearly. You do not need to write either; if you meet one, it is putting values into text.
8. What Is Still Missing
Look closely at one line from Section 2:
axi_smoke seed=4210 status=PASS runtime=182.4Now imagine a hundred of those, one per test, and the problem becomes obvious: nothing lines up. The name column is a different width for every test, so the numbers wander across the page and the whole thing is unreadable as a table.
An f-string can fix that, because the braces accept more than just what to show — they also accept instructions about how to show it, after a colon:
test_name = "axi_smoke"
runtime_s = 182.4
print(f"{test_name:<14}{runtime_s:>9.1f}")axi_smoke 182.4That is a fixed-width name column and a right-aligned runtime with one decimal place. The next chapter — the last of this module — is entirely about what goes after that colon.
9. Predict, Run, Observe
Predict all five before reading on. One of them prints something surprising.
name = "smoke"
n = 7
print(f"{name}_{n}")
print(f"{name}{n}")
print(f"{name} has {len(name)} characters")
print("{name}_{n}")
print(f"{n * 2}")The verified output:
smoke_7
smoke7
smoke has 5 characters
{name}_{n}
14The second has no separator because none was written — an f-string inserts nothing you did not put there, exactly as + did in Chapter 4.3.
The fourth is the surprise, and it is the Section 5 failure: no f, so the braces are just characters and Python printed them. If you predicted smoke_7 you assumed the braces are always special. They are only special inside an f-string.
The fifth is 14, not 77 — n * 2 is arithmetic because n is a number. Had n been the string "7", {n * 2} would have given 77 by the repetition rule from Chapter 4.3. The braces do not change what an expression means; they only place its result into text.
10. Exercises
Exercise 1 — convert to f-strings. Rewrite each using an f-string, and say how many characters of punctuation you removed:
name = "reg_rw"
seed = 91
print("test " + name + " seed " + str(seed))
print("runs/" + name + "/sim.log")Exercise 2 — predict. Given t = "burst" and k = 3, give the exact output of each:
(a) f"{t}{k}" (b) f"{t}_{k}" (c) f"{t} {k}" (d) f"{t.upper()}" (e) f"{len(t) + k}" (f) "{t}"
Exercise 3 — the silent failure. A colleague says their script prints path={p} instead of the path. Say in one sentence what is wrong, how you knew from the symptom alone, and what the fix is.
Exercise 4 — build a path. Given base = "runs", test = "axi_smoke", seed = 4210, write one f-string producing exactly runs/axi_smoke_4210/sim.log. Then write the concatenation version and say which you would rather review.
Exercise 5 — quotes. Write an f-string whose output is exactly the log says "PASS", given status = "PASS". Do it twice — once using single outer quotes, once with escapes — and say which you prefer.
Exercise 6 — two failures. Run both of these. Write down what each does, and explain in two sentences why one raises and the other does not:
print(f"{undefined_thing}")
print("{undefined_thing}")Exercise 7 — the debug form. Use the = form to print the name and value of a variable holding a runtime. Then say why you would not use that form in a report a colleague reads.
Exercise 8 — expression or name. This works but is hard to read. Rewrite it so the braces hold only names, and say why that version is better:
line = " UVM_ERROR: mismatch "
print(f"severity is {line.strip().partition(':')[0].lower()}")11. Summary
- An f-string puts values inside text.
fbefore the opening quote, braces where a value goes. - The
fis part of the syntax. Without it, braces are ordinary characters — and there is no error. Braces in your output mean a missingf. - No
str()is needed. Python converts each value to text as it builds the string. - The result is an ordinary
str, fully built when the line runs. - Any expression can go inside the braces — a method call, a function call, arithmetic, a slice — because the contents are evaluated first.
- Keep the braces short anyway. Compute anything substantial on its own line and put the name in the braces.
- A misspelled name inside braces is a
NameError, which is the proof that brace contents are real code. - Quote rules from Chapter 4.2 still apply, and
{{produces one literal brace. f"{value=}"prints the expression and its value — excellent for debugging, not for output a reader sees.- Prefer f-strings over concatenation with
str(), and over the older.format()and%forms, because each value stays where it appears in the text. - A colon inside the braces introduces formatting instructions, which is the next chapter.
You can now build any text you need. The final chapter of this module makes a column of numbers line up.
Where this fits
Part of the Python curriculum.
