Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
test_name = "axi_smoke"
seed = 4210
print("runs/" + test_name + "_" + str(seed) + "/sim.log")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
runs/axi_smoke_4210/sim.log

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
test_name = "axi_smoke"
seed = 4210
print(f"runs/{test_name}_{seed}/sim.log")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
runs/axi_smoke_4210/sim.log

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
status = "PASS"
print(f"status={status}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
status=PASS
  • The f before 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 of status here".

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
seed = 4210
print(f"seed is {seed}")
print(type(f"seed is {seed}"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
test_name = "axi_smoke"
seed = 4210
status = "PASS"
runtime_s = 182.4
print(f"{test_name} seed={seed} status={status} runtime={runtime_s}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
axi_smoke seed=4210 status=PASS runtime=182.4

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
test_name = "axi_smoke"
seed = 4210
print(f"{test_name.upper()}")
print(f"{len(test_name)}")
print(f"{seed + 1}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
AXI_SMOKE
9
4211

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "UVM_ERROR: x"
print(f"severity={line[:9]}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
severity=UVM_ERROR

4. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
status = "PASS"
print(f'the tool said "{status}"')
print(f"the tool said \"{status}\"")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
test_name = "axi_smoke"
seed = 4210
banner = f"""test: {test_name}
seed: {seed}"""
print(banner)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
test: axi_smoke
seed: 4210

And if you need a literal brace in the output — which is rare but does happen — double it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
status = "PASS"
print(f"{{not a field}} {status}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{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:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
seed = 4210
print("seed={seed}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
print(f"{missing_name}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
NameError: name 'missing_name' is not defined

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
seed = 4210
runtime_s = 182.4
print(f"{seed=}")
print(f"{runtime_s=}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
seed=4210
runtime_s=182.4

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
axi_smoke seed=4210 status=PASS runtime=182.4

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
test_name = "axi_smoke"
runtime_s = 182.4
print(f"{test_name:<14}{runtime_s:>9.1f}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
axi_smoke         182.4

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke_7
smoke7
smoke has 5 characters
{name}_{n}
14

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

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line = "  UVM_ERROR: mismatch  "
print(f"severity is {line.strip().partition(':')[0].lower()}")

11. Summary

  • An f-string puts values inside text. f before the opening quote, braces where a value goes.
  • The f is part of the syntax. Without it, braces are ordinary characters — and there is no error. Braces in your output mean a missing f.
  • 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.