Python Numbers and Mathematical Operations

Week 2: Python Fundamentals - Working with Numbers

Session Overview

Welcome to our exploration of numbers and mathematical operations in Python! Today, we'll dive deep into how Python handles various numeric types and the rich set of mathematical operations it provides. Whether you're doing simple calculations or complex scientific computing, understanding Python's numeric capabilities is essential for effective programming.

Python Numeric Types

Python provides several built-in numeric types to handle different kinds of numbers and mathematical operations.

Integers (int)

Integers represent whole numbers without a fractional part. In Python, integers have unlimited precision, meaning they can be as large as your computer's memory allows.

# Basic integer literals
a = 10
b = -25
c = 0

# Large integers - Python handles these effortlessly!
really_big = 123456789012345678901234567890
print(really_big + 1)  # No overflow!

# Integer literals can use underscores for readability
million = 1_000_000
billion = 1_000_000_000
print(billion - million)  # 999000000

# Binary, octal, and hexadecimal literals
binary = 0b1010      # Binary (base 2) - evaluates to 10
octal = 0o17         # Octal (base 8) - evaluates to 15
hexadecimal = 0xFF   # Hexadecimal (base 16) - evaluates to 255

Analogy: Integers as Building Blocks

Think of integers like building blocks that can only be stacked as whole units:

  • You can have 1, 2, 3, ... blocks, but never half a block
  • You can stack them as high as you want (unlimited precision)
  • They can be positive (stacked above ground) or negative (below ground)
  • They're perfect for counting discrete objects or iterations

Floating-Point Numbers (float)

Floating-point numbers represent real numbers with a fractional part. They follow the IEEE 754 standard and have limited precision.

# Basic float literals
x = 3.14
y = -0.5
z = 2.0  # Still a float, even with zero decimal

# Scientific notation
avogadro = 6.022e23  # 6.022 × 10²³
tiny = 1.6e-35      # 1.6 × 10⁻³⁵

# Float precision limitations
result = 0.1 + 0.2
print(result)  # 0.30000000000000004, not exactly 0.3!
print(result == 0.3)  # False - beware of float comparisons!

# Proper way to compare floats
import math
print(math.isclose(result, 0.3))  # True

Analogy: Floats as Measuring Tape

Think of floating-point numbers like a measuring tape:

  • They can measure continuous quantities (length, time, probability)
  • They have limited precision (just as real measuring tapes have small markings)
  • The precision decreases for very large or very small measurements
  • Two measurements might look the same to the naked eye but have tiny differences

Complex Numbers (complex)

Complex numbers have a real and imaginary part, and are useful in many scientific and engineering fields.

# Complex number literals
c1 = 3 + 4j  # Real part 3, imaginary part 4
c2 = 2 - 3j  # Real part 2, imaginary part -3

# Creating from real and imaginary parts
c3 = complex(2, 5)  # 2 + 5j

# Accessing real and imaginary parts
print(c1.real)  # 3.0
print(c1.imag)  # 4.0

# Complex arithmetic
sum_c = c1 + c2  # (5+1j)
prod_c = c1 * c2  # (18-1j)

# Absolute value (magnitude)
mag = abs(c1)  # 5.0 (√(3² + 4²))

Decimal and Fraction Types

For applications requiring exact decimal representation or rational numbers, Python provides specialized types in the standard library.

# Decimal type for precise decimal arithmetic
from decimal import Decimal, getcontext

# Set precision
getcontext().prec = 28

# Exact decimal representation
d1 = Decimal('0.1')
d2 = Decimal('0.2')
d_sum = d1 + d2
print(d_sum)  # 0.3 exactly!
print(d_sum == Decimal('0.3'))  # True

# Fraction type for rational numbers
from fractions import Fraction

# Create fractions
f1 = Fraction(1, 3)  # 1/3
f2 = Fraction(2, 5)  # 2/5
f_sum = f1 + f2      # 11/15
print(f_sum)

# Convert float to fraction
print(Fraction(0.5))  # 1/2
print(Fraction(1.25))  # 5/4

These specialized numeric types ensure accuracy for financial calculations, scientific computing, and other applications where precision is critical.

Converting Between Numeric Types

Python provides built-in functions to convert between different numeric types:

