Python & AI Tutorials Logo
Python Programming

4. Working with Numbers

In Chapter 3, you learned how to create variables and work with Python's basic numeric types: integers and floating-point numbers. Now it's time to put those numbers to work. In this chapter, you'll learn how to perform calculations, combine operations, and use Python's built-in tools for working with numeric data.

Numbers are fundamental to programming. Whether you're calculating totals, processing measurements, managing inventory, or analyzing data, you'll need to perform arithmetic operations. Python makes numeric operations straightforward and intuitive, but there are important details to understand—especially about how different types of division work, how operators interact, and how floating-point numbers behave.

By the end of this chapter, you'll be comfortable performing calculations, understanding operator behavior, and using Python's numeric functions to solve real-world problems.

4.1) Basic Arithmetic: Addition, Subtraction, and Multiplication

Let's start with the most fundamental arithmetic operations. Python uses familiar symbols for basic math, and these operations work just as you'd expect from everyday arithmetic.

4.1.1) Addition with the + Operator

The + operator adds two numbers together. It works with both integers and floating-point numbers:

python
# basic_addition.py
# Adding integers
total = 15 + 27
print(total)  # Output: 42
 
# Adding floats
price = 19.99 + 5.50
print(price)  # Output: 25.49
 
# You can add multiple numbers in one expression
sum_total = 10 + 20 + 30 + 40
print(sum_total)  # Output: 100

Addition is straightforward, but there's an important detail: when you add an integer and a float, Python automatically converts the result to a float to preserve the decimal precision:

python
# mixed_addition.py
result = 10 + 3.5
print(result)  # Output: 13.5
print(type(result))  # Output: <class 'float'>

This automatic conversion happens because a float can represent both whole numbers and decimals, while an integer cannot represent decimals. Python chooses the type that won't lose information.

4.1.2) Subtraction with the - Operator

The - operator subtracts the second number from the first:

python
# basic_subtraction.py
# Subtracting integers
difference = 100 - 42
print(difference)  # Output: 58
 
# Subtracting floats
remaining = 50.75 - 12.25
print(remaining)  # Output: 38.5
 
# Subtraction can produce negative results
balance = 20 - 35
print(balance)  # Output: -15

Like addition, subtraction also promotes integers to floats when you mix types:

python
# mixed_subtraction.py
result = 100 - 0.01
print(result)  # Output: 99.99
print(type(result))  # Output: <class 'float'>

4.1.3) Multiplication with the * Operator

The * operator multiplies two numbers:

python
# basic_multiplication.py
# Multiplying integers
product = 6 * 7
print(product)  # Output: 42
 
# Multiplying floats
area = 3.5 * 2.0
print(area)  # Output: 7.0
 
# Multiplying by zero always gives zero
result = 1000 * 0
print(result)  # Output: 0

Multiplication follows the same type conversion rules. When you multiply an integer by a float, the result is a float:

python
# mixed_multiplication.py
result = 5 * 2.5
print(result)  # Output: 12.5
print(type(result))  # Output: <class 'float'>
 
# Even if the result is a whole number
result = 4 * 2.0
print(result)  # Output: 8.0 (note the .0)
print(type(result))  # Output: <class 'float'>

Notice in the last example that even though 4 × 2.0 equals 8, Python represents it as 8.0 because one of the operands was a float. The type of the result depends on the types of the inputs, not on whether the mathematical result happens to be a whole number.

4.1.4) Practical Examples with Basic Arithmetic

Let's see how these operations work together in realistic scenarios:

python
# shopping_cart.py
# Calculate a shopping cart total
item1_price = 12.99
item2_price = 8.50
item3_price = 15.00
 
subtotal = item1_price + item2_price + item3_price
print(f"Subtotal: ${subtotal}")  # Output: Subtotal: $36.49
 
tax_rate = 0.08
tax = subtotal * tax_rate
print(f"Tax: ${tax}")  # Output: Tax: $2.9192
 
total = subtotal + tax
print(f"Total: ${total}")  # Output: Total: $39.4092
python
# temperature_change.py
# Calculate temperature change
morning_temp = 45.5
afternoon_temp = 68.2
 
change = afternoon_temp - morning_temp
print(f"Temperature increased by {change} degrees")
# Output: Temperature increased by 22.7 degrees

These basic operations form the foundation for more complex calculations. Understanding how they work—especially how Python handles type conversions—will help you avoid surprises when your programs perform calculations.

4.2) Division, Floor Division, and Remainder: /, //, and %

Division in Python is more nuanced than addition, subtraction, or multiplication. Python provides three different division-related operators, each serving a distinct purpose. Understanding when to use each one is crucial for writing correct programs.

4.2.1) Regular Division with /

The / operator performs "true division"—it always returns a floating-point result, even when dividing two integers:

python
# true_division.py
# Dividing integers
result = 10 / 2
print(result)  # Output: 5.0 (note: float, not 5)
print(type(result))  # Output: <class 'float'>
 
# Division that doesn't result in a whole number
result = 10 / 3
print(result)  # Output: 3.3333333333333335
 
# Dividing floats
result = 15.5 / 2.5
print(result)  # Output: 6.2

The key insight here is that / always produces a float, even when both operands are integers and the result is mathematically a whole number. This is by design: Python wants division to behave consistently and preserve decimal precision.

This behavior is different from some other programming languages where dividing two integers produces an integer result. In Python 3, if you want an integer result, you need to use floor division (which we'll cover next) or explicitly convert the result.

4.2.2) Floor Division with //

The // operator performs "floor division"—it divides and then rounds down to the nearest integer. When both operands are integers, the result is an integer. When either operand is a float, the result is a float (but still rounded down):

python
# floor_division.py
# Floor division with integers
result = 10 // 3
print(result)  # Output: 3 (not 3.333...)
print(type(result))  # Output: <class 'int'>
 
# Even division still gives an integer
result = 10 // 2
print(result)  # Output: 5 (integer, not 5.0)
print(type(result))  # Output: <class 'int'>
 
# Floor division with floats gives a float
result = 10.0 // 3
print(result)  # Output: 3.0
print(type(result))  # Output: <class 'float'>

"Rounding down" means moving toward negative infinity, not just removing the decimal part. This matters for negative numbers:

python
# floor_division_negative.py
# Positive numbers: rounds down (toward negative infinity)
result = 7 // 2
print(result)  # Output: 3
 
# Negative numbers: still rounds toward negative infinity
result = -7 // 2
print(result)  # Output: -4 (not -3!)
 
# Why -4? Because -3.5 rounded down (toward negative infinity) is -4
# Think of the number line: ... -4, -3.5, -3, -2, -1, 0, 1, 2, 3, 3.5, 4 ...

Floor division is useful when you need to split items into groups or calculate how many complete units fit into a quantity:

python
# floor_division_practical.py
# How many complete dozens in 50 eggs?
eggs = 50
eggs_per_dozen = 12
complete_dozens = eggs // eggs_per_dozen
print(f"Complete dozens: {complete_dozens}")  # Output: Complete dozens: 4
 
# How many full hours in 150 minutes?
minutes = 150
full_hours = minutes // 60
print(f"Full hours: {full_hours}")  # Output: Full hours: 2

4.2.3) Remainder (Modulo) with %

The % operator (called "modulo" or "mod") returns the remainder after division. It answers the question: "After dividing, what's left over?"

python
# modulo_basic.py
# 10 divided by 3 is 3 with remainder 1
result = 10 % 3
print(result)  # Output: 1
 
# 10 divided by 2 is 5 with remainder 0 (even division)
result = 10 % 2
print(result)  # Output: 0
 
# Works with floats too
result = 10.5 % 3
print(result)  # Output: 1.5

The modulo operator is incredibly useful for several common programming patterns:

Checking if a number is even or odd:

python
# even_odd_check.py
number = 17
 
if number % 2 == 0:
    print(f"{number} is even")
else:
    print(f"{number} is odd")
# Output: 17 is odd

Getting the leftover items:

