Skip to content

My calculator says 2 + 3 is 23

  • Python · Foundations
  • 15 min read, about 55 min with practice
  • Concept
  • Asked often: low
  • Reviewed 2026-11

You write your first useful program: it asks for two numbers and adds them.

a = input("First number: ")
b = input("Second number: ")
print(a + b)

You run it and type 2 and 3:

First number: 2
Second number: 3
23

Python didn’t add the numbers. It stuck them together. Why?

  • Input: two pieces of text, exactly as input() gave them back: "2" and "3". They may have spaces round them, and may be decimals like "2.5".
  • Output: text like "2 + 3 = 5", with the numbers as typed, spaces removed. A whole answer shows no .0: "0.5 + 0.5 = 1", not "= 1.0".
  • The hard part: text that looks like a number is still text, until you turn it into one.

Example: add_typed("2.5", "1") gives "2.5 + 1 = 3.5".

Hints open one at a time:

Hint 1 of 3

input() always gives back a str, even when the user types digits.

Hint 2 of 3

int("2") gives the number 2. What does int("2.5") do?

Hint 3 of 3

float() reads decimals. A float that is a whole number can become an int again.

A variable is a name for a value. price = 5 makes the name price and points it at the number 5. Point it at something new any time: price = price + 1. Names are case-sensitive, so Price is a different name.

Every value has a type. type() tells you which:

Valuetype()What + does
5int, a whole numberAdds: 5 + 3 is 8
2.5float, a decimalAdds: 2.5 + 1 is 3.5
"5"str, textJoins: "5" + "3" is "53"

input() always gives back text. Whatever the user types, you get a str. Turn it into a number with int() or float(). And turn a number into text with str(), or put it in an f-string.

print() shows values. It puts a space between them and a new line at the end. sep= and end= change those. An f-string puts values inside text: f"{qty} pens".

Here are text and numbers side by side. Each name points to its value. The player stops to ask what happens next. Then type your own lines under it.

Text joins, numbers add

Names

none yet

Objects

no lists, dicts or tuples yet

Step 1 of 8. Before the first line runs, there are no names and no objects.

a = "2"
b = "3"
joined = a + b
x = 2
y = 3
total = x + y
print(joined, total)

Try your own input

Use numbers, text, lists, dicts and tuples. It knows =, +=, del, print(), comparisons, slices, comprehensions, the common list methods, sorted(), list(), dict(), tuple(), range(), and copy.deepcopy() after import copy.

All three do one sum on two short pieces of text, so all are O(1). What changes is which typed text they get right.

The program around add_typed stays the same. It asks for the two numbers and prints the answer:

calculator.py
# calculator.py: run it with python calculator.py
from best import add_typed
a = input("First number: ")
b = input("Second number: ")
print(add_typed(a, b))
Okay

Add the text

The idea: Put a + between what input() gave back.

On 2 and 3: a is "2" and b is "3", both text. + on text joins it, so the answer is "23".

def add_typed(a: str, b: str) -> str:
# a and b are what input() gave back.
return f"{a} + {b} = {a + b}"
Time
O(1)
Space
O(1)
Why
One join.

Where it breaks: every time. + never adds text as numbers.

Better

int() first

The idea: Turn each piece of text into a whole number with int(), then add.

The waste in Okay: it uses the text as it came, when it needs to be a number.

On 2 and 3: int("2") + int("3") is 5. int() ignores spaces round the digits, so " 2 " works too.

Highlighted lines are new in this rung.

def add_typed(a: str, b: str) -> str:
# a and b are what input() gave back.
total = int(a) + int(b)
return f"{a.strip()} + {b.strip()} = {total}"
Time
O(1)
Space
O(1)
Why
Two conversions and one sum.
What changed from the previous rung
def add_typed(a: str, b: str) -> str:
# a and b are what input() gave back.
return f"{a} + {b} = {a + b}"
total = int(a) + int(b)
return f"{a.strip()} + {b.strip()} = {total}"

Where it breaks: int("2.5") raises ValueError: invalid literal for int() with base 10: '2.5'. Any decimal crashes the program.

Best

float(), then tidy the answer

The idea: Turn the text into floats, add, and show a whole answer without .0.