# Converting to integers
i1 = int(5.7)     # 5 (truncates, doesn't round)
i2 = int(-3.2)    # -3
i3 = int("42")    # 42
i4 = int("101", 2)  # 5 (converts binary "101" to integer)

# Converting to floats
f1 = float(42)     # 42.0
f2 = float("3.14")  # 3.14
f3 = float("-infinity")  # -inf

# Converting to complex
c1 = complex(3)      # 3+0j
c2 = complex(2, -4)  # 2-4j
c3 = complex("3+4j") # 3+4j

# Converting between Decimal, Fraction, and built-in types
from decimal import Decimal
from fractions import Fraction

d = Decimal("3.14")
f = Fraction(314, 100)

# To built-in types
float_d = float(d)  # 3.14
int_f = int(f)      # 3 (truncated)

# Between special types
frac_from_decimal = Fraction(d)  # 157/50
dec_from_fraction = Decimal(f.numerator) / Decimal(f.denominator)  # 3.14

It's important to understand the implications of these conversions, especially when precision matters:

Basic Mathematical Operations

Arithmetic Operators

Python provides all standard arithmetic operators:

# Addition
sum_result = 5 + 3  # 8

# Subtraction
diff_result = 10 - 4  # 6

# Multiplication
product = 6 * 7  # 42

# Division (always returns float)
quotient = 12 / 4  # 3.0

# Integer Division (floors the result)
int_quotient = 13 // 5  # 2

# Modulus (remainder)
remainder = 13 % 5  # 3

# Negative numbers in modulo operations
neg_remainder = -13 % 5  # 2 (result is always < divisor and ≥ 0)

# Exponentiation
power = 2 ** 3  # 8
large_power = 10 ** 20  # 100000000000000000000

Operation Order (Precedence)

Python follows the standard mathematical order of operations:

# Order of operations: PEMDAS (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction)
result1 = 2 + 3 * 4        # 14 (not 20)
result2 = (2 + 3) * 4      # 20
result3 = 2 ** 3 * 2       # 16 (2³ * 2, not 2 * 2³)
result4 = 2 ** (3 * 2)     # 64 (2⁶)
result5 = 20 / 4 / 2       # 2.5 (left-to-right: (20/4)/2)

Assignment Operators

Python provides compound assignment operators for concise updates:

x = 10

# Addition assignment
x += 5  # x = x + 5, now x is 15

# Subtraction assignment
x -= 3  # x = x - 3, now x is 12

# Multiplication assignment
x *= 2  # x = x * 2, now x is 24

# Division assignment
x /= 4  # x = x / 4, now x is 6.0 (becomes float!)

# Integer division assignment
x //= 2  # x = x // 2, now x is 3.0 (still float from earlier division)

# Modulus assignment
y = 10
y %= 3  # y = y % 3, now y is 1

# Exponentiation assignment
y **= 3  # y = y ** 3, now y is 1 (1³ = 1)

Advanced Mathematical Operations

Functions from the Math Module

The math module provides advanced mathematical functions:

import math

# Constants
print(math.pi)       # 3.141592653589793
print(math.e)        # 2.718281828459045
print(math.inf)      # Positive infinity
print(math.nan)      # Not a Number

# Rounding functions
print(math.ceil(4.2))   # 5 (ceiling - rounds up)
print(math.floor(4.8))  # 4 (floor - rounds down)
print(round(4.5))       # 4 (built-in round function - to nearest even integer for ties)
print(math.trunc(4.8))  # 4 (truncate - removes decimal part)

# Powers and logarithms
print(math.sqrt(25))          # 5.0 (square root)
print(math.pow(2, 3))         # 8.0 (exponentiation, returns float)
print(math.log(100, 10))      # 2.0 (logarithm base 10)
print(math.log2(8))           # 3.0 (logarithm base 2)
print(math.log10(100))        # 2.0 (logarithm base 10)
print(math.exp(2))            # 7.38905609893065 (e²)

# Trigonometric functions (angles in radians)
print(math.sin(math.pi/2))    # 1.0
print(math.cos(math.pi))      # -1.0
print(math.tan(math.pi/4))    # 0.9999999999999999 (very close to 1)
print(math.degrees(math.pi))  # 180.0 (convert radians to degrees)
print(math.radians(180))      # 3.141592653589793 (convert degrees to radians)