python
# leftover_items.py
eggs = 50
eggs_per_dozen = 12
 
leftover = eggs % eggs_per_dozen
print(f"Leftover eggs: {leftover}")  # Output: Leftover eggs: 2

Wrapping values within a range (like clock arithmetic):

python
# clock_arithmetic.py
# What hour is it 25 hours from now? (on a 12-hour clock)
current_hour = 10
hours_later = 25
future_hour = (current_hour + hours_later) % 12
print(f"Hour: {future_hour}")  # Output: Hour: 11

4.2.4) Relationship Between //, %, and /

Floor division and modulo are closely related. Together, they give you the complete result of division:

python
# division_relationship.py
dividend = 17
divisor = 5
 
quotient = dividend // divisor  # How many times 5 goes into 17
remainder = dividend % divisor  # What's left over
 
print(f"{dividend} ÷ {divisor} = {quotient} remainder {remainder}")
# Output: 17 ÷ 5 = 3 remainder 2
 
# You can verify: quotient * divisor + remainder should equal dividend
verification = quotient * divisor + remainder
print(f"Verification: {quotient} × {divisor} + {remainder} = {verification}")
# Output: Verification: 3 × 5 + 2 = 17

This relationship is always true: for any numbers a and b (where b is not zero), a == (a // b) * b + (a % b).

4.2.5) Choosing the Right Division Operator

Here's a quick guide for choosing which operator to use:

  • Use / when you need the exact mathematical result with decimals (most common in calculations)
  • Use // when you need to count how many complete groups fit (like dozens, hours, pages)
  • Use % when you need the remainder or leftover amount (like checking even/odd, wrapping values, finding leftovers)
python
# choosing_operators.py
# Scenario: Distributing 47 candies among 6 children
 
candies = 47
children = 6
 
# How many candies per child? (use //)
per_child = candies // children
print(f"Each child gets {per_child} candies")  # Output: Each child gets 7 candies
 
# How many candies are left over? (use %)
leftover = candies % children
print(f"Leftover candies: {leftover}")  # Output: Leftover candies: 5
 
# What's the exact average? (use /)
average = candies / children
print(f"Average per child: {average}")  # Output: Average per child: 7.833333333333333

4.3) Augmented Assignment Operators

When programming, you'll frequently need to update a variable by performing an operation on its current value. For example, adding to a running total, subtracting from a balance, or multiplying a value by a factor. Python provides a concise way to do this with augmented assignment operators.

4.3.1) What Augmented Assignment Operators Are

An augmented assignment operator combines an arithmetic operation with assignment. Instead of writing:

python
# traditional_update.py
count = 10
count = count + 5  # Add 5 to count
print(count)  # Output: 15

You can write:

python
# augmented_update.py
count = 10
count += 5  # Same as: count = count + 5
print(count)  # Output: 15

Both versions do exactly the same thing, but the augmented version is more concise and clearly expresses the intent: "increase count by 5."

4.3.2) All Augmented Assignment Operators

Python provides augmented assignment operators for all arithmetic operations:

python
# all_augmented_operators.py
# Addition
x = 10
x += 5  # x = x + 5
print(f"After += 5: {x}")  # Output: After += 5: 15
 
# Subtraction
x = 10
x -= 3  # x = x - 3
print(f"After -= 3: {x}")  # Output: After -= 3: 7
 
# Multiplication
x = 10
x *= 4  # x = x * 4
print(f"After *= 4: {x}")  # Output: After *= 4: 40
 
# Division
x = 10
x /= 2  # x = x / 2
print(f"After /= 2: {x}")  # Output: After /= 2: 5.0
 
# Floor division
x = 10
x //= 3  # x = x // 3
print(f"After //= 3: {x}")  # Output: After //= 3: 3
 
# Modulo
x = 10
x %= 3  # x = x % 3
print(f"After %= 3: {x}")  # Output: After %= 3: 1
 
# Exponentiation (we'll see more about ** in section 4.6)
x = 2
x **= 3  # x = x ** 3 (2 to the power of 3)
print(f"After **= 3: {x}")  # Output: After **= 3: 8

4.3.3) Common Use Cases for Augmented Assignment

Augmented assignment operators shine in several common programming patterns:

Accumulating a total:

python
# accumulating_total.py
total = 0
total += 10  # Add first item
total += 25  # Add second item
total += 15  # Add third item
print(f"Total: {total}")  # Output: Total: 50

Counting occurrences:

python
# counting.py
count = 0
# Imagine these happen as we process data
count += 1  # Found one
count += 1  # Found another
count += 1  # Found another
print(f"Count: {count}")  # Output: Count: 3

Updating a balance:

python
# balance_updates.py
balance = 100.00
balance -= 25.50  # Purchase
balance += 50.00  # Deposit
balance -= 10.00  # Purchase
print(f"Balance: ${balance}")  # Output: Balance: $114.5

Applying repeated operations:

python
# repeated_operations.py
value = 100
value *= 1.1  # Increase by 10%
value *= 1.1  # Increase by 10% again
value *= 1.1  # Increase by 10% again
print(f"Value after three 10% increases: {value}")
# Output: Value after three 10% increases: 133.10000000000002

4.3.4) Important Details About Augmented Assignment

Augmented assignment creates a new object for immutable types:

Remember from Chapter 3 that numbers are immutable—you can't change a number's value, only create new numbers. When you write x += 5, Python creates a new number object and assigns it to x. The old number object is discarded (we'll explore this concept more deeply in Chapter 17 when we discuss Python's object model):

python
# augmented_with_immutables.py
x = 10
print(id(x))  # Output: (some memory address)
 
x += 5
print(id(x))  # Output: (different memory address)

For now, just understand that x += 5 is equivalent to x = x + 5—it's a convenient shorthand, not a fundamentally different operation.

You can't use augmented assignment before a variable exists:

python
# augmented_requires_existing.py
# This will cause an error:
# count += 1  # NameError: name 'count' is not defined
 
# You must initialize the variable first:
count = 0
count += 1  # Now this works
print(count)  # Output: 1

Type conversions happen the same way:

python
# augmented_type_conversion.py
x = 10  # integer
x += 2.5  # Add a float
print(x)  # Output: 12.5
print(type(x))  # Output: <class 'float'>

The result follows the same type conversion rules as the regular operators: if you mix integers and floats, the result is a float.

4.3.5) When to Use Augmented Assignment

Use augmented assignment operators whenever you're updating a variable based on its current value. They make your code:

  • More concise: x += 5 is shorter than x = x + 5
  • More readable: The intent is immediately clear
  • Less error-prone: You don't risk mistyping the variable name

Compare these two versions:

python
# comparison.py
# Without augmented assignment
accumulated_distance_in_kilometers = 0
accumulated_distance_in_kilometers = accumulated_distance_in_kilometers + 10
accumulated_distance_in_kilometers = accumulated_distance_in_kilometers + 25
 
# With augmented assignment
accumulated_distance_in_kilometers = 0
accumulated_distance_in_kilometers += 10
accumulated_distance_in_kilometers += 25

The augmented version is clearer and has less chance of typos. Augmented assignment operators are a small feature, but they're used constantly in real Python code.

4.4) Operator Precedence and Parentheses

When you combine multiple operations in a single expression, Python needs rules to determine the order of evaluation. Should 2 + 3 * 4 be calculated as (2 + 3) * 4 = 20 or as 2 + (3 * 4) = 14? The answer depends on operator precedence—the rules that determine which operations happen first.

4.4.1) Understanding Operator Precedence

Python follows the same precedence rules you learned in mathematics: multiplication and division happen before addition and subtraction. This is often remembered with the acronym PEMDAS (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction), though we'll cover exponents in section 4.6.

python
# precedence_basic.py
# Multiplication happens before addition
result = 2 + 3 * 4
print(result)  # Output: 14 (not 20)
# Python calculates: 2 + (3 * 4) = 2 + 12 = 14
 