The waste in Better: it reads only whole numbers, when people type decimals too.

float() reads both "2" and "2.5". But 2.0 + 3.0 is 5.0, and people expect 5. total.is_integer() is true for 5.0, so int(total) drops the .0.

Highlighted lines are new in this rung.

def add_typed(a: str, b: str) -> str:
# a and b are what input() gave back.
total = float(a) + float(b)
if total.is_integer():
total = int(total)
return f"{a.strip()} + {b.strip()} = {total}"
Time
O(1)
Space
O(1)
Why
Two conversions, one sum and one check.
What changed from the previous rung
def add_typed(a: str, b: str) -> str:
# a and b are what input() gave back.
total = int(a) + int(b)
total = float(a) + float(b)
if total.is_integer():
total = int(total)
return f"{a.strip()} + {b.strip()} = {total}"

When Best isn’t best: text like "two" still raises ValueError. The exceptions lesson shows how to catch it and ask again. For money, use whole cents or the decimal module, because floats can’t hold 0.1 exactly.

Now write add_typed in the page. Press Run tests to check your code. Python runs in your browser, so nothing is sent anywhere.

Try Okay and Better from the ladder first: each fails a different test.

Add two typed numbers

Ctrl + Enter runs

Loads about 6 MB the first time. Best on Wi-Fi.

  1. Not run yet: add_typed('2', '3') → '2 + 3 = 5'
  2. Not run yet: add_typed(' 2 ', '3 ') → '2 + 3 = 5'
  3. Not run yet: add_typed('2.5', '1') → '2.5 + 1 = 3.5'
  4. Not run yet: Extra test
  5. Not run yet: Extra test

Extra tests check tricky cases. You see their input only if they fail.

Try your own input
Way"2", "3""2.5", "1"GoodNot so good
Okay: a + b232.51ShortJoins text, never adds
Better: int()5ValueErrorRight for whole numbersCrashes on decimals
Best: float(), then tidy53.5Right for both, and reads wellA few more lines
  • int() of a decimal number: int(2.9) is 2. It drops the part after the point; it doesn’t round.
  • int() of decimal text: int("2.9") is a ValueError. Use float() first.
  • Names: letters, digits and _, not starting with a digit. my_total is fine; 2nd and my-total are not.
  • Swapping two values: a, b = b, a swaps them in one line, with no extra name.
  • Decimal places: f"{total:.2f}" shows two places. The value itself doesn’t change.

Questions to ask first

  • Can the input have decimals, spaces or signs?
  • What should happen when the text isn’t a number?

A 60-second walkthrough

“input() always returns a string, so + concatenates. I convert with float(), which accepts whole numbers, decimals and surrounding spaces. A whole result like 5.0 I turn back into an int for display. Text like 'two' raises ValueError, so in a real tool I’d catch that and ask again.”

Likely follow-ups

  • How do you check a value’s type? type(x), or isinstance(x, int) to also accept subclasses.
  • What is the difference between int and float? An int is a whole number of any size. A float is a decimal with about 15 to 17 digits of precision.
  • Why does 0.1 + 0.2 print 0.30000000000000004? Floats are stored in binary, which can’t hold 0.1 exactly. The numbers and strings lesson explains it.

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

Write greet(name). name is what the user typed: it may have spaces at either end or between words, in any case. Return "Hello, Ana Maria!", with each word starting with a capital letter and one space between words.

Hint 1 of 3

name.strip() removes spaces at both ends.

Hint 2 of 3

name.split() with no argument splits on any run of spaces, and drops the empty pieces.

Hint 3 of 3

" ".join(words) puts the words back with one space each.

Greet by name

Ctrl + Enter runs

Loads about 6 MB the first time. Best on Wi-Fi.

  1. Not run yet: greet('Ana') → 'Hello, Ana!'
  2. Not run yet: greet(' ana ') → 'Hello, Ana!'
  3. Not run yet: greet(' ana maria ') → 'Hello, Ana Maria!'
  4. Not run yet: Extra test

Extra tests check tricky cases. You see their input only if they fail.

Try your own input
Show the Okay, Better and Best answers
Okay

Put the name in an f-string

The idea: Use the name exactly as typed.

"Ana" gives "Hello, Ana!".

