5.6 Arithmetic Operators and Mathematical Functions
Right, let’s get our hands dirty with the actual doing of math. This isn’t high school algebra; it’s Python, which means it’s both incredibly powerful and occasionally, well, quirky. I’m going to walk you through the operators you’ll use every day and the built-in functions that’ll save your bacon. Pay attention—this is where the tiny, invisible gotchas live that can derail an entire analysis.
The Usual Suspects: +, -, *, /
These work exactly as you’d expect. Mostly. Addition, subtraction, multiplication—no surprises. Division (/) is where Python wised up. Even if you feed it two integers, it always returns a float. This is a fantastic design choice that prevents a whole class of “why is my result 4 instead of 4.5?” headaches that plague other languages.
# The basics work as advertised
print(5 + 3) # 8
print(5 - 3) # 2
print(5 * 3) # 15
print(5 / 2) # 2.5 (a float, even though 5 and 2 are integers)
Floor Division and Modulo: // and %
Sometimes you want to know how many whole times one number fits into another (//, floor division) and what’s left over (%, modulo). These are your go-to tools for splitting things into whole units.
# How many full weeks are in 17 days?
days = 17
full_weeks = days // 7 # 2
remaining_days = days % 7 # 3
print(f"{days} days is {full_weeks} weeks and {remaining_days} days.")
But here’s the first “trench” insight: floor division rounds towards negative infinity, not towards zero. This is mathematically correct but can be startling if you’re used to other languages. For positive numbers, it’s a non-issue (10 // 3 is 3). For negatives, watch this:
print(10 // 3) # 3
print(-10 // 3) # -4 (not -3!)
Why -4? Because -4 * 3 is -12, and the next whole number up towards zero is -3, but -3 * 3 is -9, which is greater than -10. Floor division answers the question “what’s the next lowest whole number?” It’s consistent, but you need to know it.
The Power of ** and pow()
Exponentiation. You can use the ** operator or the built-in pow() function. They are largely interchangeable for simple cases.
print(2 ** 8) # 256
print(pow(2, 8)) # 256
So why have two ways? pow() has a secret third argument: pow(x, y, z). This calculates (x ** y) % z but does it way more efficiently. It’s a lifesaver in cryptography and anywhere you’re dealing with massive numbers.
# Calculate (5 ** 100) % 13
# Doing 5**100 first creates an astronomically huge number, then mods it.
result_dumb = 5 ** 100 % 13
# pow() does the modular exponentiation without the huge intermediate number.
result_smart = pow(5, 100, 13)
print(result_dumb, result_smart) # Both are 1, but one didn't strain your RAM.
The round() Function: A Noble Lie
round() is useful but fundamentally a lie. It doesn’t actually round a floating-point number to a given precision. It rounds it to the nearest value that can be precisely represented as a binary floating-point number. Because of base-2 representation quirks, this can lead to some head-scratching moments.
# Seems fine...
print(round(3.14159, 2)) # 3.14
# Wait, what?
print(round(2.675, 2)) # 2.67, not 2.68!?
Why 2.67? Because 2.675 is actually stored as a value slightly less than 2.675 in its binary form, so when round() looks at it, it correctly rounds down. This isn’t a bug; it’s a consequence of the inherent imprecision of binary floats. The best practice here is to never use round() for financial calculations or anywhere you need exact decimal representation. Use the decimal module for that. Use round() for displaying estimates to users, not for precise computation.
The Math Module: Your Toolbox
Python’s math module is a treasure trove of standard functions. Need a square root? math.sqrt(). Need logarithms, trigonometry, or constants? It’s all there.
import math
print(math.sqrt(144)) # 12.0
print(math.factorial(5)) # 120
print(math.pi) # 3.141592653589793
print(math.log(100, 10)) # 2.0
But again, a word from the trenches: these functions work on floats. They inherit all the precision and representation issues that come with them. For int and Decimal, you have other options. For example, math.sqrt() will always return a float, even if the answer is a perfect integer. If you need an integer square root and want to avoid floating-point entirely, you’d do:
def is_perfect_square(num):
root = math.isqrt(num) # Integer square root function (returns floor)
return root * root == num
print(is_perfect_square(144)) # True
print(is_perfect_square(10)) # False
The key takeaway? Choose your tool based on your problem. Use int for whole numbers, float for scientific calculations where speed and memory are key and approximate precision is acceptable, and decimal for money or any scenario where decimal accuracy is non-negotiable. And always, always test your arithmetic at the boundaries.