3. Numbers, text, and bytes
3.1 Integers have no overflow
>>> 2 ** 64
18446744073709551616
>>> (2 ** 64) * (2 ** 64)
340282366920938463463374607431768211456
>>> 7 // 2, -7 // 2, 7 % 3, -7 % 3 # floor division and modulo round toward -inf
(3, -4, 1, 2)
>>> 0b1010, 0o17, 0xff, 1_000_000 # literals; _ is a digit separator
(10, 15, 255, 1000000)
Python int grows as needed, so there is no integer overflow (and no
overflow vulnerability class like
C §28).
The cost is that every integer is a heap object of 28+ bytes. A list of a
million integers uses about 36 MB in Python and 8 MB as a NumPy int64
array. That difference is Chapter 53.
Since 3.11, converting huge strings to int is limited to 4300 digits by
default (sys.set_int_max_str_digits), because the conversion is
quadratic and was a denial-of-service vector (CVE-2020-10735).
3.2 Floats are IEEE 754 doubles
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> float("nan") == float("nan")
False
>>> 1e308 * 10
inf
The Maths guide explains why 0.1 has no
exact binary representation. The rules: compare floats with
math.isclose, never use floats for money, and expect nan to poison
every computation it touches (a common cause of a training loss suddenly
reading nan).
For money, use decimal.Decimal with string inputs, or integer cents:
from decimal import Decimal
print(Decimal("0.10") + Decimal("0.20")) # 0.30
print(Decimal(0.1)) # 0.1000000000000000055511151231257827021181583404541015625
fractions.Fraction gives exact rational arithmetic when you need it.
3.3 str is a sequence of Unicode code points
s = "naïve café 🐍"
print(len(s)) # 12: code points, not bytes
print(s[0], s[-1]) # n 🐍
print(s.upper()) # NAÏVE CAFÉ 🐍
print(s.encode("utf-8")) # b'na\xc3\xafve caf\xc3\xa9 \xf0\x9f\x90\x8d'
print(len(s.encode("utf-8"))) # 17 bytes
Strings are immutable. Building a big string with += in a loop can be
quadratic; collect parts in a list and "".join(parts) once.
The operations you will use daily:
line = " GET /index.html HTTP/1.1 "
print(line.strip().split()) # ['GET', '/index.html', 'HTTP/1.1']
print("a,b,,c".split(",")) # ['a', 'b', '', 'c']
print("-".join(["2026", "10", "04"])) # 2026-10-04
print("report.csv".removesuffix(".csv"))# report
print("Error: disk".startswith(("Error", "Fatal"))) # True
print(f"{3.14159:.2f} {42:>6} {255:#x} {0.257:.1%}") # 3.14 42 0xff 25.7%
name, n = "load", 3
print(f"{name=} {n * 2=}") # name='load' n * 2=6 (debug form)
3.4 bytes is a sequence of integers 0–255
Files, sockets, hashes, and encryption work on bytes, not text. Python 3 keeps the two types strictly separate, which is the right design (the Security guide Ch 5 explains why mixing them causes bugs):
data = b"GET / HTTP/1.1\r\n"
print(data[0]) # 71: indexing bytes gives an int
print(data[:3]) # b'GET'
print(data.decode("ascii")) # str
print(bytes([0x48, 0x69])) # b'Hi'
print(bytes.fromhex("deadbeef"), b"\xde\xad".hex())
buf = bytearray(b"hello") # the mutable version
buf[0] = ord("H")
print(buf) # bytearray(b'Hello')
The rule: decode bytes to str as soon as they enter your program
(from a file, socket, or subprocess), work with str inside, and encode
back to bytes at the edge. Always name the encoding; it is almost always
"utf-8".
# Mixing them is a TypeError, on purpose:
"abc" + b"def"
# TypeError: can only concatenate str (not "bytes") to str
3.5 Booleans and truthiness
bool is a subclass of int (True == 1). In if and while, every
object has a truth value. These are false: None, False, 0, 0.0,
"", b"", and empty containers ([], {}, set(), ()). Everything
else is true.
items: list[str] = []
if not items:
print("nothing to do")
and and or return one of their operands, not a bool:
port = None
print(port or 8080) # 8080
print("" or "default") # default
print(0 or 8080) # 8080 <- careful: 0 might be a valid value
Use x if x is not None else default when 0 or "" are legitimate
values.