# Division happens before subtraction
result = 10 - 8 / 2
print(result)  # Output: 6.0 (not 1.0)
# Python calculates: 10 - (8 / 2) = 10 - 4.0 = 6.0

Here's the precedence order for the operators we've learned so far (highest to lowest):

  1. Parentheses () — highest precedence, always evaluated first
  2. Exponentiation ** — (we'll cover this in section 4.6)
  3. Multiplication, Division, Floor Division, Modulo *, /, //, % — same level, evaluated left to right
  4. Addition, Subtraction +, - — same level, evaluated left to right

Let's see more examples of how precedence works:

python
# precedence_examples.py
# Multiplication before addition
result = 5 + 2 * 3
print(result)  # Output: 11 (5 + 6)
 
# Division before subtraction
result = 20 - 10 / 2
print(result)  # Output: 15.0 (20 - 5.0)
 
# Multiple operations at the same level: left to right
result = 10 - 3 + 2
print(result)  # Output: 9 ((10 - 3) + 2)
 
result = 20 / 4 * 2
print(result)  # Output: 10.0 ((20 / 4) * 2)

4.4.2) Using Parentheses to Control Order

Parentheses override the default precedence. Operations inside parentheses are always performed first, regardless of the operators involved:

python
# parentheses_override.py
# Without parentheses: multiplication first
result = 2 + 3 * 4
print(result)  # Output: 14
 
# With parentheses: addition first
result = (2 + 3) * 4
print(result)  # Output: 20
 
# Another example
result = 10 - 8 / 2
print(result)  # Output: 6.0
 
result = (10 - 8) / 2
print(result)  # Output: 1.0

You can nest parentheses to create more complex expressions. Python evaluates from the innermost parentheses outward:

python
# nested_parentheses.py
result = ((2 + 3) * 4) - 1
print(result)  # Output: 19
# Step 1: (2 + 3) = 5
# Step 2: (5 * 4) = 20
# Step 3: 20 - 1 = 19
 
result = 2 * (3 + (4 - 1))
print(result)  # Output: 12
# Step 1: (4 - 1) = 3
# Step 2: (3 + 3) = 6
# Step 3: 2 * 6 = 12

4.4.3) When Operators Have the Same Precedence

When multiple operators at the same precedence level appear in an expression, Python evaluates them from left to right (this is called "left associativity"):

python
# left_to_right.py
# Addition and subtraction: left to right
result = 10 - 3 + 2 - 1
print(result)  # Output: 8
# Evaluation: ((10 - 3) + 2) - 1 = (7 + 2) - 1 = 9 - 1 = 8
 
# Multiplication and division: left to right
result = 20 / 4 * 2
print(result)  # Output: 10.0
# Evaluation: (20 / 4) * 2 = 5.0 * 2 = 10.0
 
# This matters! Different order gives different result:
result = 20 / (4 * 2)
print(result)  # Output: 2.5

4.4.4) Practical Examples with Precedence

Let's look at realistic scenarios where understanding precedence matters:

python
# temperature_conversion.py
# Convert Fahrenheit to Celsius: C = (F - 32) * 5 / 9
fahrenheit = 98.6
 
# Correct: parentheses ensure subtraction happens first
celsius = (fahrenheit - 32) * 5 / 9
print(f"{fahrenheit}°F = {celsius}°C")
# Output: 98.6°F = 37.0°C
 
# Wrong: without parentheses, multiplication happens first
# celsius = fahrenheit - 32 * 5 / 9  # This would be wrong!
# This would calculate: fahrenheit - ((32 * 5) / 9)
python
# calculate_average.py
# Calculate average of three numbers
num1 = 85
num2 = 92
num3 = 78
 
# Correct: parentheses ensure addition happens before division
average = (num1 + num2 + num3) / 3
print(f"Average: {average}")  # Output: Average: 85.0
 
# Wrong: without parentheses, only num3 is divided by 3
# average = num1 + num2 + num3 / 3  # This would be wrong!
# This would calculate: 85 + 92 + (78 / 3) = 85 + 92 + 26.0 = 203.0
python
# discount_calculation.py
# Calculate price after discount and tax
original_price = 100.00
discount_rate = 0.20
tax_rate = 0.08
 
# Calculate discount amount
discount = original_price * discount_rate
 
# Calculate price after discount
discounted_price = original_price - discount
 
# Calculate final price with tax
final_price = discounted_price * (1 + tax_rate)
print(f"Final price: ${final_price}")  # Output: Final price: $86.4
 
# Or in one expression (using parentheses to be clear):
final_price = (original_price * (1 - discount_rate)) * (1 + tax_rate)
print(f"Final price: ${final_price}")  # Output: Final price: $86.4

4.4.5) Best Practices for Operator Precedence

Use parentheses for clarity, even when not strictly necessary:

Sometimes adding parentheses makes your intent clearer, even if they don't change the result:

python
# clarity_with_parentheses.py
# These are equivalent, but the second is clearer:
result = 2 + 3 * 4
result = 2 + (3 * 4)  # Clearer: shows you know multiplication happens first
 
# Complex expressions benefit from parentheses:
result = (subtotal * tax_rate) + (subtotal * tip_rate)  # Clear intent

Break complex expressions into steps:

When an expression becomes too complex, it's often better to split it into multiple lines:

python
# breaking_into_steps.py
# Instead of this complex one-liner:
result = ((price * quantity) * (1 - discount)) * (1 + tax)
 
# Consider breaking it into steps:
subtotal = price * quantity
discounted = subtotal * (1 - discount)
final = discounted * (1 + tax)
result = final

The multi-step version is easier to read, debug, and verify. Don't sacrifice clarity for conciseness.

When in doubt, use parentheses:

If you're unsure about precedence, add parentheses. The Python interpreter won't mind, and your future self (or other programmers reading your code) will thank you.

Understanding operator precedence helps you write correct expressions and read other people's code. The key principle: when combining operations, think about the order of evaluation, and use parentheses to make your intent explicit.

4.5) Mixing Integers and Floats in Expressions

You've already seen that Python automatically handles mixing integers and floats in simple operations. Now let's explore this behavior more systematically and understand the rules that govern type conversion in numeric expressions.

4.5.1) The Type Promotion Rule

When Python performs an arithmetic operation involving both an integer and a float, it automatically converts (or "promotes") the integer to a float before performing the operation. The result is always a float:

python
# type_promotion.py
# Integer + Float = Float
result = 10 + 3.5
print(result)  # Output: 13.5
print(type(result))  # Output: <class 'float'>
 
# Float + Integer = Float (order doesn't matter)
result = 3.5 + 10
print(result)  # Output: 13.5
print(type(result))  # Output: <class 'float'>
 
# This applies to all arithmetic operators
result = 5 * 2.0
print(result)  # Output: 10.0 (float, not int)
print(type(result))  # Output: <class 'float'>

Why does Python do this? Because floats can represent both whole numbers and decimals, while integers cannot represent decimals. Converting to float preserves all information and avoids losing precision.

Here's a visual representation of how Python decides the result type:

Yes

No

Yes

No

Operation with
int and float

Any operand
is float?

Result is float

Division
operator /?

Result is int

4.5.2) Type Promotion in Complex Expressions

When an expression contains multiple operations with mixed types, Python applies the promotion rule at each step:

python
# complex_mixed_types.py
# Multiple operations with mixed types
result = 10 + 3.5 * 2
print(result)  # Output: 17.0
print(type(result))  # Output: <class 'float'>
 
# What happens step by step:
# 1. 3.5 * 2 → 3.5 * 2.0 (2 promoted to float) → 7.0 (float)
# 2. 10 + 7.0 → 10.0 + 7.0 (10 promoted to float) → 17.0 (float)
 
# Another example
result = 5 / 2 + 3
print(result)  # Output: 5.5
print(type(result))  # Output: <class 'float'>
 
# Step by step:
# 1. 5 / 2 → 2.5 (division always produces float)
# 2. 2.5 + 3 → 2.5 + 3.0 (3 promoted to float) → 5.5 (float)