# Hyperbolic functions
print(math.sinh(1))           # 1.1752011936438014
print(math.cosh(1))           # 1.5430806348152437
print(math.tanh(1))           # 0.7615941559557649

# Special functions
print(math.factorial(5))      # 120 (5!)
print(math.gcd(24, 36))       # 12 (greatest common divisor)
print(math.isclose(0.1+0.2, 0.3))  # True (compare floats with tolerance)

Working with Complex Numbers

For complex number operations, Python offers the cmath module:

import cmath

# Complex number operations
z = 3 + 4j
print(cmath.phase(z))       # 0.9272952180016122 (angle in radians)
print(cmath.polar(z))       # (5.0, 0.9272952180016122) (magnitude and phase)
print(cmath.rect(5, 0.927)) # (2.9997455122896188+4.000494110703771j) (from polar coordinates)

# Complex versions of standard functions
print(cmath.sqrt(-1))       # 1j (square root of negative number)
print(cmath.exp(1j * cmath.pi))  # (-1+1.2246467991473532e-16j) (Euler's formula: e^(iπ) ≈ -1)
print(cmath.log(1j))        # (0+1.5707963267948966j) (natural logarithm)

# Trigonometric functions with complex arguments
print(cmath.sin(1j))        # (0+1.1752011936438014j)

Statistical Operations

The statistics module provides functions for statistical calculations:

import statistics

# Sample data
data = [2, 5, 7, 9, 11, 13, 14, 16, 18]

# Basic statistics
print(statistics.mean(data))      # 10.555555555555555 (average)
print(statistics.median(data))    # 11 (middle value)
print(statistics.mode([1, 2, 2, 3, 3, 3, 4]))  # 3 (most common value)

# Measures of spread
print(statistics.stdev(data))     # 5.385164807134504 (standard deviation - sample)
print(statistics.pstdev(data))    # 5.050863356622927 (standard deviation - population)
print(statistics.variance(data))  # 29.00000000000001 (variance - sample)

# Quartiles and percentiles
print(statistics.quantiles(data))  # [7.0, 11.0, 16.0] (quartiles)

# Normal distribution
from statistics import NormalDist
dist = NormalDist(mu=100, sigma=15)  # Normal distribution with mean 100 and std dev 15
print(dist.cdf(115))  # 0.8413447460685429 (probability X ≤ 115)
print(dist.pdf(100))  # 0.026596552261351526 (probability density at x=100)

Advanced Statistical Analysis

For more advanced statistical operations, consider using NumPy and SciPy libraries:

# Example using NumPy (requires installation: pip install numpy)
import numpy as np

# Create an array
arr = np.array([2, 5, 7, 9, 11, 13, 14, 16, 18])

# Statistics
print(np.mean(arr))       # 10.555555555555555
print(np.median(arr))     # 11.0
print(np.std(arr))        # 5.050863356622927
print(np.percentile(arr, [25, 50, 75]))  # [ 7. 11. 16.]

# Linear algebra
mat1 = np.array([[1, 2], [3, 4]])
mat2 = np.array([[5, 6], [7, 8]])
print(np.dot(mat1, mat2))  # Matrix multiplication: [[19 22] [43 50]]
print(np.linalg.det(mat1))  # Determinant: -2.0000000000000004
print(np.linalg.inv(mat1))  # Inverse: [[-2.   1. ] [ 1.5 -0.5]]

Number Theory Operations

Python provides several functions for number theory operations, especially useful in cryptography, algorithms, and theoretical mathematics:

import math

# Prime number testing (basic implementation)
def is_prime(n):
    """Check if a number is prime (naive implementation)."""
    if n <= 1:
        return False
    if n <= 3:
        return True
    if n % 2 == 0 or n % 3 == 0:
        return False
    i = 5
    while i * i <= n:
        if n % i == 0 or n % (i + 2) == 0:
            return False
        i += 6
    return True

# Test some numbers
for num in [2, 3, 4, 17, 20, 97]:
    print(f"{num} is prime: {is_prime(num)}")

# Greatest Common Divisor (GCD)
print(math.gcd(48, 60))  # 12