def greet(name: str) -> str:
return f"Hello, {name}!"
Time
O(n)
Space
O(n)
Why
n is the length of the name; the f-string copies it.

Where it breaks: " ana " gives "Hello, ana !", with the spaces and the small letter kept.

Better

strip() and title()

The idea: Remove the spaces at the ends, and capitalise each word.

The waste in Okay: it trusts the user to type the name neatly.

Highlighted lines are new in this rung.

def greet(name: str) -> str:
return f"Hello, {name.strip().title()}!"
Time
O(n)
Space
O(n)
Why
Each method makes one new string.
What changed from the previous rung
def greet(name: str) -> str:
return f"Hello, {name}!"
return f"Hello, {name.strip().title()}!"

Where it breaks: spaces between words stay. "ana maria" gives "Hello, Ana Maria!".

Best

split(), then join()

The idea: Split the name into words, capitalise each, and join them with one space.

The waste in Better: it tidies the ends, but not the middle.

Highlighted lines are new in this rung.

def greet(name: str) -> str:
words = name.split()
return f"Hello, {' '.join(word.capitalize() for word in words)}!"
Time
O(n)
Space
O(n)
Why
One pass to split, one to join.
What changed from the previous rung
def greet(name: str) -> str:
return f"Hello, {name.strip().title()}!"
words = name.split()
return f"Hello, {' '.join(word.capitalize() for word in words)}!"

When Best isn’t best: names like “McDonald” or “van der Berg” have their own capitals. Real systems keep the name as the person typed it, with only the outer spaces removed.

Write to_fahrenheit(text). text is a temperature in °C, typed by a user, like "36.6". Return "36.6°C is 97.9°F", both with one place after the point. The formula is °F = °C × 9 / 5 + 32.

Hint 1 of 3

The text may be a decimal, so int() won’t do.

Hint 2 of 3

36.6 * 9 / 5 + 32 is 97.88000000000001 in Python. Floats can’t hold every decimal exactly.

Hint 3 of 3

Show one place with {value:.1f}.

Celsius to Fahrenheit

Ctrl + Enter runs

Loads about 6 MB the first time. Best on Wi-Fi.

  1. Not run yet: to_fahrenheit('37') → '37.0°C is 98.6°F'
  2. Not run yet: to_fahrenheit('36.6') → '36.6°C is 97.9°F'
  3. Not run yet: Extra test
  4. Not run yet: Extra test

Extra tests check tricky cases. You see their input only if they fail.

Try your own input
Show the Okay, Better and Best answers
Okay

int() and the formula

The idea: Turn the text into a whole number and apply the formula.

"100" gives "100°C is 212.0°F". The / always gives a float.

def to_fahrenheit(text: str) -> str:
celsius = int(text)
return f"{celsius}°C is {celsius * 9 / 5 + 32}°F"
Time
O(1)
Space
O(1)
Why
One sum.

Where it breaks: "36.6" raises ValueError.

Better

float()

The idea: Read decimals with float().

The waste in Okay: it reads only whole numbers.

Highlighted lines are new in this rung.

def to_fahrenheit(text: str) -> str:
celsius = float(text)
return f"{celsius}°C is {celsius * 9 / 5 + 32}°F"
Time
O(1)
Space
O(1)
Why
One sum.
What changed from the previous rung
def to_fahrenheit(text: str) -> str:
celsius = int(text)
celsius = float(text)
return f"{celsius}°C is {celsius * 9 / 5 + 32}°F"

Where it breaks: "36.6" gives "36.6°C is 97.88000000000001°F". The value is right, but nobody wants to read it.

Best

One place after the point

The idea: Show both numbers with :.1f.

The waste in Better: it shows every digit Python stores.

Highlighted lines are new in this rung.

def to_fahrenheit(text: str) -> str:
celsius = float(text)
return f"{celsius:.1f}°C is {celsius * 9 / 5 + 32:.1f}°F"
Time
O(1)
Space
O(1)
Why
One sum.
What changed from the previous rung
def to_fahrenheit(text: str) -> str:
celsius = float(text)
return f"{celsius}°C is {celsius * 9 / 5 + 32}°F"
return f"{celsius:.1f}°C is {celsius * 9 / 5 + 32:.1f}°F"

