Strings and f-Strings
In Chapter 47, you wrote name: str = "James" and moved on. A string was just a label, a value you stored and printed. Now strings become your primary data type for building SmartNotes: titles, note bodies, formatted reports, tag lists. You need to do more than store text. You need to build it, format it, and measure it.
Emma pulls up a SmartNotes requirement. "We need a function that displays a note summary: the title, word count, and reading time, all in one line. How would you build that string?"
James tries concatenation: "Title: " + title + " Words: " + str(word_count). It works, but it is ugly and fragile.
"There is a better way," Emma says. She types an f before the opening quote.
Three Ways to Create Strings
Python gives you three quote styles. Each serves a different purpose:
# Single quotes: simple text
greeting: str = 'Hello, World!'
# Double quotes: same as single quotes
greeting: str = "Hello, World!"
# Triple double quotes: text that spans multiple lines
note_body: str = """This is the first line.
This is the second line.
This is the third line."""
# Triple single quotes: also spans multiple lines (works the same way)
note_body: str = '''This is the first line.
This is the second line.
This is the third line.'''
Single and double quotes are interchangeable. Pick one style and use it consistently. Triple quotes (both """...""" and '''...''') preserve line breaks, which makes them useful for multi-line text like note bodies or descriptions.
Quotes Inside Quotes
The real reason Python gives you both single and double quotes: you can put one kind inside the other without any tricks.
# Double quotes inside single quotes
message: str = 'Emma said "use f-strings" and walked away'
# Single quotes inside double quotes
greeting: str = "Hi 'Emma', how are you?"
# Both work because the outer quotes are different from the inner quotes
Output:
Emma said "use f-strings" and walked away
Hi 'Emma', how are you?
If you need the same quote character inside and outside, use the escape character \" or \':
# Escaped double quote inside double quotes
message: str = "Emma said \"use f-strings\" and walked away"
# Escaped single quote inside single quotes
greeting: str = 'Hi \'Emma\', how are you?'
Both approaches produce the same output. Mixing quote styles is cleaner than escaping, so prefer 'He said "hello"' over "He said \"hello\"".
Single quotes ('hello') and double quotes ("hello") create identical strings. Python does not care which you use. The convention in most Python projects is double quotes. The practical difference shows up when your text contains quotes: wrap the string in the other kind to avoid escaping. Triple quotes ("""...""" or '''...''') let you write text across multiple lines without any special characters.
f-Strings: Embedding Data in Text
Suppose you have a name and an age, and you want to build a sentence like "James is 30 years old". You could glue pieces together with +:
name: str = "James"
age: int = 30
intro: str = name + " is " + str(age) + " years old"
This works, but it is hard to read and easy to break. You have to convert age to a string with str(), and every space needs its own quoted piece.
An f-string solves this. Put an f before the opening quote, and Python lets you drop variables directly into the text using curly braces {}:
name: str = "James"
age: int = 30
intro: str = f"{name} is {age} years old"
Output:
James is 30 years old
Python sees the f, finds each {} pair, evaluates whatever is inside, and places the result into the string. No +, no str() conversion.
You can put more than just variable names inside the braces. Any piece of code that produces a value works. In Python, a piece of code that produces a value is called an expression: a variable name like name, a math calculation like word_count / wpm, or a function call like len(title) are all expressions.
word_count: int = 1500
wpm: int = 250
summary: str = f"Reading time: {word_count / wpm} minutes"
Output:
Reading time: 6.0 minutes
Format Specifiers
The number 6.0 is fine, but what if the division produces 6.123456789? You probably want to display 6.1 instead of all those decimal places. A format specifier tells Python how to display a value inside an f-string.
The syntax is: inside the curly braces, after your expression, add a colon : followed by a formatting rule. The colon separates "what value to show" from "how to show it":
f"{expression:formatting_rule}"
↑ ↑
what how
Here are the format specifiers you will use most often:
| Specifier | What It Does | Example | Result |
|---|---|---|---|
:.1f | Show 1 decimal place | f"{3.14159:.1f}" | "3.1" |
:.2f | Show 2 decimal places | f"{3.14159:.2f}" | "3.14" |
:, | Add comma as thousands separator | f"{1500000:,}" | "1,500,000" |
The f in :.1f stands for "fixed-point," meaning a fixed number of decimal places. The number before f controls how many.
Try all three:
price: float = 3.14159
population: int = 1500000
print(f"1 decimal: {price:.1f}")
print(f"2 decimals: {price:.2f}")
print(f"With commas: {population:,}")
Output:
1 decimal: 3.1
2 decimals: 3.14
With commas: 1,500,000
Now the reading time example makes sense:
word_count: int = 1537
wpm: int = 250
summary: str = f"Reading time: {word_count / wpm:.1f} minutes"
Output:
Reading time: 6.1 minutes
Without :.1f, the result would be 6.148, which is more precision than a reader needs. The format specifier rounds it to one decimal place for display.
Python has older ways to format strings (str.format() and % formatting). f-strings replaced both. If you have used those before, the format specifiers (:.1f, :,) work the same way inside f-strings.
Escape Sequences
Some characters have special meaning inside strings. To include them literally, you use a backslash \ as an escape character:
# Newline: \n starts a new line
header: str = "Title\tWord Count\nMy Note\t350"
# Tab: \t inserts a horizontal tab
row: str = "Python\t85\tPassed"
# Backslash: \\ produces a single backslash
path: str = "C:\\Users\\James\\notes.txt"
Output (from print statements):
Title Word Count
My Note 350
Python 85 Passed
C:\Users\James\notes.txt
| Escape | What It Produces | When You Need It |
|---|---|---|
\n | New line | Multi-line output from a single string |
\t | Tab | Aligning columns in text |
\\ | Single backslash | File paths on Windows |
\' | Single quote | Single quote inside a single-quoted string |
\" | Double quote | Double quote inside a double-quoted string |
\r | Carriage return | Reading files created on Windows (lines end with \r\n) |
\0 | Null character | Rare, but you may see it in data from external systems |
If a string has many backslashes (like a Windows file path), you can put r before the opening quote. Python then treats every backslash as a literal character, not an escape:
# Without raw string: every backslash must be doubled
path: str = "C:\\Users\\James\\notes.txt"
# With raw string: backslashes are literal
path: str = r"C:\Users\James\notes.txt"
Both produce the same result: C:\Users\James\notes.txt. Raw strings save you from doubling every \.
Measuring Strings with len()
Python comes with a set of ready-to-use functions that you do not need to create yourself. These are called built-in functions. One of them is len(), which returns the number of characters in a string, including spaces and special characters:
title: str = "SmartNotes"
length: int = len(title)
Output:
10
Spaces count:
greeting: str = "Hello, World!"
len(greeting)
Output:
13
An empty string has length zero:
empty: str = ""
len(empty)
Output:
0
len() works on any sequence, not just strings. You will use it again with lists and dicts in Lesson 4.
PRIMM-AI+ Practice: Building f-Strings
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode before predicting.
Look at these f-strings. Without running anything, predict the exact output of each one. Write your predictions and a confidence score from 1 to 5 before checking.
name: str = "Alice"
score: int = 92
total: int = 100
line1: str = f"{name} scored {score}/{total}"
line2: str = f"Percentage: {score / total * 100:.1f}%"
line3: str = f"Name length: {len(name)}"
Check your predictions
line1: "Alice scored 92/100". The braces insert the variable values directly. The / between {score} and {total} is literal text, not division.
line2: "Percentage: 92.0%". The expression score / total * 100 evaluates to 92.0, and :.1f formats it to one decimal place. The % after the closing brace is literal text.
line3: "Name length: 5". len(name) evaluates to 5 because "Alice" has 5 characters.
If you got all three correct, your f-string intuition is solid. The tricky part is recognizing which / and % are operators and which are literal text.
Run
Press Shift+Tab to exit Plan Mode.
Create a file called fstring_practice.py with the three f-strings above. Add print() calls for each one. Run it with uv run python fstring_practice.py. Compare the output to your predictions.
Investigate
For any prediction that was wrong, write one sentence explaining what you misunderstood. Focus on the difference between expressions inside {} (evaluated by Python) and text outside {} (printed literally).
If you want to go deeper, run /investigate @fstring_practice.py in Claude Code and ask why score / total * 100 evaluates the way it does inside an f-string.
Modify
Change score to 78 and total to 90. Predict the new output of all three lines before running. Did line3 change? (It should not, because len(name) does not depend on score or total.)
Make [Mastery Gate]
Without looking at any examples, write 3 f-strings for SmartNotes:
- An f-string that shows a note title and word count:
"My Note: 350 words" - An f-string that calculates and displays reading time with one decimal place
- An f-string that shows the note title length
Run your file with uv run python to verify the output matches what you expect.
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Check Your f-String Understanding
I'm learning Python f-strings. Here is my understanding:
"An f-string evaluates expressions inside curly braces at
runtime, and :.1f means one decimal place." Is my summary
accurate? What details am I missing?
Read AI's response. Did it mention that the format specifier goes after a colon inside the braces? Did it explain that the expression can be any valid Python expression, not just variable names? Compare its answer to what you learned in this lesson.
What you're learning: You are verifying your mental model by stating it confidently and letting AI find gaps. This is the same verify-then-refine pattern you use when reviewing AI-generated code.
Prompt 2: Generate Formatted Output
Write 5 Python f-strings for a simple note-taking app.
Each f-string should format different data: a title with
word count, a reading time with one decimal place, a
percentage complete, a file path, and a multi-part summary
using at least two variables. Use type annotations on all
variables.
Read AI's output. Check: did it use :.1f for the reading time? Did it use type annotations on every variable? Are the f-string expressions correct? If anything looks wrong, tell AI what to fix and why.
What you're learning: You are reviewing AI-generated string formatting against the rules from this lesson. Spotting format specifier errors now saves debugging time later.
James leans back. "So single quotes, double quotes, triple quotes, f-strings, escape sequences, and len(). That is a lot for one lesson."
Emma shakes her head. "You will use f-strings and len() every day. The rest you will recognize when you see it. The important thing is: you can build any string you need now, and you can measure it."
"What I cannot do," James says, "is pull out a single character. If the title is 'SmartNotes', how do I get just the 'S'?"
"That is Lesson 2. You reach inside the string with title[0]."