# Least Common Multiple (LCM)
def lcm(a, b):
    """Calculate the Least Common Multiple of two numbers."""
    return abs(a * b) // math.gcd(a, b)

print(lcm(8, 12))  # 24

# Factorial
print(math.factorial(5))  # 120 (5!)

# Calculate the nth Fibonacci number (recursive with memoization)
fibonacci_cache = {}
def fibonacci(n):
    """Return the nth Fibonacci number."""
    if n in fibonacci_cache:
        return fibonacci_cache[n]
    
    if n <= 1:
        result = n
    else:
        result = fibonacci(n-1) + fibonacci(n-2)
    
    fibonacci_cache[n] = result
    return result

# Print first 10 Fibonacci numbers
for i in range(10):
    print(f"Fibonacci({i}) = {fibonacci(i)}")

For more advanced number theory operations, consider the sympy library, which provides functions for symbolic mathematics, including primality testing, factorization, and more.

Random Numbers

Generating random numbers is essential for simulations, games, cryptography, and many other applications. Python provides the random module for this purpose:

import random

# Set a seed for reproducibility
random.seed(42)

# Random float between 0 and 1
rand_float = random.random()
print(rand_float)  # 0.6394267984578837

# Random float within a range
rand_range = random.uniform(10, 20)
print(rand_range)  # 10.236557064081125

# Random integer within a range (inclusive)
rand_int = random.randint(1, 10)
print(rand_int)  # 10

# Random integer within a range (exclusive upper bound)
rand_range_int = random.randrange(1, 10)
print(rand_range_int)  # 9

# Random choice from a sequence
colors = ['red', 'green', 'blue', 'yellow']
rand_color = random.choice(colors)
print(rand_color)  # 'blue'

# Random sample from a sequence (without replacement)
rand_sample = random.sample(colors, 2)
print(rand_sample)  # ['blue', 'red']

# Random sample with replacement
rand_choices = random.choices(colors, k=3)
print(rand_choices)  # ['green', 'yellow', 'green']

# Shuffle a list in place
deck = list(range(1, 11))
random.shuffle(deck)
print(deck)  # [10, 1, 3, 5, 7, 9, 2, 6, 8, 4]

# For cryptographic applications, use secrets module
import secrets
crypto_random = secrets.randbelow(100)  # Random int in range(0, 100)
print(crypto_random)
token = secrets.token_hex(16)  # Random hex string (32 characters)
print(token)

Financial Mathematics

Python is widely used for financial calculations. Here are some common financial math operations:

# Simple interest calculation
def simple_interest(principal, rate, time):
    """
    Calculate simple interest.
    
    Args:
        principal: Principal amount
        rate: Annual interest rate (as a decimal)
        time: Time in years
    
    Returns:
        The interest earned
    """
    return principal * rate * time

# Compound interest calculation
def compound_interest(principal, rate, time, n=1):
    """
    Calculate compound interest.
    
    Args:
        principal: Principal amount
        rate: Annual interest rate (as a decimal)
        time: Time in years
        n: Number of times interest is compounded per year
    
    Returns:
        The final amount after compound interest
    """
    return principal * (1 + rate/n)**(n*time)

# Example usage
initial_investment = 1000
annual_rate = 0.05  # 5%
years = 10

# Calculate using both methods
si = simple_interest(initial_investment, annual_rate, years)
ci = compound_interest(initial_investment, annual_rate, years) - initial_investment

print(f"Simple interest: ${si:.2f}")     # $500.00
print(f"Compound interest: ${ci:.2f}")   # $628.89

# Present Value calculation
def present_value(future_value, rate, time):
    """Calculate the present value of a future sum."""
    return future_value / (1 + rate)**time

# Future Value calculation
def future_value(present_value, rate, time):
    """Calculate the future value of a present sum."""
    return present_value * (1 + rate)**time

# Loan payment calculation
def loan_payment(principal, rate, years):
    """Calculate monthly payment for a loan."""
    monthly_rate = rate / 12
    num_payments = years * 12
    return principal * (monthly_rate * (1 + monthly_rate)**num_payments) / ((1 + monthly_rate)**num_payments - 1)

