Make a day03 folder for today’s files, open it in your editor, and run each script from its terminal, as on Days 1 and 2.
3.1 Quotes and Escape Characters
Since Day 1 you have written text like "Hello". Python calls a piece of text a string, and its type is str. A string is a sequence of characters: letters, digits, spaces, punctuation, each one a single symbol. Today you learn to take strings apart, change them, and build them into neat output. First, how do you put a quote mark inside a string, when quote marks are what start and end it?
Python lets you write a string with either kind of quote, "double" or 'single'. Both make exactly the same string. Having two kinds means you can use one kind to hold the other:
print("Hello, Ana")
print('Hello, Ana')
print("It's raining")
print('She said "hi"')Hello, Ana
Hello, Ana
It's raining
She said "hi"
Lines 1 and 2 print the same thing: the quotes are not part of the text. In line 3 the apostrophe is safe because the string is in double quotes, and in line 4 the double quotes are safe inside single quotes. This course uses double quotes unless the text holds a ".
Watch out
An apostrophe inside single quotes. Python ends a string at the first quote that matches the opening one:
print('It's raining') File ".../day03/apostrophe_in_single.py", line 1
print('It's raining')
^
SyntaxError: unterminated string literal (detected at line 1)Python reads 'It' as a whole string, then sees s raining, then a new ' that is never closed. It is the same “unterminated string literal” error you met on Day 1. Use double quotes: print("It's raining").
Sometimes you need both kinds of quote, or a character you cannot type into a string at all, such as a line break. For these, Python uses an escape character: a backslash \ followed by a letter or symbol, which together stand for one special character. (“Escape” because the backslash escapes the normal meaning of the next character.)
| Escape | Means | Example | Prints |
|---|---|---|---|
\n |
New line | "one\ntwo" |
one and two |
\t |
Tab (jumps to the next column) | "Name:\tAna" |
Name: then a gap |
\" |
A double quote | "say \"hi\"" |
say "hi" |
\' |
A single quote | 'It\'s' |
It's |
\\ |
One backslash | "C:\\new_folder" |
C:\new_folder |
Worked example: Escapes in action
print("Line one\nLine two")
print("Name:\tAna")
print("She said \"hi\"")
print('It\'s raining')
print("C:\\new_folder")Line one
Line two
Name: Ana
She said "hi"
It's raining
C:\new_folderLine 1 prints on two lines: \n is a line break, not two characters. Line 3’s \" keeps the quotes from ending the string. Line 5 needs \\: a single \ before n would make \n, a new line, and the output would break after C:.
For text that spans several lines, such as a poem or a menu, writing \n everywhere is hard to read. A triple-quoted string starts and ends with three quote marks, """ (or '''), and can run over several lines. Every line break inside it becomes part of the string:
poem = """Roses are red,
violets are blue,
this string has three lines."""
print(poem)Roses are red,
violets are blue,
this string has three lines.
Finally, the REPL shows a string differently from print. The REPL shows a string as you would type it into code: in quotes (single unless the text holds an apostrophe), with escapes written out. print shows the characters themselves:
>>> "It's raining"
"It's raining"
>>> 'Hello'
'Hello'
>>> "Line one\nLine two"
'Line one\nLine two'
>>> print("Line one\nLine two")
Line one
Line twoKey idea
A string is text in matching single, double, or triple quotes. A backslash starts an escape character: \n is a new
line, \t a tab, \" and \' quote marks, and \\ one backslash.
Pause and try
Question 1 · Tracing
What does this program print?
print("Ready,\nset,\ngo!")
print("Say \"cheese\"")
print('Don\'t type \\n')Check your reasoning
Ready,
set,
go!
Say "cheese"
Don't type \n- Line 1 has two
\nescapes, so it prints on three lines. - Line 2’s
\"escapes print as plain double quotes. - Line 3 is in single quotes, so the apostrophe needs
\'. Then\\prints one backslash, and thenafter it is an ordinary letter, so you see\nas two characters, not a line break.
Reread 3.1 Quotes and Escape Characters if your output differed.
Question 2 · Multiple choice
Which line prints Ana's "cat" without an error?
print('Ana's "cat"')print("Ana's "cat"")print("Ana's \"cat\"")print("Ana\'s "cat"")
Check your reasoning
C. The double quotes around the text are escaped with \", so they do not end the string, and the apostrophe is safe inside double quotes.
- A is wrong: the apostrophe ends the single-quoted string after
Ana, aSyntaxError. - B is wrong: the
"beforecatends the string, andcatbecomes a stray name, aSyntaxError. - D is wrong:
\'is fine, but the unescaped"beforecatstill ends the string.
Reread 3.1 Quotes and Escape Characters if you chose differently.
3.2 Length, Concatenation, and in
Once text is in a string, the first questions are often simple: how long is it, can I stick two strings together, and does it contain some word? Python answers each with one short piece of syntax.
- The built-in function
len()gives the length of a string: how many characters it has. Spaces and punctuation count, and the empty string""has length 0. - Concatenation means joining strings end to end, and
+does it for strings. Python adds nothing between them; a space appears only if you include one. - Repetition:
*with a string and an int repeats the string that many times.
>>> len("Python")
6
>>> len("New York")
8
>>> len("")
0
>>> "snow" + "ball"
'snowball'
>>> "snow" + " " + "ball"
'snow ball'
>>> "ha" * 3
'hahaha'
>>> "-" * 10
'----------'Notice that + and * mean something different for strings than for numbers. The type of the values decides what an operator does, as Day 2 showed with /.
Worked example: A name badge
first_name = "Grace"
last_name = "Hopper"
full_name = first_name + " " + last_name
border = "*" * (len(full_name) + 4)
print(border)
print("* " + full_name + " *")
print(border)
print("Characters:", len(full_name))****************
* Grace Hopper *
****************
Characters: 12| Line | Worked out | Value |
|---|---|---|
| 3 | "Grace" + " " + "Hopper" |
"Grace Hopper" |
| 4 | len is 12, plus 4 is 16 |
"*" repeated 16 times |
| 6 | "* " + full_name + " *" |
"* Grace Hopper *" |
The border is 4 characters longer than the name to make room for "* " and " *". Change first_name to "Ada" and the border shrinks to fit, because it is worked out from len(full_name).
Two more operators ask yes-or-no questions about strings, so they give a bool (Day 2):
inasks whether one string appears inside another:"York" in "New York"isTrue. The part you look for is a substring, a run of characters inside a string.==asks whether two values are equal:"stop" == "stop"isTrue. It is two equals signs; a single=assigns. Day 4 covers comparisons fully; today you only need==to check whether two strings match.
>>> "York" in "New York"
True
>>> "york" in "New York"
False
>>> "ark" in "parking"
True
>>> "stop" == "stop"
True
>>> "stop" == "Stop"
FalseBoth are case-sensitive: "y" and "Y" are different characters, so "york" is not in "New York".
Key idea
len(text) counts characters, + joins strings, and text * n repeats one. part in text and a == b give True
or False, and both care about capital letters.
Watch out
Joining a string and a number with +. + joins strings or adds numbers, but it will not guess which you mean when you mix them:
age = 15
print("Age: " + age)Traceback (most recent call last):
File ".../day03/age_label.py", line 2, in <module>
print("Age: " + age)
~~~~~~~~^~~~~
TypeError: can only concatenate str (not "int") to strRead it as “a string can only be joined to another string, not to an int.” Convert the number first, "Age: " + str(age), or let print separate its arguments with commas, print("Age:", age). Section 3.8 shows a third, neater way.
Pause and try
Question 3 · Tracing
Predict exactly what the REPL prints for each line.
>>> len("hi there")
>>> "ab" * 2 + "c"
>>> "3" + "4"
>>> "car" in "Scar"Check your reasoning
>>> len("hi there")
8
>>> "ab" * 2 + "c"
'ababc'
>>> "3" + "4"
'34'
>>> "car" in "Scar"
True"hi there"has 2 letters, a space, and 5 letters: 8. The space counts.*comes before+, so"ab" * 2is'abab', then+ "c"gives'ababc'."3"and"4"are strings, so+joins them:'34', not 7."car"appears inside"Scar"(the last three letters), so True.
Reread 3.2 Length, Concatenation, and in if any answer differed.
3.3 Indexing: One Character at a Time
To print someone’s initials, you need the first character of a name, not the whole string. Python numbers every character’s position so you can ask for one.
The position of a character is its index. Indexes start at 0, not 1: the first character is at index 0, the second at index 1, and so on. Think of the index as “how many characters come before this one”: the first character has 0 before it. To get the character at an index, write the index in square brackets after the string. This is called indexing.
Python also counts from the end with negative indexes: -1 is the last character, -2 the one before it. Here are both numberings for "Python":
| Character | Index | Negative index |
|---|---|---|
P |
0 | -6 |
y |
1 | -5 |
t |
2 | -4 |
h |
3 | -3 |
o |
4 | -2 |
n |
5 | -1 |
>>> word = "Python"
>>> word[0]
'P'
>>> word[3]
'h'
>>> word[5]
'n'
>>> word[-1]
'n'
>>> word[-6]
'P'
>>> word[len(word) - 1]
'n'A string of 6 characters has indexes 0 to 5, so the last index is always len(word) - 1. word[-1] is a shorter way to say the same thing, and it works for a string of any length. Indexing gives a new string of length 1; Python has no separate type for a single character.
Now vary the index past the end:
word = "Python"
print(word[6])Traceback (most recent call last):
File ".../day03/index_too_far.py", line 2, in <module>
print(word[6])
~~~~^^^
IndexError: string index out of range
An IndexError means you asked for a position that does not exist. "Python" has 6 characters, but because counting starts at 0, there is no index 6.
Key idea
text[i] is the character at index i. Indexes run from 0 to len(text) - 1, and negative indexes count from the
end, with -1 the last character. An index past the end is an IndexError.
Watch out
Counting from 1. It is natural to call the first letter “letter 1”, but word[1] is the second character, 'y'. Subtract 1 from the everyday position to get the index. That is also why word[len(word)] is always an IndexError.
Pause and try
Question 4 · Tracing
Predict exactly what the REPL prints for each line. Draw the index table for "Lisbon" first.
>>> city = "Lisbon"
>>> city[1]
>>> city[-2]
>>> city[len(city) - 3]Check your reasoning
>>> city = "Lisbon"
>>> city[1]
'i'
>>> city[-2]
'o'
>>> city[len(city) - 3]
'b'The indexes are L0 i1 s2 b3 o4 n5, and from the end n-1 o-2 b-3 s-4 i-5 L-6.
city[1]is the second character,'i'(not'L', which is index 0).city[-2]is second from the end,'o'.len(city)is 6, so the index is 6 - 3 = 3, which holds'b'.
Reread 3.3 Indexing: One Character at a Time if any answer differed.
3.4 Slicing
A date stored as "2026-10-09" holds a year, a month, and a day. Indexing gets one character at a time; to get the four-character year you need a range of positions.
A slice is a part of a string taken from a range of indexes, written text[start:stop]. It starts at start and stops before stop: the character at stop is not included. So word[0:2] is index 0 and index 1, two characters. A handy check: a slice has stop - start characters.
Either number can be left out. Leave out start and the slice begins at the start of the string; leave out stop and it runs to the end.
>>> word = "Python"
>>> word[0:2]
'Py'
>>> word[2:5]
'tho'
>>> word[:3]
'Pyt'
>>> word[3:]
'hon'
>>> word[-3:]
'hon'
>>> word[:]
'Python'
>>> word[4:100]
'on'word[2:5]takes indexes 2, 3, and 4:'tho'.word[:3]is the first three characters, andword[3:]is everything from index 3 on. Together they rebuild"Python".word[-3:]starts three from the end: the last three characters.word[4:100]goes past the end, but a slice never causes anIndexError: it stops at the end of the string. Only indexing a single position does.
Worked example: Taking a date apart
date = "2026-10-09"
year = date[0:4]
month = date[5:7]
day = date[8:]
print("Year:", year)
print("Month:", month)
print("Day:", day)Year: 2026
Month: 10
Day: 09| Part | Indexes | Slice |
|---|---|---|
2026 |
0 to 3 | date[0:4] |
- |
4 | skipped |
10 |
5 to 6 | date[5:7] |
- |
7 | skipped |
09 |
8 to 9 | date[8:] |
Each stop is one past the last index wanted, and date[8:] runs to the end. Each part is still a string: day is "09", not the number 9.
A slice can take a third number, the step: text[start:stop:step] takes every step-th character. A step of 2 takes every second character. A negative step walks backwards, so text[::-1], with start and stop left out, is the whole string reversed.
>>> letters = "abcdefgh"
>>> letters[0:6:2]
'ace'
>>> letters[::2]
'aceg'
>>> letters[1::2]
'bdfh'
>>> letters[::-1]
'hgfedcba'
>>> "stressed"[::-1]
'desserts'letters[0:6:2] takes indexes 0, 2, and 4. letters[1::2] starts at 1 and takes every second character to the end. [::-1] is the usual way to reverse a string in Python, and with == from 3.2 it tells you whether a word reads the same both ways.
Key idea
text[start:stop] takes characters from start up to, but not including, stop; a missing start or stop means
the beginning or the end. text[start:stop:step] takes every step-th character, and text[::-1] reverses the
string.
Watch out
Expecting stop to be included. To get "Pyth" from "Python", students often write word[0:3], which gives only 'Pyt'. The stop index is excluded, so you need word[0:4]. Check with stop - start: 4 - 0 = 4 characters.
Pause and try
Question 5 · Tracing
Predict exactly what the REPL prints for each line.
>>> item = "notebook"
>>> item[0:4]
>>> item[4:]
>>> item[1:6:2]
>>> item[::-1]Check your reasoning
>>> item = "notebook"
>>> item[0:4]
'note'
>>> item[4:]
'book'
>>> item[1:6:2]
'oeo'
>>> item[::-1]
'koobeton'The indexes are n0 o1 t2 e3 b4 o5 o6 k7.
item[0:4]takes indexes 0 to 3:'note'.item[4:]takes 4 to the end:'book'.item[1:6:2]takes 1, 3, and 5 (6 is excluded): o, e, o, so'oeo'.item[::-1]is the whole word backwards:'koobeton'.
Reread 3.4 Slicing if any answer differed.
Question 6 · Multiple choice
code = "AB-2026-X". Which expression gives "2026"?
code[3:6]code[3:7]code[2:7]code[-6:-1]
Check your reasoning
B. The 2 is at index 3 and the last 6 at index 6, so the stop must be one past it, 7. Check: 7 - 3 = 4 characters.
- A is wrong: the stop is excluded, so it gives
'202'. - C is wrong: index 2 is the dash, so it gives
'-2026'. - D is wrong:
-6is the2but-1isX, and stopping before it still includes the second dash:'2026-'.
Reread 3.4 Slicing if you chose differently.
3.5 Strings Are Immutable
Suppose word holds "cat" and you want "bat". Indexing reads a character, so it seems you could assign to one:
word = "cat"
word[0] = "b"
print(word)Traceback (most recent call last):
File ".../day03/change_letter.py", line 2, in <module>
word[0] = "b"
~~~~^^^
TypeError: 'str' object does not support item assignment
Python refuses. A string is immutable: once made, its characters can never change. (“Item assignment” means assigning to one position.) You can still get "bat". Build a new string from the pieces you want, and point the name at it:
word = "cat"
word = "b" + word[1:]
print(word)bat
Line 2 makes a new string, "b" + "at", and rebinds word to it (rebinding is from Day 2). The old "cat" is not changed; the name simply refers to a different string now.
Why make strings unchangeable? Because a value that cannot change is safe to share. If two names refer to the same string, nothing done through one name can surprise the other. It also means every string operation you learn today, slices and the methods in 3.6, leaves the original alone and gives you a new string.
Key idea
Strings are immutable: you cannot change a character in place. To “change” a string, build a new one and assign it to the name.
Pause and try
Question 7 · Debugging
This program should print Hello world, with a capital H. It stops with an error. Quote the last line of the error, explain it, and write the fix.
title = "hello world"
title[0] = title[0].upper()
print(title)Check your reasoning
TypeError: 'str' object does not support item assignmenttitle[0].upper() on the right side is fine: it reads index 0 and returns the new string "H". The problem is the left side, which tries to put that character into position 0 of title, and strings are immutable. Build a new string from the capital letter and the rest of the text, and rebind title:
title = "hello world"
title = title[0].upper() + title[1:]
print(title)Hello worldtitle[1:] is "ello world", so line 2 makes "Hello world".
Reread 3.5 Strings Are Immutable if you missed it.
3.6 String Methods
Text that people type is messy: extra spaces, random capitals, the wrong punctuation. Cleaning it with slices alone would be slow. Python gives every string a set of ready-made tools for these jobs.
A method is a function that belongs to a value. You call it by writing the value, a dot, the method’s name, and parentheses: name.upper(). The value before the dot is what the method works on, and any arguments go inside the parentheses, as with print and len. Because strings are immutable (3.5), a method never changes the string: it returns (gives back) a new value.
| Method | Returns | Example | Result |
|---|---|---|---|
upper(), lower() |
A copy in capitals / small letters | "Hi".upper() |
'HI' |
strip() |
A copy without spaces, tabs, or new lines at the ends | " hi ".strip() |
'hi' |
replace(old, new) |
A copy with every old replaced by new |
"a-b-c".replace("-", "") |
'abc' |
find(part) |
The index where part first starts, or -1 |
"hello".find("l") |
2 |
startswith(part) |
True if the string begins with part |
"hello".startswith("he") |
True |
count(part) |
How many times part appears |
"banana".count("a") |
3 |
Spaces, tabs, and new lines are together called whitespace; strip removes it from both ends but not from the middle.
>>> name = " Ada Lovelace "
>>> name.strip()
'Ada Lovelace'
>>> name.upper()
' ADA LOVELACE '
>>> name.strip().lower()
'ada lovelace'
>>> name
' Ada Lovelace 'The last line shows name itself is unchanged, spaces and all. name.strip().lower() is a method chain: Python runs it left to right, so strip() returns 'Ada Lovelace' and lower() works on that new string.
The other four methods search and replace:
>>> sentence = "the cat sat on the mat"
>>> sentence.replace("cat", "dog")
'the dog sat on the mat'
>>> sentence.count("at")
3
>>> sentence.find("sat")
8
>>> sentence.find("dog")
-1
>>> sentence.startswith("the")
True
>>> sentence.startswith("cat")
Falsereplacechanged every"cat"(there is one).count("at")findsatin “cat”, “sat”, and “mat”: 3.find("sat")is 8: “the cat “ is 8 characters before it.findgives -1 when the part is missing, because -1 can never be a real starting position.startswithgives a bool, likein, but checks only the beginning.
Worked example: Cleaning an email address
raw_email = " Maya.Lopez@Example.com "
email = raw_email.strip().lower()
at_position = email.find("@")
print("Cleaned:", email)
print("Name part:", email[:at_position])
print("Domain:", email[at_position + 1:])
print("Dots:", email.count("."))Cleaned: maya.lopez@example.com
Name part: maya.lopez
Domain: example.com
Dots: 2| Line | Worked out | Value |
|---|---|---|
| 2 | strip(), then lower() |
"maya.lopez@example.com" |
| 3 | "maya.lopez" is 10 characters |
10 |
| 5 | email[:10] |
"maya.lopez" |
| 6 | email[11:] |
"example.com" |
find gives the @’s index, and the slices use it: everything before it, and everything after it (+ 1 skips the @ itself). The program works for any address, because it never uses a fixed number.
Watch out
Calling a method and throwing away the result. A method returns a new string; it does not change the old one:
name = "ada"
name.upper()
print(name)adaLine 2 makes 'ADA' and then discards it, because nothing stores it. Assign the result: name = name.upper().
Pause and try
Question 8 · Tracing
Predict exactly what the REPL prints for each line. Note the spaces at both ends of code.
>>> code = " ab-12-ab "
>>> code.strip().upper()
>>> code.count("ab")
>>> code.replace("-", "")
>>> code.find("12")Check your reasoning
>>> code = " ab-12-ab "
>>> code.strip().upper()
'AB-12-AB'
>>> code.count("ab")
2
>>> code.replace("-", "")
' ab12ab '
>>> code.find("12")
4strip()removes the end spaces, thenupper()makes'AB-12-AB'."ab"appears twice.replaceremoves both dashes but keeps the end spaces:' ab12ab '.- Counting from 0, the space is 0,
a1,b2,-3, so"12"starts at 4.
Reread 3.6 String Methods if any answer differed.
3.7 Splitting and Joining
A sentence is made of words, and a date of numbers separated by dashes. Often you want the separate pieces, or you want to glue pieces back together with something new between them. Two more methods do exactly that.
split() breaks a string into pieces. With no argument it splits wherever there is whitespace, and ignores extra spaces. With an argument, such as split("-"), it splits at each copy of that separator and drops the separators.
join() does the opposite: it glues pieces together, putting the string it is called on between each pair. Read "-".join(pieces) as “join the pieces with a dash”.
>>> "Ada Lovelace".split()
['Ada', 'Lovelace']
>>> " red green blue ".split()
['red', 'green', 'blue']
>>> "2026-10-09".split("-")
['2026', '10', '09']
>>> "-".join("abc")
'a-b-c'
>>> "/".join("2026-10-09".split("-"))
'2026/10/09'The result of split is shown in square brackets, like ['Ada', 'Lovelace']. That is a list: one value holding several values in order. Lists get a whole day of their own (Day 6). Today, just read the printed result as “these strings, in this order”.
join needs the pieces to glue. A string is a sequence of characters, so "-".join("abc") puts a dash between each character. The last line splits a date at its dashes and joins the pieces with slashes, turning 2026-10-09 into 2026/10/09. ("2026-10-09".replace("-", "/") gives the same here.)
Key idea
text.split() breaks a string into a list of pieces at whitespace, or at a separator you give.
separator.join(pieces) glues pieces into one string with the separator between them.
Watch out
Calling join on the pieces instead of the separator. Writing "abc".join("-") reads naturally but means “join the characters of "-" with abc between them”. With only one character there is nothing to put between, so you get just '-'. The separator always comes first: "-".join("abc").
Pause and try
Question 9 · Tracing
Predict exactly what the REPL prints for each line.
>>> "one,two,,three".split(",")
>>> "+".join("123")
>>> " ".join("to be or not".split())
>>> "".join("a b c".split())Check your reasoning
>>> "one,two,,three".split(",")
['one', 'two', '', 'three']
>>> "+".join("123")
'1+2+3'
>>> " ".join("to be or not".split())
'to be or not'
>>> "".join("a b c".split())
'abc'- Splitting at each comma gives four pieces. Between the two commas there is nothing, so one piece is the empty string
''. "+"goes between each character of"123":'1+2+3'.split()with no argument ignores the extra spaces, so the words are joined back with single spaces:'to be or not'.- Joining with the empty string
""puts nothing between the pieces:'abc'.
Reread 3.7 Splitting and Joining if any answer differed.
3.8 f-strings and Format Specifiers
Day 2 left a promise: round(2.50, 2) shows 2.5, and a price should show as 2.50. And building a line like "Total: " + str(total) with + and str() is clumsy. One tool fixes both.
An f-string is a string with the letter f just before its opening quote. Inside it, anything in curly braces { } is worked out as an expression, and its value is placed into the text. A value placed into a string this way is said to be interpolated. The expression can be a name or any calculation, and Python converts the value to text for you, so there is no need for str().
Worked example: Ticket sentences
name = "Ana"
tickets = 3
price = 12.5
print(f"{name} bought {tickets} tickets.")
print(f"Total: {tickets * price}")
print(f"Total: {tickets * price:.2f}")Ana bought 3 tickets.
Total: 37.5
Total: 37.50Line 4 places three names into one sentence: no +, no str(), and the spaces are exactly where they are in the text. Line 5 works out 3 * 12.5 inside the braces and shows 37.5. Line 6 is the same calculation with :.2f after it, which shows exactly two decimal places.
The :.2f is a format specifier: instructions, after a colon inside the braces, for how to show the value. Take .2f apart:
:separates the expression from the instructions..2means two digits after the decimal point.fmeans fixed-point: always show exactly that many decimal places, rounding or adding zeros as needed.
Change the 2 to get a different number of places: :.1f for one, :.3f for three.
>>> price = 2.5
>>> f"{price:.2f}"
'2.50'
>>> f"{3.14159:.3f}"
'3.142'
>>> f"{0.1 + 0.2:.2f}"
'0.30'
>>> f"{7:.2f}"
'7.00'
>>> round(2.5, 2)
2.5Each line gives a string with exactly the decimals asked for: 2.50, 3.142 (rounded), 0.30 (the floating-point error from Day 2 is hidden in the display), and 7.00 from a plain int. Compare round and :.2f side by side:
| Tool | Gives | 2.5 becomes |
Use it for |
|---|---|---|---|
round(value, 2) |
A number | 2.5 |
A rounded value to compute with |
f"{value:.2f}" |
A string | '2.50' |
Showing a value, such as money |
Key idea
In an f-string, f"...{expression}...", each expression in braces is worked out and placed into the text.
{value:.2f} shows a number with exactly two decimal places.
Watch out
Forgetting the f. Without it, the braces are ordinary characters:
name = "Ana"
print("Hello, {name}!")Hello, {name}!There is no error, so this mistake can slip through. If braces show up in your output, check for the f before the quote.
Pause and try
Question 10 · Tracing
Predict exactly what the REPL prints for each line.
>>> width = 4
>>> height = 2.5
>>> f"{width} x {height} = {width * height}"
>>> f"Area: {width * height:.1f} m"
>>> f"{1 / 3:.2f}"
>>> f"{2 / 3:.2f}"Check your reasoning
>>> width = 4
>>> height = 2.5
>>> f"{width} x {height} = {width * height}"
'4 x 2.5 = 10.0'
>>> f"Area: {width * height:.1f} m"
'Area: 10.0 m'
>>> f"{1 / 3:.2f}"
'0.33'
>>> f"{2 / 3:.2f}"
'0.67'- With no specifier, each value appears as
printwould show it.4 * 2.5is the float10.0. :.1fgives one decimal place:10.0.- 1/3 is 0.333…, shown to two places as
0.33. - 2/3 is 0.666…, which rounds to
0.67;:.2frounds, it does not just cut off.
Reread 3.8 f-strings and Format Specifiers if any answer differed.
3.9 Reading Input
Every program so far has used the same values each time it ran. To ask the user something, such as their name or age, a program needs to read what they type.
The built-in function input() shows a message, called a prompt, then waits. The user types a line and presses Enter, and input() returns what they typed, without the Enter, as a string. Put a space at the end of the prompt so the typing does not touch it.
name = input("What is your name? ")
print(f"Nice to meet you, {name}!")When you run it and type Ana, the terminal shows:
What is your name? Ana
Nice to meet you, Ana!
Line 1 prints the prompt and pauses until Enter is pressed. Then name refers to "Ana", and line 2 places it into an f-string.
Now vary the program to ask for two numbers and add them. Watch what happens when the user types 5 and 3:
first = input("First number: ")
second = input("Second number: ")
print("Sum:", first + second)First number: 5
Second number: 3
Sum: 53
The sum is 53, with no error message. input() always returns a string, whatever the user types. first is "5" and second is "3", so + joins them (3.2). input cannot know you wanted a number: someone might type a name, a phone number with a leading 0, or “maybe”, so Python leaves the converting to you.
Convert with int() or float() from Day 2. You can wrap the call around input() directly:
first = int(input("First number: "))
second = int(input("Second number: "))
print("Sum:", first + second)First number: 5
Second number: 3
Sum: 8
Python works out int(input("First number: ")) from the inside out: input runs first and returns "5", then int("5") gives the int 5. Use float(input(...)) for decimals such as prices. If the user types something that is not a number, such as five, the conversion stops with the ValueError from Day 2; Day 15 shows how to handle bad input.
Key idea
input(prompt) shows the prompt, waits for a line, and always returns it as a string. Wrap it in int() or float()
when you need a number: age = int(input("Age: ")).
Watch out
Doing math with the raw input. Day 2 promised this one. Typing 15 gives the string "15":
age = input("How old are you? ")
print("Next year you will be", age + 1)How old are you? 15
Traceback (most recent call last):
File ".../day03/age_next_year.py", line 2, in <module>
print("Next year you will be", age + 1)
~~~~^~~
TypeError: can only concatenate str (not "int") to strage + 1 tries to join the string "15" and the int 1. Convert as you read: age = int(input("How old are you? ")), and the program prints Next year you will be 16.
Pause and try
Question 11 · Tracing
The user types Oslo, then 3. What does the whole terminal session show?
city = input("City: ")
nights = input("Nights: ")
print(city.upper(), nights * 2)
print(f"{city[:3]}-{int(nights) * 2}")Check your reasoning
City: Oslo
Nights: 3
OSLO 33
Osl-6cityis"Oslo"andnightsis the string"3".- Line 3:
city.upper()isOSLO, andnights * 2repeats the string:"33", not 6. - Line 4:
city[:3]isOsl, andint(nights) * 2converts first, so it is the number 6.
Reread 3.9 Reading Input if your output differed.
Putting it together
A café wants a small program that prints an order slip. The user types a name (with stray spaces), a drink, the number of cups, and the price per cup. This run types Sam, Latte, 2, and 3.75:
# Coffee order receipt
name = input("Name: ").strip()
drink = input("Drink: ").strip().lower()
cups = int(input("Cups: "))
price = float(input("Price per cup: "))
total = cups * price
order_code = name[:3].upper() + str(cups)
print("-" * 24)
print(f"Order {order_code} for {name}")
print(f"{cups} x {drink}")
print(f"Total: {total:.2f}")
print("-" * 24)Name: Sam
Drink: Latte
Cups: 2
Price per cup: 3.75
------------------------
Order SAM2 for Sam
2 x latte
Total: 7.50
------------------------
| Line | Worked out | Value | Idea |
|---|---|---|---|
| 2 | " Sam".strip() |
"Sam" |
3.6, 3.9 |
| 3 | "Latte".strip().lower() |
"latte" |
3.6 |
| 4 | int("2") |
2 |
3.9 |
| 5 | float("3.75") |
3.75 |
3.9 |
| 7 | 2 * 3.75 |
7.5 |
Day 2 |
| 8 | "Sam"[:3].upper() + str(2) |
"SAM2" |
3.4, 3.2 |
| 10 | "-" * 24 |
24 dashes | 3.2 |
| 13 | f"Total: {7.5:.2f}" |
"Total: 7.50" |
3.8 |
Lines 2 and 3 call methods straight on what input returns, so the stray spaces never reach a variable. Line 8 slices the name, makes it capitals, and joins the cup count as a string: + needs both sides to be strings, so str(cups) is required. Lines 11 to 13 build the slip with f-strings, and :.2f shows 7.5 as 7.50.
Vocabulary
| Term | Definition | Example |
|---|---|---|
string (str) |
A sequence of characters: text | "Hello" |
| character | One letter, digit, space, or symbol in a string | "H" |
| escape character | A backslash and a symbol that stand for one special character | \n, \" |
| triple-quoted string | A string in """ that can span several lines |
poem = """Roses…""" |
| length | How many characters a string has; len() gives it |
len("cat") is 3 |
| empty string | The string with no characters | "" |
| concatenation | Joining strings end to end with + |
"snow" + "ball" |
| repetition | Repeating a string with * and an int |
"ha" * 3 |
| substring | A run of characters inside a string | "York" in "New York" |
| case-sensitive | Treating capital and small letters as different | "a" == "A" is False |
| index / indexing | A character’s position, from 0 / getting the character at one | word[0] |
| negative index | A position counted from the end; -1 is the last character | word[-1] |
IndexError |
Asking for a position that does not exist | "cat"[3] |
| slice / step | Characters from start up to but not including stop / the gap between them |
word[1:4:2] |
| immutable | Cannot be changed once made | word[0] = "b" fails |
| method / method chain | A function that belongs to a value / calls run left to right | name.strip().lower() |
| whitespace | Spaces, tabs, and new lines | " hi\n" |
| separator | The string that split cuts at or join puts between pieces |
"-" |
| list | One value holding several in order, in square brackets (Day 6) | ['Ada', 'Lovelace'] |
| f-string / interpolated | A string starting with f / a value placed into it from braces |
f"Hi, {name}" |
| format specifier | Instructions after : in braces for how to show a value |
{price:.2f} |
| prompt | The message input() shows before the user types |
input("Age: ") |
Summary
- A string is text in single, double, or triple quotes. Escape characters such as
\n,\", and\\stand for characters you cannot type directly. len()counts characters,+joins strings,*repeats them, andinand==giveTrueorFalse.- Indexes start at 0, and negative indexes count from the end. A slice
text[start:stop:step]stops beforestop, andtext[::-1]reverses. - Strings are immutable, so slices and methods (
upper,lower,strip,replace,find,startswith,count,split,join) return new values and leave the original alone. - An f-string places values into text, and
:.2fshows exactly two decimal places. input()always returns a string; convert withint()orfloat()before doing math.
Practice
Do these in your day03 folder. Type each program yourself, run it, and only then open the answer. Run each one several times with different typed input.
Question 12 · Short answer
Initials. Ask the user for their full name (a first and a last name separated by a space) and print their initials in capitals, followed by dots, such as A.L. for ada lovelace. Hint: find can tell you where the space is.
Check your reasoning
One possible solution:
full_name = input("Full name: ").strip()
space = full_name.find(" ")
first_initial = full_name[0].upper()
last_initial = full_name[space + 1].upper()
print(f"Initials: {first_initial}.{last_initial}.")Full name: ada lovelace
Initials: A.L.strip() removes stray spaces at the ends, so index 0 is really the first letter. find(" ") gives the space’s index, 3, so the last name starts at space + 1, index 4. upper() capitalizes each initial even if the user typed in small letters. The f-string adds the dots.
Reread 3.3 Indexing: One Character at a Time and 3.6 String Methods if your result differed.
Question 13 · Short answer
Reverse and palindrome. A palindrome reads the same forwards and backwards, like “level”. Ask for a word, print it reversed using slicing, then print whether it is a palindrome as True or False. Make Racecar count as a palindrome even with its capital R.
Check your reasoning
One possible solution:
word = input("Word: ").strip().lower()
reversed_word = word[::-1]
print(f"Reversed: {reversed_word}")
print("Palindrome:", word == reversed_word)Word: Racecar
Reversed: racecar
Palindrome: Truelower() makes the capital R match the small r at the end; without it, "Racecar" == "racecaR" is False. word[::-1] reverses the word, and word == reversed_word gives a bool, which print shows as True or False. Typing Hopper prints reppoh and False.
Reread 3.4 Slicing and 3.2 Length, Concatenation, and in if your result differed.
Question 14 · Short answer
Receipt line. Ask for a price and a quantity, then print one receipt line showing the quantity, the price, and the total, with both money amounts to two decimal places, such as 3 x 4.50 = 13.50.
Check your reasoning
One possible solution:
price = float(input("Price: "))
quantity = int(input("Quantity: "))
total = price * quantity
print(f"{quantity} x {price:.2f} = {total:.2f}")Price: 4.5
Quantity: 3
3 x 4.50 = 13.50The price can have a decimal point, so it uses float(input(...)); the quantity is a whole number, so it uses int(input(...)). 4.5 * 3 is 13.5, and :.2f shows both money values with two places. Without the conversions, "4.5" * "3" would be a TypeError.
Reread 3.8 f-strings and Format Specifiers and 3.9 Reading Input if your result differed.
Review questions
Question 15 · Tracing
What does this program print?
title = " the hobbit "
clean = title.strip()
print(clean.upper())
print(clean[4:].replace("b", "B"))
print(len(clean), clean.count("t"))Check your reasoning
THE HOBBIT
hoBBit
10 2| Line | Worked out | Value |
|---|---|---|
| 2 | strip() removes the end spaces |
"the hobbit" |
| 3 | upper() of clean |
THE HOBBIT |
| 4 | clean[4:] is "hobbit", then every b becomes B |
hoBBit |
| 5 | 10 characters (the space counts); t in “the” and “hobbit” |
10 2 |
clean[4:] starts at index 4 because “the “ takes indexes 0 to 3. replace changes every b, not just the first.
Reread 3.4 Slicing and 3.6 String Methods if your output differed.
Question 16 · Multiple choice
Which expression gives the string '2.50'?
round(2.5, 2)f"{2.5:.2f}"f"{2.5}:.2f""{2.5:.2f}"
Check your reasoning
B. An f-string with the format specifier :.2f inside the braces shows exactly two decimal places.
- A is wrong:
roundgives the number2.5; a float never shows a trailing zero. - C is wrong: the
:.2fis outside the braces, so it is plain text:'2.5:.2f'. - D is wrong: without the
f, the braces are ordinary characters:'{2.5:.2f}'.
Reread 3.8 f-strings and Format Specifiers if you chose differently.
Question 17 · Tracing
What does this program print? Track word and new_word.
word = "stone"
word.upper()
new_word = word.replace("s", "")
word = word + "s"
print(word, new_word)
print(word.find("t"), new_word.find("s"))Check your reasoning
stones tone
1 -1| Line | Worked out | word |
new_word |
|---|---|---|---|
| 1 | "stone" |
"stone" |
|
| 2 | "STONE", thrown away |
"stone" |
|
| 3 | remove every s |
"stone" |
"tone" |
| 4 | "stone" + "s" |
"stones" |
"tone" |
Line 2 changes nothing: the result is never stored. In line 6, t is at index 1 of "stones", and "tone" has no s, so find gives -1. If you printed STONES, reread the Watch Out in 3.6.
Reread 3.5 Strings Are Immutable and 3.6 String Methods if your output differed.
Question 18 · Debugging
This program should print the cost of the tickets at 12.50 each. The user types 3, and it stops with the error below. Explain it and write the fix.
tickets = input("Tickets: ")
total = tickets * 12.5
print(f"Total: {total:.2f}")Tickets: 3
Traceback (most recent call last):
File ".../day03/ticket_cost.py", line 2, in <module>
total = tickets * 12.5
~~~~~~~~^~~~~~
TypeError: can't multiply sequence by non-int of type 'float'Check your reasoning
tickets is the string "3", because input always returns a string. A string times an int is repetition (3.2), but a string times a float has no meaning, so Python stops. (“Sequence” here means the string.) Convert the input:
tickets = int(input("Tickets: "))
total = tickets * 12.5
print(f"Total: {total:.2f}")Tickets: 3
Total: 37.50Worse than the error is the near miss: with a price of 12 instead of 12.5, "3" * 12 would quietly give "333333333333". Converting input right away avoids both.
Reread 3.9 Reading Input if you missed it.
Question 19 · Short answer
A card number is stored as the string "4111222233334444". Write a program that prints it with every digit except the last four replaced by *, and that still works if the number has a different length.
Check your reasoning
One possible solution:
card = "4111222233334444"
masked = "*" * (len(card) - 4) + card[-4:]
print(masked)************4444card[-4:] is the last four characters, from 3.4. The number of stars is the length minus 4, worked out with len so that any length works, and * repeats the star that many times. A fixed "*" * 12 would be wrong for a 15-digit card.
Reread 3.2 Length, Concatenation, and in and 3.4 Slicing if your result differed.
Go further
The official Python Tutorial covers strings, indexing, and slicing in An Informal Introduction to Python: Text, and f-strings in Input and Output: Formatted String Literals.
Reflection
Check each statement. If the answer is “not yet,” reread the linked subsection.
- I can write strings with quotes inside them and use escape characters. Reread
- I can use len, +, *, in, and == with strings. Reread
- I can get any character with a positive or negative index. Reread
- I can slice any part of a string, use a step, and reverse a string. Reread
- I can explain why a string cannot be changed in place, and build a new one instead. Reread
- I can clean and search text with string methods. Reread
- I can split a string into pieces and join pieces back together. Reread
- I can build formatted output with f-strings and :.2f. Reread
- I can read input and convert it to a number. Reread