Once any operation in an expression produces a float, all subsequent operations involving that result will also produce floats.

4.5.3) Special Case: Regular Division Always Produces Floats

Remember from section 4.2 that the / operator always produces a float, even when dividing two integers:

python
# division_always_float.py
# Even when the result is a whole number
result = 10 / 2
print(result)  # Output: 5.0 (not 5)
print(type(result))  # Output: <class 'float'>
 
# This means any expression with / will have a float result
result = 10 / 2 + 3  # 5.0 + 3 → 5.0 + 3.0 → 8.0
print(result)  # Output: 8.0
print(type(result))  # Output: <class 'float'>

If you want an integer result from division, use floor division // instead (but remember it rounds down):

python
# floor_division_integer.py
# Floor division with integers produces an integer
result = 10 // 2
print(result)  # Output: 5 (integer)
print(type(result))  # Output: <class 'int'>
 
# But floor division with any float produces a float
result = 10.0 // 2
print(result)  # Output: 5.0 (float)
print(type(result))  # Output: <class 'float'>

4.5.4) Practical Implications of Type Mixing

Understanding type promotion helps you predict and control the types of your results:

python
# practical_type_mixing.py
# Calculating price per item
total_cost = 47.50
num_items = 5
 
price_per_item = total_cost / num_items  # Float / int → float
print(f"Price per item: ${price_per_item}")
# Output: Price per item: $4.75
 
# Calculating average (will be float even if inputs are integers)
total_points = 450
num_tests = 5
 
average = total_points / num_tests  # Int / int → float
print(f"Average: {average}")  # Output: Average: 90.0
 
# If you need an integer result, convert explicitly
average_rounded = int(total_points / num_tests)
print(f"Average (as integer): {average_rounded}")
# Output: Average (as integer): 90

4.5.5) When Integer Results Matter

Sometimes you specifically need integer results for counting, indexing, or other discrete operations. Here's how to ensure you get integers:

python
# ensuring_integer_results.py
# Using floor division when you need integer results
items = 47
items_per_box = 12
 
# How many complete boxes?
complete_boxes = items // items_per_box  # Integer result
print(f"Complete boxes: {complete_boxes}")
# Output: Complete boxes: 3
 
# If you use regular division, you get a float
boxes_float = items / items_per_box
print(f"Boxes (float): {boxes_float}")
# Output: Boxes (float): 3.9166666666666665
 
# Converting float to integer (truncates toward zero)
boxes_truncated = int(boxes_float)
print(f"Boxes (truncated): {boxes_truncated}")
# Output: Boxes (truncated): 3

Note the difference: // rounds down (toward negative infinity), while int() truncates toward zero. For positive numbers they're the same, but for negative numbers they differ:

python
# truncation_vs_floor.py
# For positive numbers: same result
print(7 // 2)    # Output: 3
print(int(7/2))  # Output: 3
 
# For negative numbers: different results
print(-7 // 2)    # Output: -4 (rounds down toward negative infinity)
print(int(-7/2))  # Output: -3 (truncates toward zero)

4.5.6) Avoiding Unexpected Float Results

Sometimes you might be surprised to get a float when you expected an integer. This usually happens because of division or mixing types:

python
# unexpected_floats.py
# Calculating average - result is always float because of division
count = 10
total = 100
average = total / count
print(average)  # Output: 10.0 (float, even though it's a whole number)
 
# If you need an integer and you know it divides evenly:
average_int = total // count
print(average_int)  # Output: 10 (integer)
 
# Or convert explicitly:
average_int = int(total / count)
print(average_int)  # Output: 10 (integer)

The key takeaway: Python's type promotion rules are designed to preserve precision and avoid data loss. When you mix integers and floats, or when you use regular division, expect float results. If you need integers, use floor division or explicit conversion, but be aware of how they handle rounding.

4.6) Useful Numeric Built-in Functions: abs(), min(), max(), and pow()

Python provides several built-in functions that perform common numeric operations. These functions work with both integers and floats, and they're essential tools for everyday programming. Let's explore the most frequently used ones.

4.6.1) Absolute Value with abs()

The abs() function returns the absolute value of a number—its distance from zero, ignoring the sign:

python
# absolute_value.py
# Absolute value of negative numbers
result = abs(-42)
print(result)  # Output: 42
 
# Absolute value of positive numbers (unchanged)
result = abs(42)
print(result)  # Output: 42
 
# Works with floats too
result = abs(-3.14)
print(result)  # Output: 3.14
 
# Absolute value of zero is zero
result = abs(0)
print(result)  # Output: 0

The absolute value function is useful whenever you care about magnitude but not direction:

python
# practical_abs.py
# Calculate temperature difference (magnitude only)
morning_temp = 45.5
evening_temp = 38.2
 
difference = abs(evening_temp - morning_temp)
print(f"Temperature changed by {difference} degrees")
# Output: Temperature changed by 7.3 degrees
 
# Calculate distance between two points (always positive)
position1 = 10
position2 = 25
 
distance = abs(position2 - position1)
print(f"Distance: {distance}")  # Output: Distance: 15
 
# Works the same regardless of order
distance = abs(position1 - position2)
print(f"Distance: {distance}")  # Output: Distance: 15

4.6.2) Finding Minimum and Maximum with min() and max()

The min() function returns the smallest of its arguments, and max() returns the largest:

python
# min_max_basic.py
# Find minimum of two numbers
smallest = min(10, 25)
print(smallest)  # Output: 10
 
# Find maximum of two numbers
largest = max(10, 25)
print(largest)  # Output: 25
 
# Works with more than two arguments
smallest = min(5, 12, 3, 18, 7)
print(smallest)  # Output: 3
 
largest = max(5, 12, 3, 18, 7)
print(largest)  # Output: 18
 
# Works with floats and mixed types
smallest = min(3.5, 2, 4.1, 1.9)
print(smallest)  # Output: 1.9

These functions are invaluable for finding extremes in data:

python
# practical_min_max.py
# Find the best and worst test scores
test1 = 85
test2 = 92
test3 = 78
test4 = 95
 
highest_score = max(test1, test2, test3, test4)
lowest_score = min(test1, test2, test3, test4)
 
print(f"Highest score: {highest_score}")  # Output: Highest score: 95
print(f"Lowest score: {lowest_score}")    # Output: Lowest score: 78
 
# Clamp a value within a range
value = 150
minimum = 0
maximum = 100
 
# Ensure value is not below minimum
value = max(value, minimum)
# Ensure value is not above maximum
value = min(value, maximum)
 
print(f"Clamped value: {value}")  # Output: Clamped value: 100

The clamping pattern (using max() and min() together) is common when you need to constrain a value to a specific range:

python
# clamping_pattern.py
def clamp(value, min_value, max_value):
    """Constrain value to be within [min_value, max_value]"""
    return max(min_value, min(value, max_value))
 
# Test the clamp function
print(clamp(150, 0, 100))   # Output: 100 (too high, clamped to max)
print(clamp(-10, 0, 100))   # Output: 0 (too low, clamped to min)
print(clamp(50, 0, 100))    # Output: 50 (within range, unchanged)

4.6.3) Exponentiation with pow()

The pow() function raises a number to a power. Python also has the ** operator for exponentiation, which is more commonly used, but pow() offers some additional features:

python
# exponentiation.py
# Using the ** operator (most common)
result = 2 ** 3  # 2 to the power of 3
print(result)  # Output: 8
 
result = 5 ** 2  # 5 squared
print(result)  # Output: 25
 
# Using pow() function (equivalent)
result = pow(2, 3)
print(result)  # Output: 8
 
result = pow(5, 2)
print(result)  # Output: 25
 
# Negative exponents give fractions
result = 2 ** -3  # 1 / (2^3) = 1/8
print(result)  # Output: 0.125
 
# Fractional exponents give roots
result = 9 ** 0.5  # Square root of 9
print(result)  # Output: 3.0
 
