Integers
Python integers are exact and have no upper bound. Most languages give you a fixed number of bits (32 or 64) and overflow silently. Python integers grow as large as your memory allows.
1# No overflow in Python2x = 2 ** 643print(x) # 18446744073709551616: bigger than any 64-bit int45# Factorial of 100: try this in C6import math7print(math.factorial(100))8# 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
Integer literals
Python lets you write integer literals in four bases. All four produce the same kind of object. The base only affects how you write the number, not how Python stores it.
1decimal = 255 # base 10 (default)2hexadecimal = 0xFF # base 16: common for colors, bitmasks3octal = 0o377 # base 8: file permissions (chmod 644)4binary = 0b11111111 # base 2: bitwise operations56print(decimal == hexadecimal == octal == binary) # True: all 2557print(hex(255)) # '0xff'8print(bin(255)) # '0b11111111'9print(oct(255)) # '0o377'
Underscore separators
Underscores in numeric literals are ignored by Python. Use them to make large numbers readable, the same way you'd use commas in prose.
1population = 8_100_000_000 # 8.1 billion: easy to read2max_int_32 = 2_147_483_647 # clear grouping3hex_color = 0xFF_A5_00 # orange in hex: grouped by channel45print(population) # 8100000000: underscores stripped
Bitwise operators
Python supports bitwise operations on integers. You'll use these for flags, permissions, and low-level data manipulation.
1a = 0b1010 # 102b = 0b1100 # 1234print(bin(a & b)) # AND: 0b1000 (8), bits set in both5print(bin(a | b)) # OR: 0b1110 (14), bits set in either6print(bin(a ^ b)) # XOR: 0b0110 (6), bits set in exactly one7print(bin(~a)) # NOT: -0b1011, flip all bits8print(bin(a << 1)) # left shift: 0b10100 (20), multiply by 29print(bin(a >> 1)) # right shift: 0b0101 (5), divide by 2
Floats and why they lie
Floats are Python's decimal numbers. They're fast and cover an enormous range, but they have a fundamental limitation that trips up almost every programmer.
1print(0.1 + 0.2) # 0.300000000000000042print(0.1 + 0.2 == 0.3) # False
This isn't a Python bug. It's how floating-point math works in every language: C, Java, JavaScript, Go. Python is just honest about it.
Why it happens
Computers store numbers in binary (base 2). Just like 1/3 has no exact decimal representation (0.333...), 1/10 has no exact binary representation. It's a repeating fraction in binary: 0.0001100110011...
Python stores floats using 64 bits (the IEEE 754 double precision standard). That gives you about 15-17 significant digits. The closest 64-bit float to 0.1 is not exactly 0.1, it's:
1# The actual value Python stores for 0.12from decimal import Decimal3print(Decimal(0.1))
When you add two slightly-off approximations of 0.1, you get an approximation of 0.2 that's slightly different from the closest approximation of 0.3. The errors compound. This is expected behavior, not a bug.
Comparing floats correctly
Never use == to compare floats. Use math.isclose(), which checks whether two floats are close enough to be considered equal given the inherent imprecision.
1import math23a = 0.1 + 0.24b = 0.356print(a == b) # False, never use == for floats7print(math.isclose(a, b)) # True8print(math.isclose(a, b, rel_tol=1e-9)) # True, default tolerance9print(abs(a - b) < 1e-9) # True, manual version of the same check
Special float values
Python floats can represent infinity and "not a number." These arise from certain operations rather than being written directly in most code.
1import math23inf = math.inf # positive infinity4print(inf) # inf5print(-inf) # -inf6print(inf + 1) # inf, infinity absorbs addition7print(inf > 1e308) # True, larger than any finite float89nan = math.nan # "not a number"10print(nan) # nan11print(nan == nan) # False, nan is never equal to anything, even itself12print(math.isnan(nan)) # True, correct way to check1314# These operations produce inf or nan15print(1.0 / 0.0) # ZeroDivisionError for floats16print(float('inf')) # construct from string17print(float('nan'))
Float precision in practice
1# Rounding for display, doesn't change the stored value2x = 3.1415926535897933print(round(x, 2)) # 3.144print(f"{x:.4f}") # "3.1416": formatted string56# repr() shows enough digits to reconstruct the float exactly7print(repr(0.1)) # 0.1: Python is smart about display8print(repr(0.10000000000000001)) # 0.1: same float, same repr910# sys.float_info shows the limits of your platform's floats11import sys12print(sys.float_info.max) # ~1.8e+30813print(sys.float_info.epsilon) # ~2.2e-16, smallest difference from 1.0
Arithmetic operators
Python has seven arithmetic operators. Most behave as expected, but three have gotchas worth knowing upfront.
| Operator | Name | Example | Result |
|---|---|---|---|
+ | Addition | 3 + 2 | 5 |
- | Subtraction | 3 - 2 | 1 |
* | Multiplication | 3 * 2 | 6 |
/ | Division | 7 / 2 | 3.5 (always float) |
// | Floor division | 7 // 2 | 3 (integer result) |
% | Modulo | 7 % 2 | 1 (remainder) |
** | Exponentiation | 2 ** 10 | 1024 |
Division always returns a float
In Python 3, / always returns a float, even when dividing two integers evenly. Use // when you need an integer result.
1print(10 / 2) # 5.0: float, even though it divides evenly2print(10 // 2) # 5: integer (floor division)3print(7 / 2) # 3.54print(7 // 2) # 3: rounds toward negative infinity, not zero
Floor division rounds toward negative infinity
// always rounds down, meaning toward negative infinity, not toward zero. This matters with negative numbers.
1print(7 // 2) # 3: expected2print(-7 // 2) # -4: rounds toward -inf, not -33print(7 // -2) # -4: same rule45# Compare with int() which truncates toward zero6print(int(-7 / 2)) # -3: truncates toward zero7print(-7 // 2) # -4: floors toward -infinity
Modulo: remainder after division
The % operator gives the remainder after floor division. Classic uses: checking for even/odd, wrapping around a range, extracting digits.
1# Even/odd2print(10 % 2) # 0: even3print(7 % 2) # 1: odd45# Wrapping: keep index in range 0..n-16index = 77n = 58print(index % n) # 2: wraps around910# Extract last two digits of a year11year = 202512print(year % 100) # 251314# Check divisibility15for n in range(1, 16):16 if n % 3 == 0 and n % 5 == 0:17 print("FizzBuzz")18 elif n % 3 == 0:19 print("Fizz")20 elif n % 5 == 0:21 print("Buzz")
Exponentiation
1print(2 ** 10) # 10242print(2 ** 0.5) # 1.4142...: square root via fractional exponent3print((-1) ** 0.5) # (6.123233995736766e-17+1j), complex number!45# Faster for large exponents: pow() with three arguments does modular exponentiation6print(pow(2, 100, 1000)) # 2^100 mod 1000 = 376, fast even for huge exponents
Operator precedence
Python follows standard math precedence. When in doubt, use parentheses, they make intent explicit and are never wrong.
1# PEMDAS/BODMAS applies2print(2 + 3 * 4) # 14, not 20: * before +3print((2 + 3) * 4) # 20: parens override45# ** binds tighter than unary minus, this surprises people6print(-2 ** 2) # -4, not 4: reads as -(2**2)7print((-2) ** 2) # 4: use parens when you mean negative base
Math functions
Python gives you math tools in two places: built-in functions available everywhere, and the math module for everything else.
Built-in functions
1# abs(): absolute value2print(abs(-7)) # 73print(abs(3.14)) # 3.1445# round(): round to n decimal places6print(round(3.14159, 2)) # 3.147print(round(3.14159, 0)) # 3.0: still a float8print(round(3.14159)) # 3: integer when no ndigits910# min() and max()11print(min(3, 1, 4, 1, 5)) # 112print(max(3, 1, 4, 1, 5)) # 513print(min([3, 1, 4, 1, 5])) # 1: also accepts a list1415# sum()16print(sum([1, 2, 3, 4, 5])) # 1517print(sum(range(1, 101))) # 5050: Gauss's formula check1819# divmod(): quotient and remainder in one call20q, r = divmod(17, 5)21print(q, r) # 3 2: same as (17 // 5, 17 % 5)2223# pow(): same as ** but accepts an optional modulus24print(pow(2, 10)) # 102425print(pow(2, 10, 1000)) # 24: 1024 mod 1000
Gotcha with round():Python uses banker's rounding (round half to even). So round(0.5) is 0 and round(1.5) is 2. This reduces cumulative rounding error in statistical calculations, but it surprises people expecting standard rounding.
The math module
1import math23# Constants4print(math.pi) # 3.1415926535897935print(math.e) # 2.7182818284590456print(math.tau) # 6.283...: 2 * pi7print(math.inf) # inf89# Rounding10print(math.floor(3.9)) # 3: always rounds down11print(math.ceil(3.1)) # 4: always rounds up12print(math.trunc(3.9)) # 3: truncates toward zero13print(math.trunc(-3.9)) # -3: toward zero (unlike floor)1415# Roots and powers16print(math.sqrt(16)) # 4.017print(math.isqrt(17)) # 4: integer square root (floor)1819# Logarithms20print(math.log(math.e)) # 1.0: natural log21print(math.log(100, 10)) # 2.0: log base 1022print(math.log2(1024)) # 10.0: log base 223print(math.log10(1000)) # 3.0
1import math23# Trig (angles in radians)4print(math.sin(math.pi / 2)) # 1.05print(math.cos(0)) # 1.06print(math.degrees(math.pi)) # 180.07print(math.radians(180)) # 3.14159...89# Useful utilities10print(math.factorial(10)) # 362880011print(math.gcd(48, 18)) # 6: greatest common divisor12print(math.lcm(4, 6)) # 12: least common multiple (Python 3.9+)13print(math.comb(10, 3)) # 120: "10 choose 3" combinations14print(math.perm(10, 3)) # 720: permutations1516# Float comparison (the right way)17print(math.isclose(0.1 + 0.2, 0.3)) # True18print(math.isfinite(math.inf)) # False19print(math.isinf(math.inf)) # True20print(math.isnan(float('nan'))) # True
Decimal: when you need exact math
For money, taxes, and anything where a penny of error is unacceptable, use decimal.Decimal. It stores numbers as exact decimal values, no binary approximation.
1from decimal import Decimal23# floats lie about money4price = 1.105tax_rate = 0.08256total = price * (1 + tax_rate)7print(total) # 1.1907500000000002: wrong89# Decimal gives exact results10price = Decimal("1.10") # use strings, not floats11tax_rate = Decimal("0.0825")12total = price * (1 + tax_rate)13print(total) # 1.19075: correct
Always construct Decimal from a string, not a float. Decimal(0.1) captures the imprecise float value. Decimal("0.1") gives you the exact decimal 0.1.
Controlling precision and rounding
1from decimal import Decimal, ROUND_HALF_UP, getcontext23# Set global precision (default is 28 significant digits)4getcontext().prec = 656result = Decimal("1") / Decimal("3")7print(result) # 0.333333: respects precision setting89# Round to 2 decimal places for display10price = Decimal("19.999")11rounded = price.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)12print(rounded) # 20.001314# All arithmetic operations return Decimal15a = Decimal("0.10")16b = Decimal("0.20")17print(a + b) # 0.30: exact
fractions.Fraction: exact rational math
For exact fraction arithmetic (1/3 + 1/6 = 1/2), use fractions.Fraction. Less common than Decimal, but useful in geometry and statistics.
1from fractions import Fraction23a = Fraction(1, 3) # exactly 1/34b = Fraction(1, 6) # exactly 1/656print(a + b) # 1/2: exact7print(a * b) # 1/188print(float(a + b)) # 0.5
Use float for science, graphics, and performance-critical code. Use Decimal for money and anything that will be displayed to users as a dollar amount. Use Fraction for exact rational arithmetic where the denominator matters.
Real-world patterns
The numeric operations you'll reach for constantly in production.
Clamp a value to a range
Keep a value between a minimum and maximum. Common in UI (slider values, opacity), game logic (health points), and input validation.
1def clamp(value, low, high):2 return max(low, min(high, value))34print(clamp(5, 0, 10)) # 5: already in range5print(clamp(-3, 0, 10)) # 0: below min, snapped to 06print(clamp(15, 0, 10)) # 10: above max, snapped to 1078# Python 3.9+ has math.clamped equivalent in many contexts9# but the above is idiomatic and clear
Percentage calculations
1original = 1202discounted = 9034# What percentage is discounted of original?5pct = (discounted / original) * 1006print(f"{pct:.1f}%") # 75.0%78# How much was discounted?9saving_pct = ((original - discounted) / original) * 10010print(f"Saved {saving_pct:.0f}%") # Saved 25%1112# Apply a percentage increase13price = 10014markup = 1.15 # 15% markup15print(price * markup) # 115.0
Safe division
Division by zero raises an exception. Guard against it when the denominator comes from user input or data.
1def safe_divide(a, b, fallback=0):2 return a / b if b != 0 else fallback34print(safe_divide(10, 2)) # 5.05print(safe_divide(10, 0)) # 06print(safe_divide(10, 0, fallback=None)) # None
Format numbers for display
1# Thousands separator2print(f"{1_234_567:,}") # 1,234,5673print(f"{1_234_567.89:,.2f}") # 1,234,567.8945# Fixed decimal places6print(f"{3.14159:.2f}") # 3.147print(f"{0.0042:.4f}") # 0.00428print(f"{0.0042:.2e}") # 4.20e-03: scientific notation910# Padding for alignment11for n in [1, 10, 100, 1000]:12 print(f"{n:>6,}") # right-aligned, 6 wide
Integer math tricks
1# Check if a number is a power of two2def is_power_of_two(n):3 return n > 0 and (n & (n - 1)) == 045print(is_power_of_two(8)) # True6print(is_power_of_two(10)) # False78# Count digits without converting to string9import math10def count_digits(n):11 return math.floor(math.log10(abs(n))) + 1 if n != 0 else 11213print(count_digits(12345)) # 514print(count_digits(0)) # 11516# Round up to the nearest multiple17def round_up_to(n, multiple):18 return math.ceil(n / multiple) * multiple1920print(round_up_to(23, 5)) # 2521print(round_up_to(20, 5)) # 20