When Best isn’t best: if the result goes on to more sums, keep the float and format it only when you show it.

Write line(name, qty, price). Every line must be exactly 22 characters, so the columns line up. Put the name in 10 places on the left and the count in 3 places on the right. Then " x ", then the price in 6 places with 2 decimals.

A name longer than 10 characters is cut to 9, plus "…". Example: line("Pen", 3, 1.5) gives "Pen 3 x 1.50".

Hint 1 of 3

In an f-string, {name:<10} pads to 10 places on the left; {qty:>3} pads on the right.

Hint 2 of 3

{price:>6.2f} shows 2 decimals in 6 places.

Hint 3 of 3

Padding never cuts a long value. Cut it yourself with name[:9].

A receipt line that lines up

Ctrl + Enter runs

Loads about 6 MB the first time. Best on Wi-Fi.

  1. Not run yet: line('Pen', 3, 1.5) → 'Pen 3 x 1.50'
  2. Not run yet: line('Notebook', 12, 10) → 'Notebook 12 x 10.00'
  3. Not run yet: line('Highlighter pen', 1, 2.25) → 'Highlight… 1 x 2.25'
  4. Not run yet: Extra test

Extra tests check tricky cases. You see their input only if they fail.

Try your own input
Show the Okay, Better and Best answers
Okay

Join with str()

The idea: Turn each number into text with str() and join the pieces with +.

"Pen 3 x 1.5" holds the right facts.

def line(name: str, qty: int, price: float) -> str:
return name + " " + str(qty) + " x " + str(price)
Time
O(1)
Space
O(1)
Why
A few short pieces of text.

Where it breaks: lines of different lengths don’t line up, and 1.5 isn’t shown as money.

Better

Widths in an f-string

The idea: Pad each value to its own width with :<10, :>3 and :>6.2f.

The waste in Okay: it builds the line by hand, when the f-string can pad it.

Highlighted lines are new in this rung.

def line(name: str, qty: int, price: float) -> str:
return f"{name:<10}{qty:>3} x {price:>6.2f}"
Time
O(1)
Space
O(1)
Why
One f-string.
What changed from the previous rung
def line(name: str, qty: int, price: float) -> str:
return name + " " + str(qty) + " x " + str(price)
return f"{name:<10}{qty:>3} x {price:>6.2f}"

Where it breaks: a width is the least, not the most. “Highlighter pen” takes 15 places and pushes the columns right.

Best

Cut long names

The idea: Cut a name over 10 characters to 9 plus …, then pad as before.

The waste in Better: it trusts every name to be short.

Highlighted lines are new in this rung.

def line(name: str, qty: int, price: float) -> str:
if len(name) > 10:
name = name[:9] + "…"
return f"{name:<10}{qty:>3} x {price:>6.2f}"
Time
O(1)
Space
O(1)
Why
One check and one f-string.
What changed from the previous rung
def line(name: str, qty: int, price: float) -> str:
if len(name) > 10:
name = name[:9] + "…"
return f"{name:<10}{qty:>3} x {price:>6.2f}"

When Best isn’t best: for a real report, the tabulate package or a spreadsheet does the lining up. And a huge count or price still pushes its column, so check those too if they can grow.

A few quick questions. Ones you miss are remembered, so you can find your weak spots later.

  1. Predict the outputThe user types 4. What does this print?
    n = input("How many? ")
    print(n * 3)
  2. Multiple choiceWhich line turns the typed text "2.5" into a number?
  3. Spot the bugThis line raises TypeError. Why?
    age = 30
    print("Age: " + age)
  4. Predict the outputWhat does this print?
    price = 5
    price = price * 2
    print(f"{price:.2f}")

Add these to your review deck. Each card comes back a little later every time you get it right.

  • input() always gives back a str. Convert with int() or float() before sums.
  • + adds numbers and joins text. Mixing them is a TypeError.
  • type(x) shows a value’s type: int, float, str, bool, list…
  • f-strings: f"{x}", f"{x:.2f}" (2 decimals), f"{s:<10}" (pad left), f"{n:>3}" (pad right).
  • print(a, b, sep="-", end="") changes the space between values and the new line at the end.

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