result = 8 ** (1/3)  # Cube root of 8
print(result)  # Output: 2.0

The pow() function can take an optional third argument for modular exponentiation (useful in cryptography and number theory, but beyond the scope of basic arithmetic):

python
# modular_exponentiation.py
# pow(base, exponent, modulus) computes (base ** exponent) % modulus efficiently
result = pow(2, 10, 100)  # (2^10) % 100
print(result)  # Output: 24
 
# This is more efficient than computing separately for large numbers:
# result = (2 ** 10) % 100  # Same result, but less efficient for large numbers

For most everyday use, the ** operator is more convenient than pow():

python
# practical_exponentiation.py
# Calculate compound interest: A = P(1 + r)^t
principal = 1000  # Initial amount
rate = 0.05       # 5% interest rate
years = 10
 
amount = principal * (1 + rate) ** years
print(f"Amount after {years} years: ${amount:.2f}")
# Output: Amount after 10 years: $1628.89
 
# Calculate area of a square
side_length = 5
area = side_length ** 2
print(f"Area: {area}")  # Output: Area: 25
 
# Calculate volume of a cube
side_length = 3
volume = side_length ** 3
print(f"Volume: {volume}")  # Output: Volume: 27

4.6.4) Combining Built-in Functions

These functions work well together for solving common problems:

python
# combining_functions.py
# Find the range (difference between max and min)
values = [15, 42, 8, 23, 37]
value_range = max(values) - min(values)
print(f"Range: {value_range}")  # Output: Range: 34
 
# Note: We're using a list here (we'll learn about lists in detail in Chapter 13)
# For now, just understand that max() and min() can work with a list of values
 
# Calculate distance between two points in 2D space
x1, y1 = 3, 4
x2, y2 = 6, 8
 
# Distance formula: sqrt((x2-x1)^2 + (y2-y1)^2)
# We'll use ** for squaring (square root comes in section 4.11)
distance_squared = (x2 - x1) ** 2 + (y2 - y1) ** 2
distance = distance_squared ** 0.5  # Square root via fractional exponent
print(f"Distance: {distance}")  # Output: Distance: 5.0

These built-in functions are fundamental tools in Python programming. They're efficient, well-tested, and work consistently across different numeric types. Use them whenever you need these common operations—there's no need to write your own implementations.

4.7) Rounding Numbers with round()

When working with floating-point numbers, you often need to round results to a specific number of decimal places for display, calculations, or storage. Python's round() function provides this capability.

4.7.1) Basic Rounding with round()

The round() function takes a number and rounds it to the nearest integer:

python
# basic_rounding.py
# Round to nearest integer
result = round(3.7)
print(result)  # Output: 4
 
result = round(3.2)
print(result)  # Output: 3
 
# Exactly halfway rounds to nearest even number (banker's rounding)
result = round(2.5)
print(result)  # Output: 2 (rounds to even)
 
result = round(3.5)
print(result)  # Output: 4 (rounds to even)
 
# Negative numbers work too
result = round(-3.7)
print(result)  # Output: -4
 
result = round(-3.2)
print(result)  # Output: -3

Notice the behavior when rounding numbers exactly halfway between two integers (like 2.5 or 3.5). Python uses "round half to even" (also called "banker's rounding"), which rounds to the nearest even number. This reduces bias in repeated rounding operations. For most everyday programming, this detail rarely matters, but it's good to be aware of.

4.7.2) Rounding to Specific Decimal Places

The round() function accepts an optional second argument specifying how many decimal places to keep:

python
# decimal_places.py
# Round to 2 decimal places
result = round(3.14159, 2)
print(result)  # Output: 3.14
 
# Round to 1 decimal place
result = round(3.14159, 1)
print(result)  # Output: 3.1
 
# Round to 3 decimal places
result = round(2.71828, 3)
print(result)  # Output: 2.718
 
# You can round to 0 decimal places (same as omitting the argument)
result = round(3.7, 0)
print(result)  # Output: 4.0 (note: returns float, not int)

When you specify decimal places, round() returns a float (even if you round to 0 decimal places). When you omit the second argument, it returns an integer.

4.7.3) Practical Uses of Rounding

Rounding is essential for displaying money, measurements, and other values where excessive precision is unnecessary or confusing:

python
# practical_rounding.py
# Display prices with 2 decimal places
price = 19.99
tax_rate = 0.08
total = price * (1 + tax_rate)
 
print(f"Total (unrounded): ${total}")  # Output: Total (unrounded): $21.5892
print(f"Total (rounded): ${round(total, 2)}")  # Output: Total (rounded): $21.59
 
# Calculate and display average
total_score = 456
num_tests = 7
average = total_score / num_tests
 
print(f"Average (unrounded): {average}")  # Output: Average (unrounded): 65.14285714285714
print(f"Average (rounded): {round(average, 2)}")  # Output: Average (rounded): 65.14
 
# Round measurements to reasonable precision
distance_meters = 123.456789
distance_rounded = round(distance_meters, 1)
print(f"Distance: {distance_rounded} meters")  # Output: Distance: 123.5 meters

4.7.4) Rounding vs Truncating vs Floor/Ceiling

It's important to understand that rounding is different from truncating (removing decimal places) or floor/ceiling operations:

python
# rounding_vs_others.py
value = 3.7
 
# Rounding: nearest integer
rounded = round(value)
print(f"Rounded: {rounded}")  # Output: Rounded: 4
 
# Truncating: remove decimal part (convert to int)
truncated = int(value)
print(f"Truncated: {truncated}")  # Output: Truncated: 3
 
# We'll see floor and ceil in section 4.11, but briefly:
# Floor: largest integer <= value (always rounds down)
# Ceiling: smallest integer >= value (always rounds up)

For negative numbers, the differences are more pronounced:

python
# negative_rounding.py
value = -3.7
 
# Rounding: nearest integer
rounded = round(value)
print(f"Rounded: {rounded}")  # Output: Rounded: -4
 
# Truncating: toward zero
truncated = int(value)
print(f"Truncated: {truncated}")  # Output: Truncated: -3
 
# Floor (rounds down toward negative infinity): -4
# Ceiling (rounds up toward positive infinity): -3

4.7.5) Important Considerations with Rounding

Floating-point precision can cause surprises:

Due to how computers represent floating-point numbers (which we'll discuss in section 4.10), rounding doesn't always produce exactly the result you might expect:

python
# rounding_surprises.py
# Sometimes rounding doesn't give the exact decimal you expect
value = 2.675
rounded = round(value, 2)
print(rounded)  # Output: 2.67 (not 2.68 as you might expect)
 
# This happens because 2.675 can't be represented exactly in binary floating-point
# The actual stored value is slightly less than 2.675

For most practical purposes, this isn't a problem. But if you're working with financial calculations where exact decimal arithmetic matters, you might need Python's decimal module (which we won't cover in this book, but it's worth knowing it exists).

Rounding for display vs calculation:

Often you want to round only for display, keeping full precision for calculations:

python
# rounding_for_display.py
price1 = 19.99
price2 = 15.49
tax_rate = 0.08
 
# Calculate with full precision
subtotal = price1 + price2
tax = subtotal * tax_rate
total = subtotal + tax
 
# Round only for display
print(f"Subtotal: ${round(subtotal, 2)}")  # Output: Subtotal: $35.48
print(f"Tax: ${round(tax, 2)}")            # Output: Tax: $2.84
print(f"Total: ${round(total, 2)}")        # Output: Total: $38.32
 
# The variables still contain full precision
print(f"Total (full precision): ${total}")
# Output: Total (full precision): $38.3184

The round() function is one of Python's most frequently used built-in functions. Use it whenever you need to present numeric results in a readable format or when you need to limit precision for a specific purpose.

4.8) Common Numeric Patterns (counters, totals, averages)

Now that you understand Python's numeric operations and functions, let's explore common patterns you'll use repeatedly in real programs. These patterns form the building blocks for processing numeric data.

4.8.1) Counters: Keeping Track of How Many