# Example loan calculation
loan_amount = 200000  # $200,000 loan
annual_interest = 0.04  # 4% interest
loan_term = 30  # 30 years

monthly_payment = loan_payment(loan_amount, annual_interest, loan_term)
print(f"Monthly payment: ${monthly_payment:.2f}")  # $954.83

For more complex financial calculations, consider using specialized libraries like numpy-financial or pyfinance.

Practical Examples

Temperature Converter

def celsius_to_fahrenheit(celsius):
    """Convert temperature from Celsius to Fahrenheit."""
    return (celsius * 9/5) + 32

def fahrenheit_to_celsius(fahrenheit):
    """Convert temperature from Fahrenheit to Celsius."""
    return (fahrenheit - 32) * 5/9

# Example conversions
temp_c = 25
temp_f = celsius_to_fahrenheit(temp_c)
print(f"{temp_c}°C = {temp_f:.1f}°F")  # 25°C = 77.0°F

temp_f = 98.6
temp_c = fahrenheit_to_celsius(temp_f)
print(f"{temp_f}°F = {temp_c:.1f}°C")  # 98.6°F = 37.0°C

Simple Calculator

def calculator():
    """A simple command-line calculator."""
    print("Simple Calculator")
    print("Operations: +, -, *, /, ^ (power), % (modulo)")
    print("Enter 'q' to quit")
    
    while True:
        # Get user input
        expression = input("\nEnter expression (e.g., 2 + 3): ")
        
        if expression.lower() == 'q':
            print("Calculator closed.")
            break
        
        # Parse the expression
        try:
            # Split the expression into parts
            parts = expression.split()
            if len(parts) != 3:
                print("Invalid format. Please use 'number operator number'")
                continue
                
            num1 = float(parts[0])
            operator = parts[1]
            num2 = float(parts[2])
            
            # Perform the calculation
            if operator == '+':
                result = num1 + num2
            elif operator == '-':
                result = num1 - num2
            elif operator == '*':
                result = num1 * num2
            elif operator == '/':
                if num2 == 0:
                    print("Error: Division by zero")
                    continue
                result = num1 / num2
            elif operator == '^':
                result = num1 ** num2
            elif operator == '%':
                result = num1 % num2
            else:
                print(f"Unknown operator: {operator}")
                continue
                
            print(f"Result: {result}")
            
        except ValueError:
            print("Invalid numbers. Please enter valid numbers.")
        except Exception as e:
            print(f"An error occurred: {e}")

# Uncomment to run the calculator
# calculator()

Monte Carlo Pi Approximation

A classic example of using random numbers to approximate Pi:

import random
import math

def estimate_pi(num_points):
    """
    Estimate Pi using Monte Carlo method.
    
    Args:
        num_points: Number of random points to generate
    
    Returns:
        An approximation of Pi
    """
    points_in_circle = 0
    
    for _ in range(num_points):
        # Generate random point in the square [-1,1] x [-1,1]
        x = random.uniform(-1, 1)
        y = random.uniform(-1, 1)
        
        # Check if the point is in the unit circle
        if x**2 + y**2 <= 1:
            points_in_circle += 1
    
    # Area of circle / Area of square = pi/4
    return 4 * points_in_circle / num_points

# Estimate Pi with different numbers of points
for points in [1000, 10000, 100000, 1000000]:
    pi_estimate = estimate_pi(points)
    error = abs(pi_estimate - math.pi)
    print(f"Pi estimate with {points} points: {pi_estimate:.6f} (error: {error:.6f})")

Fibonacci Sequence Generator

def fibonacci_sequence(n):
    """
    Generate the first n Fibonacci numbers.
    
    Args:
        n: Number of Fibonacci numbers to generate
    
    Returns:
        A list of the first n Fibonacci numbers
    """
    sequence = []
    a, b = 0, 1
    
    for _ in range(n):
        sequence.append(a)
        a, b = b, a + b
        
    return sequence

# Generate and print the first 15 Fibonacci numbers
fib_numbers = fibonacci_sequence(15)
print(fib_numbers)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]

# Print with their golden ratio approximations
print("\nFibonacci numbers and their ratios:")
for i in range(1, len(fib_numbers)):
    if fib_numbers[i-1] > 0:  # Avoid division by zero
        ratio = fib_numbers[i] / fib_numbers[i-1]
        print(f"F({i+1})/F({i}) = {fib_numbers[i]}/{fib_numbers[i-1]} = {ratio:.8f}")

Common Pitfalls and Best Practices

Floating-Point Precision Issues

# Floating-point arithmetic isn't always exact
a = 0.1 + 0.2
print(a)  # 0.30000000000000004
print(a == 0.3)  # False

# Solutions:
# 1. Use math.isclose for comparison
import math
print(math.isclose(a, 0.3))  # True

# 2. Round to a specific precision
print(round(a, 10) == round(0.3, 10))  # True

# 3. Use Decimal for exact decimal arithmetic
from decimal import Decimal
x = Decimal('0.1') + Decimal('0.2')
print(x)  # 0.3
print(x == Decimal('0.3'))  # True

Integer Division vs. Float Division

# Integer division truncates in Python 3.x
result1 = 5 / 2    # 2.5 (float division)
result2 = 5 // 2   # 2 (integer division)

# Unexpected results with negative numbers
print(-7 // 3)  # -3 (not -2, integer division rounds toward negative infinity)
print(-7 % 3)   # 2 (ensures that (n // d) * d + (n % d) == n)

Overflow and Underflow

# Python integers don't overflow, but they can use a lot of memory
really_big = 2 ** 1000
print(really_big)  # A 302-digit number!

# Float overflow becomes inf
x = float('inf')
print(x)  # inf
print(x > 10**308)  # True

# Float underflow becomes 0.0
y = 10**-323
print(y)  # 0.0 (underflowed)

NaN Behavior

# Not a Number (NaN) has unusual behavior
import math
nan = float('nan')

print(nan == nan)  # False!
print(nan != nan)  # True!

# Proper way to detect NaN
print(math.isnan(nan))  # True

Performance Considerations

import time

# Inefficient way to calculate sum of numbers
def slow_sum(n):
    total = 0
    for i in range(1, n+1):
        total += i
    return total

# Efficient mathematical formula
def fast_sum(n):
    return n * (n + 1) // 2

# Compare performance
n = 10**7
start = time.time()
result1 = slow_sum(n)
end = time.time()
print(f"Slow sum: {result1}, Time: {end - start:.6f} seconds")

start = time.time()
result2 = fast_sum(n)
end = time.time()
print(f"Fast sum: {result2}, Time: {end - start:.6f} seconds")

Practice Exercises

Exercise 1: Basic Calculator

Create a simple calculator function that takes two numbers and an operator as input and returns the result of the operation. Support addition, subtraction, multiplication, division, and exponentiation.

Exercise 2: Interest Calculator

Write a program that calculates the final amount after investing a principal at a given interest rate for a specified number of years, with options for both simple and compound interest.

Exercise 3: Prime Number Generator

Create a function that generates all prime numbers up to a given limit. Use the Sieve of Eratosthenes algorithm for efficiency.

Exercise 4: Statistical Analysis

Write a program that reads a list of numbers from user input, then calculates and displays:

Exercise 5: Number Sequence Generator

Create functions to generate the following number sequences:

  1. Fibonacci sequence
  2. Triangular numbers (1, 3, 6, 10, 15, ...)
  3. Square numbers (1, 4, 9, 16, 25, ...)
  4. Cubic numbers (1, 8, 27, 64, 125, ...)

Wrapping Up and Next Steps

Today we've explored Python's rich capabilities for working with numbers and performing mathematical operations. From basic arithmetic to advanced statistical and financial calculations, Python provides the tools you need for everything from simple scripts to complex scientific computing.

Key Takeaways

Where to Go from Here

  1. Explore NumPy and SciPy for advanced numerical and scientific computing
  2. Dive into financial libraries like numpy-financial for more sophisticated financial calculations
  3. Learn about symbolic mathematics with SymPy for mathematical modeling and formal proofs
  4. Explore data visualization libraries like Matplotlib and Seaborn to visualize numeric data
  5. Apply your knowledge to solve real-world problems, from basic calculations to complex simulations

Additional Resources

In our next session, we'll explore control flow in Python - how to make decisions, create loops, and control the execution of your programs.