My shop’s total says 0.30000000000000004
New to Python? Open the quick recap. It takes about two minutes.
Quick recapNames and print() in two minutes· 2 min
A name holds a value: price = 2.5 makes the name price for the number 2.5. Text goes in quotes, and print()
shows values with a space between them.
price = 2.5name = "Tea"print(name, price) # Tea 2.5See the problem
Section titled “See the problem”Your shop’s checkout adds up the prices in an order. The price file stores them as text, like "0.10":
prices = ["0.10", "0.20"]total = sum(float(p) for p in prices)print("Total:", total) # Total: 0.30000000000000004You show it with two decimals, f"{total:.2f}", and it says 0.30. Fixed?
Then a customer with a 10% coupon buys a 0.70 drink and a 0.45 snack. That’s 1.15, less 10%: exactly 1.035. Your shop rounds half a cent up, so the right charge is 1.04. The checkout charges 1.03. print(total) even shows 1.035. So why does it round down?
Define the task
Section titled “Define the task”Write order_total(prices, percent_off).
- Input:
prices, the prices in the order as text with two decimals, like"0.70". There’s at least one. Andpercent_off, a whole number from 0 to 100. - Output: the total, less
percent_offpercent, rounded to the cent, as text with two decimals, like"1.04". - The hard part: half a cent rounds up. 1.035 is
"1.04", and 1.125 is"1.13".
Example: order_total(["0.70", "0.45"], 10) gives "1.04".
Pause and think
Section titled “Pause and think”Hints open one at a time:
Hint 1 of 3
A float keeps numbers in binary. Most decimals, like 0.1 or 1.15, have no exact binary form, so Python keeps the nearest one.
Hint 2 of 3
Decimal("1.15") from the decimal module keeps decimal digits exactly. Make it from the text, not from a float.
Hint 3 of 3
round() sends a half to the even neighbour: round(Decimal("1.125"), 2) is 1.12. quantize() lets you choose ROUND_HALF_UP.
Understand the idea
Section titled “Understand the idea”Whole numbers are exact, at any size. An int has no top: 2 ** 100 is 1267650600228229401496703205376. 7 // 2 is 3 (divide and round down), and 7 % 2 is 1 (what’s left). 7 / 2 is 3.5: / always gives a float, even 4 / 2, which is 2.0. And // rounds down, toward minus infinity, so -7 // 2 is -4, not -3.
A float is the nearest binary fraction. In decimal, 1/3 never ends: 0.333… In binary, 1/10 never ends either. A float keeps about 17 significant digits, so Python stores the float nearest to 0.1:
from decimal import Decimal
print(Decimal(0.1)) # 0.1000000000000000055511151231257827021181583404541015625print(0.1 + 0.2) # 0.30000000000000004print(0.1 + 0.2 == 0.3) # Falseprint(0.1) shows 0.1, because Python prints the shortest text that gives back the same float. The tiny error is still there.
That’s the coupon bug. 1.15 is stored as 1.14999999999999991…, so 1.15 less 10% comes out a hair under 1.035. print() shows 1.035, the shortest text for that float. But :.2f sees a number under the half, and rounds it down to 1.03.
Decimal keeps decimal digits. Decimal("1.15") is exactly 1.15, and Decimal("1.15") * 90 / 100 is exactly 1.035. Make it from the text: Decimal(1.15) starts from the float, error and all.
round() sends a half to the even neighbour. round(2.5) is 2, and round(3.5) is 4. round(Decimal("1.125"), 2) is 1.12. This is called banker’s rounding: over many sums, the halves that go up and the halves that go down even out. When a rule says half up, say so:
from decimal import ROUND_HALF_UP, Decimal
Decimal("1.125").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) # Decimal('1.13')A string is a row of characters that can’t change. With s = "Python", s[0] is "P", and s[-1] is "n": a negative place counts from the back. s[0] = "J" raises TypeError: 'str' object does not support item assignment. Methods like upper(), strip() and replace() give back a new string and leave s as it was, so keep what they give back: name = name.strip().
A slice cuts out a piece: s[start:stop:step]. It starts at start, moves step places at a time, and stops before stop. Leave a part out and it means the end. Watch a slice take its places, then try your own text and slice, like -3: or ::-1.
- P0-6
- y1-5
- t2-4
- h3-3
- o4-2
- n5-1
Step 1 of 7. s holds 6 characters. Each has a place counted from the front, 0 to 5, and one counted from the back, -6 to -1, shown under it.
s = "Python" print(s[ 1:4])
An f-string puts values into text. f"{name} costs {price}" puts in the values of name and price. After a colon, say how to show a value:
| You write | You get | Which means |
|---|---|---|
f"{2.5:.2f}" | 2.50 | 2 decimals |
f"{2.5:8.2f}" | 2.50 | at least 8 wide, on the right |
f"{'Tea':<8}" | Tea | at least 8 wide, on the left |
f"{1234567:,}" | 1,234,567 | commas between thousands |
f"{0.256:.1%}" | 25.6% | a percent, 1 decimal |
f"{7:03d}" | 007 | 3 wide, with zeros in front |
f"{price=}" | price=2.5 | the name and the value, useful for debugging |
Climb the ladder
Section titled “Climb the ladder”All three give "0.30" for order_total(["0.10", "0.20"], 0). What changes is what they do with a half cent.
Floats, shown with two decimals
The idea: Add the prices as floats, take the percent off, and show it with :.2f.
It fixes the long tail: 0.1 + 0.2 shows as 0.30.
def order_total(prices: list[str], percent_off: int) -> str: total = sum(float(p) for p in prices) * (100 - percent_off) / 100 return f"{total:.2f}"- Time
O(n)- Space
O(1)- Why
- One pass over the n prices.
Where it breaks: 0.70 and 0.45 less 10% is 1.035, which should be "1.04". The float is a hair under 1.035, so it gives "1.03". A customer is charged the wrong amount, and print() hides why.
Decimal from the text, then round()
The idea: Make each price a Decimal from its text, so 1.035 is exact. Then round(total, 2).
The waste in Okay: it rounds a number that’s already a little wrong.
Decimal("0.70") + Decimal("0.45") is exactly 1.15, and 1.15 less 10% is exactly 1.035. round() then gives 1.04.
Highlighted lines are new in this rung.
from decimal import Decimal
def order_total(prices: list[str], percent_off: int) -> str: total = sum(Decimal(p) for p in prices) * (100 - percent_off) / 100 return str(round(total, 2))- Time
O(n)- Space
O(1)- Why
- One pass over the n prices.
What changed from the previous rung
from decimal import Decimal
def order_total(prices: list[str], percent_off: int) -> str: total = sum(float(p) for p in prices) * (100 - percent_off) / 100 return f"{total:.2f}" total = sum(Decimal(p) for p in prices) * (100 - percent_off) / 100 return str(round(total, 2))Where it breaks: round() sends a half to the even neighbour. 1.25 less 10% is exactly 1.125, and round() gives "1.12", where the shop’s rule says "1.13". Half a cent off a 1-cent item gives "0.00".
Decimal, rounded half up
The idea: The same exact total, then quantize(Decimal('0.01'), rounding=ROUND_HALF_UP).
The waste in Better: it works out the exact total, then rounds it by someone else’s rule.
quantize(CENT) rounds to two decimals, and rounding=ROUND_HALF_UP says what to do with a half. The result always has two decimals, even 0.00.
Highlighted lines are new in this rung.
from decimal import ROUND_HALF_UP, Decimal
CENT = Decimal("0.01")
def order_total(prices: list[str], percent_off: int) -> str: total = sum(Decimal(p) for p in prices) * (100 - percent_off) / 100 return str(total.quantize(CENT, rounding=ROUND_HALF_UP))- Time
O(n)- Space
O(1)- Why
- One pass over the n prices.
What changed from the previous rung
from decimal import Decimalfrom decimal import ROUND_HALF_UP, Decimal
CENT = Decimal("0.01")
def order_total(prices: list[str], percent_off: int) -> str: total = sum(Decimal(p) for p in prices) * (100 - percent_off) / 100 return str(round(total, 2)) return str(total.quantize(CENT, rounding=ROUND_HALF_UP))When Best isn’t best: many shops keep money as whole cents in an int, which is exact and fast: 1.15 is 115. And when the law or a bank sets the rule, check it first. Some say half to even, and some round each line, not the total.
Write it yourself
Section titled “Write it yourself”Now write it in the page. Press Run tests to check your code. Python runs in your browser, so nothing is sent anywhere. Your code is saved in this browser for next time.
Try Okay from the ladder first: it gets three of the half cents wrong.
Total an order to the cent
Ctrl + Enter runs- Not run yet:
order_total(['0.10', '0.20'], 0)→'0.30' - Not run yet:
order_total(['19.99', '5.00'], 0)→'24.99' - Not run yet:
order_total(['0.70', '0.45'], 10)→'1.04' - Not run yet:
order_total(['2.55'], 10)→'2.30' - Not run yet:
order_total(['0.50', '0.75'], 10)→'1.13' - Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Compare
Section titled “Compare”| Way | Time | Extra memory | Good | Not so good | Use it when |
|---|---|---|---|---|---|
| Okay: floats, shown with two decimals | O(n) | O(1) | Short, and fine for show | Rounds some half cents the wrong way | Measurements, not money |
Better: Decimal, then round() | O(n) | O(1) | Exact total | Half to even, not half up | Your rule is half to even |
| Best: Decimal, rounded half up | O(n) | O(1) | Exact, and rounds by your rule | A few more names to import | Money, almost always |
Common mistakes and edge cases
Section titled “Common mistakes and edge cases”round(2.675, 2)is 2.67. The float is a hair under 2.675, so evenround()can’t help it.- Text and numbers don’t mix on their own.
"Total: " + 3raisesTypeError: can only concatenate str (not "int") to str. Usestr(3), or an f-string. And"ab" * 3is"ababab", not an error. int("19.99")raisesValueError.int()takes whole numbers only. UseDecimal("19.99"), orfloat()when a float is fine.- Text compares character by character.
sorted(["9.99", "10.00"])gives["10.00", "9.99"], since"1"comes before"9". Sort prices withkey=Decimal. - A width in an f-string is the least, not the most.
f"{'Earl Grey':<5}"is all 9 characters. Cut it first with a slice:f"{name[:5]:<5}". int()refuses very long text.int("1" * 5000)raisesValueError: Exceeds the limit (4300 digits) for integer string conversion: value has 5000 digits; use sys.set_int_max_str_digits() to increase the limit. That limit keeps a huge number from tying up a server.- Building a long string by adding to the front is slow.
result = digit + resultcopies all ofresulteach time. Collect the pieces in a list, then"".join(pieces).
Say it in the interview
Section titled “Say it in the interview”Questions to ask first
- How should a half cent round: half up, or half to even? Per line, or on the total?
- Do the prices come in as text, floats or whole cents?
- How big can the numbers get, and how many items in an order?
A 60-second walkthrough
“Floats are binary fractions, so 0.70 and 0.45 aren’t stored exactly, and 1.15 less 10% comes out a hair under 1.035. Formatting it with two decimals then rounds down. I’d make each price a Decimal from its text, so the total is exact: 1.035. Then I’d round with quantize to two places and ROUND_HALF_UP, since round() sends halves to the even neighbour. It’s one pass, O(n), and the answer always has two decimals.”
Likely follow-ups
- Why not just
round(total, 2)on the float? The float is already a hair under the half, so it rounds the wrong way too. - Why not whole cents? That works well: add
intcents, and do the percent with//and the rounding rule by hand. Decimal says the rule in one word. - What does
0.1 + 0.2 == 0.3give, and how do you compare floats? False. Usemath.isclose(). - How do you reverse a string?
s[::-1]: a slice with a step of -1.
Practice
Section titled “Practice”Four problems, from easy to hard. Each has hints, tests you can run here, and its own Okay, Better and Best answers. Try before you open them.
1. Same backwards (easy)
Section titled “1. Same backwards (easy)”Write same_backwards(text): True if text reads the same backwards, counting only letters and digits, and ignoring upper and lower case. So "Step on no pets!" is True. The text can be long, so use as little extra memory as you can.
Hint 1 of 3
text == text[::-1] checks every character, spaces, capitals and ! included.
Hint 2 of 3
Keep only the letters and digits, in lower case, then compare with the reverse. But that builds a copy of the text, and its reverse.
Hint 3 of 3
Compare from both ends at once: i from the front and j from the back. Skip what isn’t a letter or digit, and stop when they meet.
Same backwards
Ctrl + Enter runs- Not run yet:
same_backwards('racecar')→True - Not run yet:
same_backwards('Never odd or even')→True - Not run yet:
same_backwards('Step on no pets!')→True - Not run yet:
same_backwards('Python')→False - Not run yet: Extra test
- Not run yet:
A text of 200,000 characters, in 1 MB→True
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Compare with the reverse
The idea: text == text[::-1]
One line, and right for a single word in lower case.
def same_backwards(text: str) -> bool: return text == text[::-1]- Time
O(n)- Space
O(n)- Why
- The reverse is a copy of all n characters.
Where it breaks: it counts the spaces, the capitals and the !. "Never odd or even" backwards is "neve ro ddo reveN".
Clean it, then compare
The idea: Keep the letters and digits in lower case, then compare that with its reverse.
The waste in Okay: it compares characters the problem says to skip.
c.isalnum() keeps letters and digits, and c.lower() makes "N" and "n" the same.
Highlighted lines are new in this rung.
def same_backwards(text: str) -> bool: kept = "".join(c.lower() for c in text if c.isalnum()) return kept == kept[::-1]- Time
O(n)- Space
O(n)- Why
- One pass to clean, one to reverse; the clean copy, its reverse, and the list join builds first.
What changed from the previous rung
def same_backwards(text: str) -> bool: return text == text[::-1] kept = "".join(c.lower() for c in text if c.isalnum()) return kept == kept[::-1]Where it breaks: "".join() first puts all the characters in a list, then builds the copy, then the reverse. For 200,000 characters it held about 5 MB, when the answer needs only two places in the text.
Two places, moving in
The idea: i from the front, j from the back. Skip what isn’t a letter or digit; if the two differ, it’s False.
The waste in Better: it copies the whole text twice, to read it once.
The text is already there to read from both ends. The loop stops when i and j meet in the middle.
Highlighted lines are new in this rung.
def same_backwards(text: str) -> bool: i, j = 0, len(text) - 1 while i < j: if not text[i].isalnum(): i += 1 elif not text[j].isalnum(): j -= 1 elif text[i].lower() != text[j].lower(): return False else: i += 1 j -= 1 return True- Time
O(n)- Space
O(1)- Why
- Each character is looked at once, by i or by j; only two places are kept.
What changed from the previous rung
def same_backwards(text: str) -> bool: kept = "".join(c.lower() for c in text if c.isalnum()) return kept == kept[::-1] i, j = 0, len(text) - 1 while i < j: if not text[i].isalnum(): i += 1 elif not text[j].isalnum(): j -= 1 elif text[i].lower() != text[j].lower(): return False else: i += 1 j -= 1 return TrueWhen Best isn’t best: for short text, Better is clearer, and often faster in Python, since join() and the slice run in C. Pick Best when the text is big, or memory is tight.
2. Reverse the words (easy)
Section titled “2. Reverse the words (easy)”Write reverse_words(sentence): the words of sentence in reverse order, with one space between them and none at the ends. In sentence, words are split by spaces or tabs, maybe several in a row, and there may be some at the ends.
Example: reverse_words(" hello world ") gives "world hello".
Hint 1 of 3
split(" ") splits at every single space, so two spaces in a row leave an empty word between them.
Hint 2 of 3
split() with nothing in the brackets splits at any run of spaces or tabs, and drops the ends. Then put the words together, last first.
Hint 3 of 3
Adding each word to the front of the answer copies the answer every time. Reverse the list of words, then join them once.
Reverse the words
Ctrl + Enter runs- Not run yet:
reverse_words('the sky is blue')→'blue is sky the' - Not run yet:
reverse_words(' hello world ')→'world hello' - Not run yet:
reverse_words('a good example')→'example good a' - Not run yet:
reverse_words('one')→'one' - Not run yet: Extra test
- Not run yet:
Speed test: 300,000 words
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Split at each space, reverse, join
The idea: " ".join(sentence.split(" ")[::-1])
One line, and right when each word has exactly one space after it.
def reverse_words(sentence: str) -> str: return " ".join(sentence.split(" ")[::-1])- Time
O(n)- Space
O(n)- Why
- One pass to split, one to reverse the list, one to join.
Where it breaks: " hello world ".split(" ") is ["", "", "hello", "world", "", ""]. The empty words keep the extra spaces, so the answer is " world hello ". A tab isn’t split at all.
split(), then each word in front
The idea: split() with no argument, then add each word to the front of the answer.
The waste in Okay: it treats each single space as a split, so runs of spaces make empty words.
split() splits at any run of spaces or tabs and ignores them at the ends, so every answer is right.
Highlighted lines are new in this rung.
def reverse_words(sentence: str) -> str: result = "" for word in sentence.split(): result = word + " " + result # put each word in front return result.strip()- Time
O(n²)- Space
O(n)- Why
- A string can’t change, so each word + answer copies the whole answer so far.
What changed from the previous rung
def reverse_words(sentence: str) -> str: return " ".join(sentence.split(" ")[::-1]) result = "" for word in sentence.split(): result = word + " " + result # put each word in front return result.strip()Where it breaks: word + " " + result builds a new string with everything so far in it. For 300,000 words, that’s copying billions of characters: too slow.
split(), reversed(), join() once
The idea: " ".join(reversed(sentence.split()))
The waste in Better: it copies the answer again for every word.
reversed() walks the list from the back without copying it, and join() builds the answer in one go.
Highlighted lines are new in this rung.
def reverse_words(sentence: str) -> str: return " ".join(reversed(sentence.split()))- Time
O(n)- Space
O(n)- Why
- join() works out the size, then copies each word once.
What changed from the previous rung
def reverse_words(sentence: str) -> str: result = "" for word in sentence.split(): result = word + " " + result # put each word in front return result.strip() return " ".join(reversed(sentence.split()))When Best isn’t best: in a language whose strings can change, like C, interviewers often want the words reversed in place: reverse the whole text, then each word. In Python, a string can’t change, so this is the way.
3. The most common words (medium)
Section titled “3. The most common words (medium)”Write top_words(text, k): the k most common words in text, most common first. A word is a run of the letters a to z, in any case: "Blue." and "blue" are the same word. When two words are equally common, the one first in alphabetical order comes first. If there are fewer than k different words, give them all.
Example: top_words("the cat and the hat", 1) gives ["the"].
Hint 1 of 3
split() keeps the punctuation: "blue." and "blue," come out as two different words.
Hint 2 of 3
A regular expression can pick out just the runs of letters: look at re.findall. Then collections.Counter counts them all in one pass.
Hint 3 of 3
Counter.most_common() keeps equal counts in the order the words first came. Sort with the key (-count, word) instead.
The most common words
Ctrl + Enter runs- Not run yet:
top_words('the cat and the hat', 1)→['the'] - Not run yet:
top_words('Blue. blue, BLUE! red', 2)→['blue', 'red'] - Not run yet:
top_words('b a b a c', 2)→['a', 'b'] - Not run yet:
top_words('one', 5)→['one'] - Not run yet: Extra test
- Not run yet:
Speed test: 200,120 words, 20,003 of them different→['zebra', 'yak', 'xenon']
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Count each word with count()
The idea: Split the lower-case text, then sort the different words by (-words.count(w), w).
It sorts ties the right way, and it’s short.
def top_words(text: str, k: int) -> list[str]: words = text.lower().split() return sorted(set(words), key=lambda w: (-words.count(w), w))[:k]- Time
O(n · u)- Space
O(n)- Why
- count() looks through all n words, once for each of the u different words.
Where it breaks: "blue.", "blue," and "blue!" are three different words to split(). And count() reads the whole list again for each different word: with about 200,000 words and 20,003 different ones, that’s 4 billion looks, and it runs out of time.
Find the words, and count them once
The idea: Find the runs of letters with re.findall, count them with a Counter, then most_common(k).
The waste in Okay: it counts the same list again and again.
re.findall(r"[a-z]+", ...) keeps only runs of letters, so the punctuation is gone. Counter counts every word in one pass, with a dict.
Highlighted lines are new in this rung.
import refrom collections import Counter
def top_words(text: str, k: int) -> list[str]: counts = Counter(re.findall(r"[a-z]+", text.lower())) return [word for word, _ in counts.most_common(k)]- Time
O(n + u log u)- Space
O(n)- Why
- One pass to find and count the words; most_common() sorts the u counts.
What changed from the previous rung
import refrom collections import Counter
def top_words(text: str, k: int) -> list[str]: words = text.lower().split() return sorted(set(words), key=lambda w: (-words.count(w), w))[:k] counts = Counter(re.findall(r"[a-z]+", text.lower())) return [word for word, _ in counts.most_common(k)]Where it breaks: most_common() keeps equal counts in the order they first came. "b a b a c" gives ["b", "a"], not ["a", "b"].
Count once, sort by count, then word
The idea: The same Counter, then sorted(counts, key=lambda w: (-counts[w], w))[:k].
The waste in Better: it lets most_common() choose the order of the ties.
The key (-count, word) sorts by count, biggest first, and then by the word itself, a to z.
Highlighted lines are new in this rung.
import refrom collections import Counter
def top_words(text: str, k: int) -> list[str]: counts = Counter(re.findall(r"[a-z]+", text.lower())) return sorted(counts, key=lambda w: (-counts[w], w))[:k]- Time
O(n + u log u)- Space
O(n)- Why
- One pass to count; then a sort of the u different words.
What changed from the previous rung
import refrom collections import Counter
def top_words(text: str, k: int) -> list[str]: counts = Counter(re.findall(r"[a-z]+", text.lower())) return [word for word, _ in counts.most_common(k)] return sorted(counts, key=lambda w: (-counts[w], w))[:k]When Best isn’t best: with millions of different words and a small k, heapq.nsmallest(k, counts, key=...) skips sorting them all: O(u log k).
4. Add two numbers written as text (hard)
Section titled “4. Add two numbers written as text (hard)”Write add_as_text(a, b): a and b are whole numbers, 0 or more, written as text, up to a million digits each. Give back their sum, as text. Neither has extra zeros in front.
Example: add_as_text("999", "1") gives "1000".
Hint 1 of 3
int(a) + int(b) is the obvious way. Try it on a number with 5,000 digits.
Hint 2 of 3
Add digit by digit, from the right, the way you learned at school: each column gives a digit and maybe a carry of 1.
Hint 3 of 3
result = digit + result copies the whole result for every digit. Append the digits to a list, then join them once, reversed.
Add two numbers written as text
Ctrl + Enter runs- Not run yet:
add_as_text('19', '5')→'24' - Not run yet:
add_as_text('999', '1')→'1000' - Not run yet:
add_as_text('0', '0')→'0' - Not run yet: Extra test
- Not run yet:
A number with 5,000 nines, plus 1 - Not run yet:
Speed test: two numbers of a million digits (how many digits, the first 3, the last 3)→[1000001, '133', '332']
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Turn them into ints
The idea: str(int(a) + int(b))
Python’s int has no top, so this is right, and it reads well.
def add_as_text(a: str, b: str) -> str: return str(int(a) + int(b))- Time
O(n²)- Space
O(n)- Why
- Turning n digits of text into an int, and back, takes time that grows faster than n.
Where it breaks: int() refuses text of more than 4,300 digits: ValueError: Exceeds the limit (4300 digits) for integer string conversion: value has 5000 digits; use sys.set_int_max_str_digits() to increase the limit. Python stops there on purpose, because turning a huge number into text and back is slow.
Add column by column, like at school
The idea: From the right: add the two digits and the carry, keep total % 10, carry total // 10.
The waste in Okay: it turns all the text into a number, when each column needs only two digits.
total % 10 is the digit to write, and total // 10 is the carry, 0 or 1. The loop goes on while there’s a digit or a carry left, so "999" plus "1" gets its last 1.
Highlighted lines are new in this rung.
def add_as_text(a: str, b: str) -> str: result = "" carry = 0 i, j = len(a) - 1, len(b) - 1 while i >= 0 or j >= 0 or carry: total = carry if i >= 0: total += int(a[i]) i -= 1 if j >= 0: total += int(b[j]) j -= 1 result = str(total % 10) + result carry = total // 10 return result- Time
O(n²)- Space
O(n)- Why
- Each of the n digits is put in front of the result, which copies the whole result.
What changed from the previous rung
def add_as_text(a: str, b: str) -> str: return str(int(a) + int(b)) result = "" carry = 0 i, j = len(a) - 1, len(b) - 1 while i >= 0 or j >= 0 or carry: total = carry if i >= 0: total += int(a[i]) i -= 1 if j >= 0: total += int(b[j]) j -= 1 result = str(total % 10) + result carry = total // 10 return resultWhere it breaks: str(total % 10) + result makes a new string and copies all of result into it, for every digit. With a million digits, that’s about 500 billion characters copied, and it runs out of time.
Collect the digits, join once
The idea: Append each digit to a list, then return ''.join(reversed(digits)).
The waste in Better: it copies the answer so far, for every digit it adds.
The digits come out from right to left, so they’re reversed once, at the end, when join() builds the text in one go.
Highlighted lines are new in this rung.
def add_as_text(a: str, b: str) -> str: digits = [] carry = 0 i, j = len(a) - 1, len(b) - 1 while i >= 0 or j >= 0 or carry: total = carry if i >= 0: total += int(a[i]) i -= 1 if j >= 0: total += int(b[j]) j -= 1 digits.append(str(total % 10)) carry = total // 10 return "".join(reversed(digits))- Time
O(n)- Space
O(n)- Why
- One pass over the digits; append() is O(1), and join() copies each digit once.
What changed from the previous rung
def add_as_text(a: str, b: str) -> str: result = "" digits = [] carry = 0 i, j = len(a) - 1, len(b) - 1 while i >= 0 or j >= 0 or carry: total = carry if i >= 0: total += int(a[i]) i -= 1 if j >= 0: total += int(b[j]) j -= 1 result = str(total % 10) + result digits.append(str(total % 10)) carry = total // 10 return result return "".join(reversed(digits))When Best isn’t best: when you control the program, sys.set_int_max_str_digits(0) lifts the limit, and int does the sum in one line. Keep the limit on for text from users.
Check yourself
Section titled “Check yourself”A few quick questions. Ones you miss are remembered, so you can find your weak spots later.
Flashcards
Section titled “Flashcards”Add these to your review deck. Each card comes back a little later every time you get it right.
Cheat sheet
Section titled “Cheat sheet”0.1 + 0.2 != 0.3: a float is the nearest binary fraction. Compare floats withmath.isclose().- Money:
Decimal("19.99")from text, thenquantize(Decimal("0.01"), rounding=ROUND_HALF_UP). Or whole cents in anint. round()sends halves to the even neighbour:round(2.5)is 2.//divides and rounds down,%is what’s left, and/always gives a float.s[start:stop:step]: the stop is never taken, a negative place counts from the back, ands[::-1]is the reverse.- A string can’t change: keep what
strip(),upper()andreplace()give back. f"{x:.2f}",f"{x:>8}",f"{n:,}",f"{x=}". A width is the least, not the most.- Build long text with a list and
"".join(pieces).
Go deeper
Section titled “Go deeper”- In the Python docs: floating point arithmetic: issues and limitations, decimal, string methods and the format spec mini-language.
- Draw your own: the slice picture on the Python visualizer.
- Next on the Python roadmap: My import got 100 times slower when the data grew 10 times, on list, tuple, dict and set, and what each one costs.
Can you do these now?
If you’re not sure of any of these, scroll back to that part before moving on.
Status: Not started