A counter is a variable that tracks how many times something happens. You initialize it to zero and increment it each time an event occurs:

python
# basic_counter.py
# Count how many numbers we've processed
count = 0  # Initialize counter
 
# Process first number
count += 1
print(f"Processed {count} number(s)")  # Output: Processed 1 number(s)
 
# Process second number
count += 1
print(f"Processed {count} number(s)")  # Output: Processed 2 number(s)
 
# Process third number
count += 1
print(f"Processed {count} number(s)")  # Output: Processed 3 number(s)

In real programs, you'd typically use counters inside loops (which we'll learn about in Chapters 10 and 11). For now, understand the pattern: start at zero, add one each time.

Counters can track different types of events:

python
# multiple_counters.py
# Track different categories
even_count = 0
odd_count = 0
 
# Check number 1
number = 4
if number % 2 == 0:
    even_count += 1
else:
    odd_count += 1
 
# Check number 2
number = 7
if number % 2 == 0:
    even_count += 1
else:
    odd_count += 1
 
# Check number 3
number = 10
if number % 2 == 0:
    even_count += 1
else:
    odd_count += 1
 
print(f"Even numbers: {even_count}")  # Output: Even numbers: 2
print(f"Odd numbers: {odd_count}")    # Output: Odd numbers: 1

4.8.2) Accumulators: Building Up Totals

An accumulator is a variable that collects a running total. Like a counter, you initialize it to zero, but instead of adding one each time, you add varying amounts:

python
# basic_accumulator.py
# Calculate total sales
total_sales = 0  # Initialize accumulator
 
# First sale
sale = 19.99
total_sales += sale
print(f"Total so far: ${total_sales}")  # Output: Total so far: $19.99
 
# Second sale
sale = 34.50
total_sales += sale
print(f"Total so far: ${total_sales}")  # Output: Total so far: $54.49
 
# Third sale
sale = 12.00
total_sales += sale
print(f"Total so far: ${total_sales}")  # Output: Total so far: $66.49

Accumulators are fundamental to data processing. They let you aggregate values as you process them:

python
# multiple_accumulators.py
# Track both total and count to calculate average later
total_score = 0
count = 0
 
# Process score 1
score = 85
total_score += score
count += 1
 
# Process score 2
score = 92
total_score += score
count += 1
 
# Process score 3
score = 78
total_score += score
count += 1
 
print(f"Total score: {total_score}")  # Output: Total score: 255
print(f"Number of scores: {count}")   # Output: Number of scores: 3

4.8.3) Calculating Averages

An average combines the counter and accumulator patterns: you need both a total (accumulator) and a count (counter), then divide:

python
# calculating_average.py
# Calculate average of test scores
total_score = 0
count = 0
 
# Add scores
total_score += 85
count += 1
 
total_score += 92
count += 1
 
total_score += 78
count += 1
 
total_score += 88
count += 1
 
# Calculate average
average = total_score / count
print(f"Average score: {average}")  # Output: Average score: 85.75
 
# Often you'll want to round the average
average_rounded = round(average, 2)
print(f"Average (rounded): {average_rounded}")  # Output: Average (rounded): 85.75

Important: Always check for division by zero:

When calculating averages, you must ensure the count isn't zero:

python
# safe_average.py
total_score = 0
count = 0
 
# If no scores were added, count is still 0
# Dividing by zero causes an error!
 
if count > 0:
    average = total_score / count
    print(f"Average: {average}")
else:
    print("No scores to average")
    # Output: No scores to average

We'll learn more about handling conditions like this in Chapter 8.

4.8.4) Finding Running Maximum and Minimum

Sometimes you need to track the largest or smallest value seen so far: You can use max() and min() functions to implement this:

python
# running_max_min_simplified.py
# Track highest and lowest using max() and min()
 
highest_temp = 72
lowest_temp = 72
 
# Update with new temperature
current_temp = 85
highest_temp = max(highest_temp, current_temp)
lowest_temp = min(lowest_temp, current_temp)
 
# Update with another temperature
current_temp = 68
highest_temp = max(highest_temp, current_temp)
lowest_temp = min(lowest_temp, current_temp)
 
print(f"High: {highest_temp}, Low: {lowest_temp}")
# Output: High: 85, Low: 68

4.9) Bitwise Operators for Integers: &, |, ^, <<, >> (Brief Overview)

Bitwise operators work on the individual bits (binary digits) of integers. While you won't use these operators in everyday programming as often as arithmetic operators, they're important for certain tasks like working with flags, permissions, low-level data manipulation, and performance optimization.

This section provides a brief overview. Understanding bitwise operations requires understanding binary representation, which is a deeper topic than we can fully explore in this introductory chapter.

4.9.1) Understanding Binary Representation (Quick Introduction)

Computers store integers as sequences of bits (0s and 1s). For example:

  • Decimal 5 is binary 101 (1×4 + 0×2 + 1×1)
  • Decimal 12 is binary 1100 (1×8 + 1×4 + 0×2 + 0×1)

Python's bin() function shows the binary representation of an integer:

python
# binary_representation.py
# See binary representation of integers
print(bin(5))   # Output: 0b101
print(bin(12))  # Output: 0b1100
print(bin(255)) # Output: 0b11111111
 
# The 0b prefix indicates binary notation

4.9.2) Bitwise AND (&)

The & operator performs a bitwise AND: each bit in the result is 1 only if the corresponding bits in both operands are 1:

python
# bitwise_and.py
# 12 is 1100 in binary
# 10 is 1010 in binary
result = 12 & 10
print(result)  # Output: 8
print(bin(result))  # Output: 0b1000
 
# How it works:
#   1100  (12)
# & 1010  (10)
# ------
#   1000  (8)

Bitwise AND is often used to check if specific bits are set (testing flags):

python
# checking_flags.py
# File permissions example (simplified)
READ = 4    # 100 in binary
WRITE = 2   # 010 in binary
EXECUTE = 1 # 001 in binary
 
permissions = 6  # 110 in binary (READ + WRITE)
 
# Check if READ permission is set
has_read = (permissions & READ) != 0
print(f"Has read: {has_read}")  # Output: Has read: True
 
# Check if EXECUTE permission is set
has_execute = (permissions & EXECUTE) != 0
print(f"Has execute: {has_execute}")  # Output: Has execute: False

4.9.3) Bitwise OR (|)

The | operator performs a bitwise OR: each bit in the result is 1 if the corresponding bit in either operand is 1:

python
# bitwise_or.py
# 12 is 1100 in binary
# 10 is 1010 in binary
result = 12 | 10
print(result)  # Output: 14
print(bin(result))  # Output: 0b1110
 
# How it works:
#   1100  (12)
# | 1010  (10)
# ------
#   1110  (14)

Bitwise OR is used to combine flags:

python
# combining_flags.py
READ = 4    # 100 in binary
WRITE = 2   # 010 in binary
EXECUTE = 1 # 001 in binary
 
# Grant READ and WRITE permissions
permissions = READ | WRITE
print(f"Permissions: {permissions}")  # Output: Permissions: 6
print(bin(permissions))  # Output: 0b110
 
# Add EXECUTE permission
permissions = permissions | EXECUTE
print(f"Permissions: {permissions}")  # Output: Permissions: 7
print(bin(permissions))  # Output: 0b111

4.9.4) Bitwise XOR (^)

The ^ operator performs a bitwise XOR (exclusive OR): each bit in the result is 1 if the corresponding bits in the operands are different:

python
# bitwise_xor.py
# 12 is 1100 in binary
# 10 is 1010 in binary
result = 12 ^ 10
print(result)  # Output: 6
print(bin(result))  # Output: 0b110
 
# How it works:
#   1100  (12)
# ^ 1010  (10)
# ------
#   0110  (6)

XOR has interesting properties, like toggling bits or swapping values:

python
# xor_properties.py
# XOR with itself gives 0
result = 5 ^ 5
print(result)  # Output: 0
 
# XOR with 0 gives the original number
result = 5 ^ 0
print(result)  # Output: 5
 
