Skip to content

Numbers, Strings & Operators

What this lesson gives you

The everyday data types and the operations on them — including the float gotcha that has embarrassed many a live demo.

Estimated time: 22 min read · Part: Values, Variables & Expressions

Learning objectives

  • Explain Python's numeric tower: int (arbitrary precision), float (IEEE 754), Decimal (exact), complex.
  • Predict float comparison failures and apply the correct fix for currency and exact decimal arithmetic.
  • Write idiomatic string operations: slicing, f-strings with format specs, and the most-used str methods.
  • Distinguish bytes from str and know when each is appropriate.
  • Apply operator precedence confidently and use augmented assignment operators correctly.

Python has three numeric types you'll use regularly: int (whole numbers with no size limit), float (IEEE 754 double-precision decimals), and the standard-library Decimal (exact decimal arithmetic). There is also complex for imaginary numbers, which appears in scientific computing but rarely in business code. The choice between them is not cosmetic — using the wrong one produces wrong answers, sometimes silently.

Integers and arithmetic operators

Python's int is arbitrary precision — it grows to accommodate any integer, limited only by memory. There is no int32/int64 overflow problem in Python; 2 ** 1000 produces the correct answer. This differs from most languages and is one of the silent quality-of-life improvements that beginners often don't notice until they switch to a language that does overflow.

integers.py
# Arithmetic operators
print(7 + 2)     # 9   — add
print(7 - 2)     # 5   — subtract
print(7 * 2)     # 14  — multiply
print(7 / 2)     # 3.5 — true division ALWAYS returns float
print(7 // 2)    # 3   — floor division: rounds toward negative infinity
print(7 % 2)     # 1   — modulo (remainder)
print(7 ** 2)    # 49  — exponentiation
print(-7 // 2)   # -4  — floor division: rounds DOWN, not toward zero!

# Arbitrary precision in action:
print(2 ** 100)  # 1267650600228229401496703205376 — no overflow

# Integer bit operations (useful for flags and binary data):
print(0b1010 & 0b1100)  # 8  — bitwise AND
print(0b1010 | 0b1100)  # 14 — bitwise OR
print(1 << 4)            # 16 — left shift (multiply by 2^4)

9 | 5 | 14 | 3.5 | 3 | 1 | 49 | -4 1267650600228229401496703205376 8 | 14 | 16

Floor division floors toward negative infinity, not zero

-7 // 2 is -4, not -3. Floor division rounds toward negative infinity, not toward zero (truncation). This matters when computing offsets, indices, or time periods with negative numbers. Most people expect -3 (truncation behavior from C) and get -4 instead. If you need truncation toward zero, use int(-7 / 2) or math.trunc(-7 / 2).

Floats and the Decimal fix

Python's float type is an IEEE 754 double-precision binary floating-point number — the standard representation used by nearly every modern programming language and hardware. The fundamental problem: most base-10 decimals (like 0.1, 0.2, 0.3) cannot be represented exactly in base-2. The number stored for 0.1 is actually 0.1000000000000000055511151231257827021181583404541015625. This is not a Python bug — it's how binary floating-point works everywhere.

For day-to-day calculations where small rounding errors don't matter (physics simulations, graphics, probability calculations), float is perfect. For financial calculations where exact decimal arithmetic is required — any code involving dollars and cents — use decimal.Decimal. The performance cost is small and the correctness gain is critical.

float_gotcha.py
# The classic float trap:
print(0.1 + 0.2)           # 0.30000000000000004
print(0.1 + 0.2 == 0.3)    # False!

# Workaround 1: math.isclose() for approximate comparisons
import math
print(math.isclose(0.1 + 0.2, 0.3))    # True — within relative tolerance

# Workaround 2: Decimal for exact arithmetic (use for money)
from decimal import Decimal, ROUND_HALF_UP

price    = Decimal("19.99")    # always pass a STRING to Decimal
tax_rate = Decimal("0.08")
tax      = price * tax_rate
total    = price + tax
total_rounded = total.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

print(f"Tax:   ${tax}")          # 1.5992 — exact
print(f"Total: ${total_rounded}") # 21.59

# Workaround 3: store integer cents
price_cents = 1999    # $19.99 stored as cents
tax_cents   = price_cents * 8 // 100  # integer division
print(f"Total: ${(price_cents + tax_cents) / 100:.2f}")   # $21.59

0.30000000000000004 False True Tax: $1.5992 Total: $21.59 Total: $21.59

Strings, f-strings, and the most-used methods

Strings (str) are sequences of Unicode characters, immutable, and first-class objects with a rich method interface. They are delimited by single or double quotes interchangeably; triple-quoted strings ("""...""" or '''...''') span multiple lines and are used for docstrings and multi-line text blocks. Raw strings (r"...") treat backslashes literally — essential for regular expressions and Windows file paths.

The dominant formatting style is the f-string (formatted string literal, introduced in Python 3.6). Prefix with f, then embed any Python expression inside { }. F-strings support a powerful format mini-language for controlling how numbers and other values are displayed, using a :{format_spec} syntax inside the braces.

strings.py
# String literals
s1 = "Hello, world"
s2 = 'same thing'
s3 = """multi
line
string"""
path = r"C:\Users\Ada\Documents"   # raw: backslashes are literal

# f-string format mini-language:
name, revenue, pct = "Acme", 1_234_567.89, 0.1234

print(f"{name:<12}")            # "Acme        " (left-align, width 12)
print(f"${revenue:,.2f}")       # "$1,234,567.89"
print(f"{pct:.1%}")             # "12.3%"
print(f"{name=}")               # "name='Acme'" — debug form (3.8+)
print(f"{2 ** 10:,}")           # "1,024" (any expression in braces)

"Acme " $1,234,567.89 12.3% name='Acme' 1,024

str_methods.py
s = "  Hello, Solutions World  "

# Cleaning:
print(s.strip())               # "Hello, Solutions World"
print(s.lower())               # "  hello, solutions world  "
print(s.upper())               # "  HELLO, SOLUTIONS WORLD  "

# Searching and replacing:
print(s.strip().startswith("Hello"))   # True
print("Solutions" in s)          # True (membership test)
print(s.replace("World", "Team"))  # new string — immutable!

# Splitting and joining:
words = "Acme,Globex,Initech".split(",")   # ["Acme", "Globex", "Initech"]
print(" | ".join(words))               # "Acme | Globex | Initech"

# Slicing (same syntax as lists):
text = "solutions"
print(text[0:4])     # "solu"
print(text[-4:])     # "ions"
print(text[::-1])    # "snoitulos"  (reversed)

"Hello, Solutions World" " hello, solutions world " True | True ['Acme', 'Globex', 'Initech'] "Acme | Globex | Initech" "solu" | "ions" | "snoitulos"

Strings are immutable

Every string method (.upper(), .replace(), .strip()) returns a new string — the original is never changed. This is why you always write s = s.strip() (rebind the name) rather than just calling s.strip() and expecting s to change. This immutability is a feature, not a limitation: it makes strings safe to use as dict keys and to share between functions without defensive copying.

bytes vs str, and encoding

Python 3 has a sharp distinction between str (text, Unicode) and bytes (raw binary data). They cannot be mixed without explicit conversion, which prevents a whole class of encoding bugs that plagued Python 2. When you read from a network socket or open a file in binary mode, you get bytes. When you work with text you get str. Converting between them requires specifying an encoding.

bytes_str.py
text  = "café"                       # str — Unicode text
bdata = text.encode("utf-8")      # str → bytes: b'caf\xc3\xa9'
back  = bdata.decode("utf-8")     # bytes → str: "café"

print(type(bdata))   # <class 'bytes'>
print(bdata)         # b'caf\xc3\xa9'
print(back)          # café

# File modes:
# open("f.txt", "r")  → yields str (text mode, decodes via locale)
# open("f.bin", "rb") → yields bytes (binary mode, no decoding)

# Rule: use UTF-8 everywhere unless you have a specific reason not to.
# Always specify encoding explicitly when opening text files:
with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()
Format spec Meaning Example → output
:d Integer f"{42:d}""42"
:.2f Float, 2 decimal places f"{3.14159:.2f}""3.14"
:, Thousands separator f"{1234567:,}""1,234,567"
:.1% Percentage, 1 decimal f"{0.456:.1%}""45.6%"
:e Scientific notation f"{12345:e}""1.234500e+04"
:<20 Left-align, width 20 f"{'hi':<20}""hi "
:>20 Right-align, width 20 f"{'hi':>20}"" hi"
:^20 Center, width 20 f"{'hi':^20}"" hi "
:0>5 Right-align, zero-pad to width 5 f"{7:0>5}""00007"
:b Binary representation f"{10:b}""1010"
Method Returns Common use
s.strip() str Remove leading/trailing whitespace
s.lower() / s.upper() str Case normalisation
s.split(sep) list[str] Tokenise by delimiter
sep.join(lst) str Concatenate with separator
s.replace(old, new) str Substitute substrings
s.startswith(p) / .endswith(p) bool Prefix/suffix check
s.find(sub) int (-1 if missing) Position of first occurrence
s.count(sub) int Number of non-overlapping occurrences
s.zfill(width) str Zero-pad (IDs, codes)
s.encode("utf-8") bytes Convert to bytes for network/file I/O
Knowledge check

InterviewA junior writes a billing feature using float for dollar amounts and a test fails because 1.10 + 2.20 != 3.30. What's your one-line diagnosis and fix?

  • Python's addition is broken; file a bug with the core team.
  • Binary floating-point can't represent these decimals exactly — use Decimal (or integer cents) for money and round only when displaying.
  • Multiply everything by 100 forever and never divide.
  • Switch the comparison to != so the test passes.
Answer

Binary floating-point can't represent these decimals exactly — use Decimal (or integer cents) for money and round only when displaying.

Floats are base-2 approximations of base-10 decimals, so exact equality fails. The professional fix for currency is decimal.Decimal (or storing integer cents); display rounding comes last. Recognising this instantly is a small but real credibility signal.

Knowledge check

Concept checkWhat does -7 // 2 evaluate to in Python, and why might that surprise someone coming from C?

  • -3 — Python truncates toward zero like C.
  • -4 — Python's floor division rounds toward negative infinity, not toward zero. C's integer division truncates toward zero, giving -3.
  • 3 — Python ignores the sign.
  • -3.5 — floor division always produces a float.
Answer

-4 — Python's floor division rounds toward negative infinity, not toward zero. C's integer division truncates toward zero, giving -3.

Python's // operator is floor division — it always rounds toward negative infinity. For -7 ÷ 2, the mathematical result is -3.5, and floor(-3.5) = -4. C's integer division truncates toward zero, giving -3. If you need truncation-toward-zero in Python, use int(-7/2) or math.trunc(-7/2) .

Knowledge check

AppliedYou receive a raw string from a CSV export: " $1,234.56 ". Write a one-liner that converts it to a Python float.

  • float(" $1,234.56 ") — float() handles all that automatically.
  • float(" $1,234.56 ".strip().replace("$", "").replace(",", "")) — strip whitespace, remove the dollar sign and commas, then convert.
  • int(" $1,234.56 ")
  • You need regex — there's no simpler way.
Answer

float(" $1,234.56 ".strip().replace("$", "").replace(",", "")) — strip whitespace, remove the dollar sign and commas, then convert.

float() only handles clean decimal strings — it raises ValueError on currency symbols or commas. The chain of string methods (strip, remove $, remove ,) transforms the raw value into something float() can parse. This pattern is extremely common in data ingestion scripts. For more complex currency parsing, the babel or money libraries help.

Exercise

Write a function format_deal_summary(deals: list[dict]) -> str that takes a list of deal dicts (each with "name", "mrr" (float), and "close_date" (str)) and returns a formatted multi-line string report. Each line should show the deal name left-aligned in 20 chars, MRR as $X,XXX.XX, and the close date. Add a total MRR line at the bottom. Use f-strings throughout and Decimal for the total to avoid float drift.

Show solution
from decimal import Decimal

def format_deal_summary(deals: list[dict]) -> str:
    lines = []
    lines.append(f"{'Deal':<20} {'MRR':>12} {'Close'}")
    lines.append("-" * 44)
    total = Decimal("0")
    for deal in deals:
        mrr = Decimal(str(deal["mrr"]))
        total += mrr
        lines.append(
            f"{deal['name']:<20} ${float(mrr):>10,.2f} {deal['close_date']}"
        )
    lines.append("-" * 44)
    lines.append(f"{'TOTAL':<20} ${float(total):>10,.2f}")
    return "\n".join(lines)

deals = [
    {"name": "Acme Corp", "mrr": 4999.99, "close_date": "2026-07-15"},
    {"name": "Globex Solutions", "mrr": 12500.00, "close_date": "2026-07-22"},
    {"name": "Initech Ltd", "mrr": 750.50, "close_date": "2026-08-01"},
]
print(format_deal_summary(deals))

Key takeaways

  • Numeric types: int (unbounded, never overflows), float (binary approximate), Decimal (exact, for money).
  • / always returns float; // floors toward negative infinity; % is remainder; ** is power.
  • Never use float for currency0.1 + 0.2 != 0.3. Use Decimal or integer cents.
  • f-strings are the modern way to format text; the format mini-language controls alignment, precision, and separators.
  • Strings are immutable — methods return new strings; always rebind if you need the result.
  • str (text/Unicode) and bytes (binary) are separate types; encode/decode explicitly with UTF-8.