# XOR is its own inverse (useful for simple encryption)
original = 42
key = 123
encrypted = original ^ key
decrypted = encrypted ^ key
print(f"Original: {original}, Encrypted: {encrypted}, Decrypted: {decrypted}")
# Output: Original: 42, Encrypted: 81, Decrypted: 42

4.9.5) Left Shift (<<) and Right Shift (>>)

The << operator shifts bits to the left, and >> shifts bits to the right:

python
# bit_shifting.py
# Left shift: multiply by powers of 2
result = 5 << 1  # Shift left by 1 bit
print(result)  # Output: 10
# 5 is 101 in binary, shifted left becomes 1010 (10)
 
result = 5 << 2  # Shift left by 2 bits
print(result)  # Output: 20
# 5 is 101 in binary, shifted left becomes 10100 (20)
 
# Right shift: divide by powers of 2 (floor division)
result = 20 >> 1  # Shift right by 1 bit
print(result)  # Output: 10
# 20 is 10100 in binary, shifted right becomes 1010 (10)
 
result = 20 >> 2  # Shift right by 2 bits
print(result)  # Output: 5
# 20 is 10100 in binary, shifted right becomes 101 (5)

Shifting left by n bits multiplies by 2^n, and shifting right divides by 2^n (floor division):

python
# shift_as_multiplication.py
# Left shift multiplies by powers of 2
print(3 << 1)  # Output: 6 (3 * 2^1 = 3 * 2)
print(3 << 2)  # Output: 12 (3 * 2^2 = 3 * 4)
print(3 << 3)  # Output: 24 (3 * 2^3 = 3 * 8)
 
# Right shift divides by powers of 2
print(24 >> 1)  # Output: 12 (24 // 2^1 = 24 // 2)
print(24 >> 2)  # Output: 6 (24 // 2^2 = 24 // 4)
print(24 >> 3)  # Output: 3 (24 // 2^3 = 24 // 8)

4.9.6) When to Use Bitwise Operators

Bitwise operators are useful in specific scenarios:

  • Working with binary flags and permissions (like file permissions in Unix/Linux)
  • Low-level data manipulation (packing multiple values into a single integer)
  • Network programming (manipulating IP addresses, protocol flags)
  • Graphics and game programming (color manipulation, collision detection)
  • Performance optimization (bit shifting is faster than multiplication/division by powers of 2)

For most everyday programming tasks, you won't need bitwise operators. But when you do need them—especially when working with systems programming, networking, or performance-critical code—they're invaluable tools.

Note: This overview covers the basics. Bitwise operations become more complex with negative numbers (involving two's complement representation), which is beyond the scope of this introduction. For now, focus on understanding that these operators exist and what they do at a high level.

4.10) Floating-Point Precision and Rounding Errors (Simple Explanation)

Floating-point numbers in Python (and in most programming languages) can represent a vast range of values, from tiny fractions to enormous numbers. However, they have a limitation that can surprise beginners: they can't represent all decimal numbers exactly. This section explains why this happens and what it means for your programs.

4.10.1) Why Floating-Point Numbers Aren't Always Exact

Computers store floating-point numbers in binary (base 2), not decimal (base 10). Some decimal fractions that seem simple to us can't be represented exactly in binary, just as 1/3 can't be represented exactly in decimal (0.333333... goes on forever).

Here's a surprising example:

python
# floating_point_surprise.py
# This seems like it should be exactly 0.3
result = 0.1 + 0.2
print(result)  # Output: 0.30000000000000004 (not exactly 0.3!)
 
# Check if it equals 0.3
print(result == 0.3)  # Output: False

This isn't a bug in Python—it's a fundamental limitation of how computers represent floating-point numbers. The decimal number 0.1 can't be represented exactly in binary floating-point, just as 1/3 can't be represented exactly in decimal.

4.10.2) Understanding the Representation Issue

Let's see more examples of this behavior:

python
# more_precision_examples.py
# Simple decimal values that aren't exact in binary
print(0.1)  # Output: 0.1 (Python rounds for display)
print(repr(0.1))  # Output: 0.1 (still rounded)
 
# But the actual stored value has tiny errors
print(0.1 + 0.1 + 0.1)  # Output: 0.30000000000000004
 
# Multiplication can accumulate these errors
result = 0.1 * 3
print(result)  # Output: 0.30000000000000004
 
# Some numbers are exact (powers of 2)
print(0.5)  # Output: 0.5 (exact)
print(0.25)  # Output: 0.25 (exact)
print(0.125)  # Output: 0.125 (exact)

Numbers that are powers of 2 (like 0.5, 0.25, 0.125) or sums of powers of 2 can be represented exactly. But most decimal fractions cannot.

4.10.3) Practical Implications

For most everyday programming, these tiny errors don't matter. But there are situations where you need to be aware of them:

Comparing floating-point numbers:

python
# comparing_floats.py
# Direct equality comparison can fail
a = 0.1 + 0.2
b = 0.3
 
print(a == b)  # Output: False (due to tiny difference)
 
# Better: check if they're close enough
difference = abs(a - b)
tolerance = 0.0001  # How close is "close enough"?
 
print(difference < tolerance)  # Output: True (they're close enough)
 
# Python 3.5+ provides math.isclose() for this (we'll see in section 4.11)

Accumulating errors in repeated calculations:

python
# accumulated_errors.py
# Adding 0.1 ten times
total = 0.0
for i in range(10):
    total += 0.1
 
print(total)  # Output: 0.9999999999999999 (not exactly 1.0)
print(total == 1.0)  # Output: False
 
# The error accumulates with each addition

Financial calculations:

python
# financial_calculations.py
# Money calculations can have surprising results
price = 0.10
quantity = 3
total = price * quantity
 
print(total)  # Output: 0.30000000000000004
 
# For financial calculations, consider rounding to cents
total_cents = round(total * 100)  # Convert to cents
total_dollars = total_cents / 100
print(total_dollars)  # Output: 0.3
 
# Or use Python's decimal module for exact decimal arithmetic
# (beyond the scope of this chapter, but worth knowing about)

4.10.4) Strategies for Working with Floating-Point Numbers

Round results for display:

python
# rounding_for_display.py
result = 0.1 + 0.2
print(f"Result: {round(result, 2)}")  # Output: Result: 0.3
 
# Or use formatted output (we'll learn more in Chapter 6)
print(f"Result: {result:.2f}")  # Output: Result: 0.30

Don't compare floats for exact equality:

python
# safe_float_comparison.py
a = 0.1 + 0.2
b = 0.3
 
# Instead of: if a == b:
# Use: if they're close enough
if abs(a - b) < 0.0001:
    print("Close enough to equal")
# Output: Close enough to equal

Be aware when precision matters:

For scientific computing, financial calculations, or any domain where exact decimal arithmetic is crucial, you might need:

  • Python's decimal module for exact decimal arithmetic
  • Python's fractions module for exact rational arithmetic
  • Careful rounding at appropriate points in your calculations

These modules are beyond the scope of this chapter, but knowing they exist is valuable.

4.10.5) When Floating-Point Precision Doesn't Matter

For most everyday programming tasks, floating-point precision is more than adequate:

python
# when_precision_is_fine.py
# Calculating area
length = 5.5
width = 3.2
area = length * width
print(f"Area: {area:.2f} square meters")  # Output: Area: 17.60 square meters
 
# Converting temperature
fahrenheit = 98.6
celsius = (fahrenheit - 32) * 5 / 9
print(f"Temperature: {celsius:.1f}°C")  # Output: Temperature: 37.0°C
 
# Calculating average
total = 456.78
count = 7
average = total / count
print(f"Average: {average:.2f}")  # Output: Average: 65.25

When you round results for display or when tiny errors are insignificant compared to your measurement precision, floating-point arithmetic works perfectly well.

4.10.6) The Key Takeaway

Floating-point numbers are approximations of real numbers. For most programming tasks, they're accurate enough. But remember:

  1. Don't compare floats for exact equality—check if they're close enough instead
  2. Round results when displaying to avoid showing meaningless precision
  3. Be aware of accumulating errors in repeated calculations
  4. For financial calculations, consider rounding to appropriate precision or using the decimal module

Understanding these limitations helps you write more robust programs and avoid surprising bugs. The good news is that for the vast majority of programming tasks, floating-point arithmetic "just works" if you follow these simple guidelines.

4.11) (Optional) Basic Mathematical Functions with the math Module (sqrt, floor, ceil, pi)

Python's built-in operators and functions cover basic arithmetic, but for more advanced mathematical operations, Python provides the math module. A module is a collection of related functions and values that you can use in your programs. We'll learn much more about modules in Chapter 22, but for now, we'll introduce the basics you need to use the math module.

4.11.1) Importing the math Module

To use the math module, you need to import it at the beginning of your program:

python
# importing_math.py
import math
 
# Now you can use functions from the math module
# by writing math.function_name()

When you import a module, you gain access to all its functions and values. You use them by writing the module name, a dot, and then the function or value name.

4.11.2) Mathematical Constants: pi and e

The math module provides precise values for important mathematical constants:

python
# math_constants.py
import math
 
# Pi (π): ratio of circle's circumference to diameter
print(math.pi)  # Output: 3.141592653589793
 
# Euler's number (e): base of natural logarithms
print(math.e)  # Output: 2.718281828459045
 
# Using pi to calculate circle properties
radius = 5
circumference = 2 * math.pi * radius
area = math.pi * radius ** 2
 
print(f"Circumference: {circumference:.2f}")  # Output: Circumference: 31.42
print(f"Area: {area:.2f}")  # Output: Area: 78.54

4.11.3) Square Root with sqrt()

The sqrt() function calculates the square root of a number:

python
# square_root.py
import math
 
# Square root of perfect squares
result = math.sqrt(16)
print(result)  # Output: 4.0
 
result = math.sqrt(25)
print(result)  # Output: 5.0
 
# Square root of non-perfect squares
result = math.sqrt(2)
print(result)  # Output: 1.4142135623730951
 
# Using sqrt in calculations
# Distance formula: sqrt((x2-x1)^2 + (y2-y1)^2)
x1, y1 = 3, 4
x2, y2 = 6, 8
 
distance = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
print(f"Distance: {distance}")  # Output: Distance: 5.0

Note that sqrt() returns a float, even for perfect squares. Also, you can't take the square root of negative numbers with sqrt() (it will raise an error).

4.11.4) Floor and Ceiling with floor() and ceil()

The floor() function rounds down to the nearest integer (toward negative infinity), and ceil() rounds up (toward positive infinity):

python
# floor_and_ceil.py
import math
 
# Floor: rounds down
result = math.floor(3.7)
print(result)  # Output: 3
 
result = math.floor(3.2)
print(result)  # Output: 3
 
# Ceiling: rounds up
result = math.ceil(3.7)
print(result)  # Output: 4
 
result = math.ceil(3.2)
print(result)  # Output: 4
 
# With negative numbers
result = math.floor(-3.7)
print(result)  # Output: -4 (rounds toward negative infinity)
 
result = math.ceil(-3.7)
print(result)  # Output: -3 (rounds toward positive infinity)

These functions are useful when you need to ensure a value is within a certain range or when you need integer results with specific rounding behavior:

python
# practical_floor_ceil.py
import math
 
# How many boxes needed to pack all items?
items = 47
items_per_box = 12
 
# Use ceil to round up (need a full box even if not completely filled)
boxes_needed = math.ceil(items / items_per_box)
print(f"Boxes needed: {boxes_needed}")  # Output: Boxes needed: 4
 
# How many complete pages for a document?
total_lines = 250
lines_per_page = 30
 
# Use floor to count only complete pages
complete_pages = math.floor(total_lines / lines_per_page)
print(f"Complete pages: {complete_pages}")  # Output: Complete pages: 8

4.11.5) Comparing floor(), ceil(), round(), and int()

It's helpful to understand how these different rounding approaches differ:

python
# comparing_rounding_methods.py
import math
 
value = 3.7
 
print(f"Original: {value}")
print(f"floor(): {math.floor(value)}")  # Output: floor(): 3
print(f"ceil(): {math.ceil(value)}")    # Output: ceil(): 4
print(f"round(): {round(value)}")       # Output: round(): 4
print(f"int(): {int(value)}")           # Output: int(): 3
 
# With negative numbers, the differences are more apparent
value = -3.7
 
print(f"\nOriginal: {value}")
print(f"floor(): {math.floor(value)}")  # Output: floor(): -4
print(f"ceil(): {math.ceil(value)}")    # Output: ceil(): -3
print(f"round(): {round(value)}")       # Output: round(): -4
print(f"int(): {int(value)}")           # Output: int(): -3

Here's a visual representation of how these functions behave:

Value: 3.7

floor: 3
rounds down

ceil: 4
rounds up

round: 4
rounds to nearest

int: 3
truncates toward zero

Value: -3.7

floor: -4
rounds down toward -∞

ceil: -3
rounds up toward +∞

round: -4
rounds to nearest

int: -3
truncates toward zero

4.11.6) Other Useful math Module Functions

The math module contains many other functions. Here are a few more that are commonly useful:

python
# other_math_functions.py
import math
 
# Absolute value (also available as built-in abs())
result = math.fabs(-5.5)
print(result)  # Output: 5.5
 
# Power function (also available as ** operator)
result = math.pow(2, 3)
print(result)  # Output: 8.0
 
# Trigonometric functions (angles in radians)
result = math.sin(math.pi / 2)  # sin(90 degrees)
print(result)  # Output: 1.0
 
result = math.cos(0)  # cos(0 degrees)
print(result)  # Output: 1.0
 
# Logarithms
result = math.log(math.e)  # Natural log (base e)
print(result)  # Output: 1.0
 
result = math.log10(100)  # Log base 10
print(result)  # Output: 2.0
 
# Check if a float is close to another (Python 3.5+)
a = 0.1 + 0.2
b = 0.3
result = math.isclose(a, b)
print(result)  # Output: True

The isclose() function is particularly useful for comparing floating-point numbers (as we discussed in section 4.10):

python
# isclose_example.py
import math
 
# Instead of direct equality comparison
a = 0.1 + 0.2
b = 0.3
 
# Don't do this:
# if a == b:  # This would be False
 
# Do this instead:
if math.isclose(a, b):
    print("Values are close enough")
# Output: Values are close enough
 
# You can specify the tolerance
if math.isclose(a, b, rel_tol=1e-9):  # Very strict tolerance
    print("Values are very close")
# Output: Values are very close

4.11.7) When to Use the math Module

Use the math module when you need:

  • Mathematical constants (π, e)
  • Square roots and other roots
  • Trigonometric functions (sin, cos, tan)
  • Logarithmic and exponential functions
  • Precise rounding control (floor, ceil)
  • Floating-point comparison (isclose)

For basic arithmetic (+, -, *, /, //, %, **), use Python's built-in operators. For more advanced mathematical operations, import and use the math module.

The math module is part of Python's standard library, meaning it's always available—you just need to import it. We'll explore more modules and learn about the import system in detail in Chapter 22.


In this chapter, you've learned how to work with numbers in Python: performing arithmetic operations, understanding different types of division, using operator precedence, mixing integers and floats, employing built-in numeric functions, recognizing common numeric patterns, working with bitwise operators, understanding floating-point precision, and using the math module for advanced operations.

These numeric operations form the foundation for countless programming tasks, from simple calculations to complex data analysis. As you continue learning Python, you'll use these operations constantly, often in combination with the control flow structures and data collections you'll learn in upcoming chapters.

Practice these operations until they become second nature. Try writing small programs that calculate real-world quantities: convert temperatures, calculate areas and volumes, process financial data, or analyze measurements. The more you practice, the more comfortable you'll become with Python's numeric capabilities.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai