3️⃣ Functions

📖 What is a Function?

A function is a reusable block of code characterized by four components:

  1. A name (identifier) – cardinality: exactly 1, required
  2. Some parameters (inputs) – cardinality: 0 to ∞, optional
  3. Some instructions (executable statements) – cardinality: at least 1, required
  4. Some return values (output) – cardinality: 0 to ∞, optional
Analogy: A function is like a recipe 📜:

  • The recipe has a name
  • It needs ingredients: these are the parameters
  • You follow the steps: these are the instructions
  • You get a meal at the end : this is what’s returned

Once you have the recipe, you can make cookies any time – just give it the ingredients!

Simple Example:

def compute_mean(value1, value2):
    result = (value1 + value2) / 2
    return result

mean = compute_mean(2,4)
print(mean)

Explanation:

  • def compute_mean(value1, value2): defines a function named compute_mean with two parameters
  • result = (value1 + value2) / 2 is an instruction step which computes the average and stores it in a variable named result
  • return result is the final instruction steps which specifies the output value
  • compute_mean(2,4) executes the function with specific valuesvalues
  • The returned value is stored in variable mean

🎯 Why Functions Matter

Scenario: Consider a program that calculates rectangular areas for multiple rooms. Without functions, the code becomes repetitive and difficult to maintain.

❌ Approach 1: Repetitive Code

# Living room area
length1 = 5
width1 = 4
area1 = length1 * width1

# Bedroom area
length2 = 3.5
width2 = 3
area2 = length2 * width2

# Kitchen area
length3 = 4
width3 = 3.5
area3 = length3 * width3

print(area1, area2, area3)

Identified Problems:

  • The calculation length * width appears three times
  • Six variables track three calculations
  • Each repetition introduces potential transcription errors
  • Modifying the formula requires updating multiple locations
  • Code readability deteriorates as repetitions increase

✅ Approach 2: Function-Based Code

def calculate_area(length, width):
    area = length * width
    return area

# Calculate areas using the function
area1 = calculate_area(5, 4)
area2 = calculate_area(3.5, 3)
area3 = calculate_area(4, 3.5)

print(area1, area2, area3)

Advantages:

  • ✅ The formula exists in a single location
  • ✅ Code length is reduced
  • ✅ Transcription errors are eliminated
  • ✅ Formula modifications require updating only one location
  • ✅ Code intent is immediately clear
  • ✅ The function can be reused throughout the program

⭐ The DRY Principle

Don’t Repeat Yourself

When identical or similar code appears multiple times in a program, a function should be created to encapsulate that logic. This principle is fundamental to professional software development.

Rule of thumb: If code is copied and pasted, it should be converted into a function.


🔨 Anatomy of Functions

def function_name(parameter1, parameter2):
    # Instructions (must be indented by 4 spaces)
    result = parameter1 + parameter2
    return result

Syntactic Components:

  1. def – keyword initiating function definition
  2. function_name – identifier for the function (use descriptive names)
  3. (parameter1, parameter2) – parameter list (may be empty)
  4. : – colon concluding the function header
  5. Indentation – all function body statements must be indented (4 spaces)
  6. return – statement transmitting output value(s) to the caller and imediately ending the function execution

EXAMPLE 1: Zero parameters, one return value

def get_pi():
    return 3.14159

pi_value = get_pi()
print("π ≈", pi_value)

Note: Even without parameters, parentheses () are mandatory.

EXAMPLE 2: One parameter, one return value

def square(x):
    result = x ** 2
    return result

number = 7
squared = square(number)
print(f"{number}² = {squared}")

Case 3: Multiple parameters, one return value

def calculate_rectangle_area(length, width):
    area = length * width
    return area

room1_area = calculate_rectangle_area(5, 3)
room2_area = calculate_rectangle_area(10, 4)

print("Room 1 area:", room1_area, "square units")
print("Room 2 area:", room2_area, "square units")

Execution:

  • calculate_rectangle_area(5, 3) is invoked
  • Parameter length receives value 5
  • Parameter width receives value 3
  • Calculation: 5 * 3 = 15
  • Value 15 is returned and stored in room1_area

Case 4: Multiple parameters, multiple return values

def divide_with_remainder(dividend, divisor):
    quotient = dividend // divisor
    remainder = dividend % divisor
    return quotient, remainder

q, r = divide_with_remainder(17, 5)
print(f"17 ÷ 5 = {q} remainder {r}")

Technical note: Multiple return values are implemented as tuples. The statement return quotient, remainder creates tuple (quotient, remainder), which is unpacked upon assignment.

✏️ Naming Convention

Function Naming Best Practices:

Effective names:

  • calculate_average() – describes the operation
  • convert_celsius_to_fahrenheit() – clear purpose

Ineffective names:

  • function1() – no semantic meaning
  • do_stuff() – vague purpose

Convention: Function names should use lowercase with underscores separating words (snake_case).


📤 Functions Communicate Through Return Values

⚠️ Critical Rule: Functions should RETURN results, not PRINT them

Why this matters:

When you write a function, you’re creating a tool that other parts of your program will use. Functions must give back results so they can be:

  • Stored in variables
  • Used in calculations
  • Passed to other functions
  • Tested and verified

❌ BAD – Using print inside functions:

def calculate_area(length, width):
    area = length * width
    print(area)  # ❌ NEVER do this!
    
result = calculate_area(5, 3)  # Prints "15" but...
print(result)  # Prints "None" - the function gave nothing back!
double = result * 2  # ERROR - can't do math with None!

✅ GOOD – Using return:

def calculate_area(length, width):
    area = length * width
    return area  # ✅ Give the value back
    
result = calculate_area(5, 3)  # Nothing prints yet - value stored
print(result)  # Now we choose when to display: 15
double = result * 2  # Works perfectly: 30

The Rule:

  • return = the function’s output (for the program to use)
  • print() = displaying information to humans (for viewing only)

In this course, functions should NEVER contain print statements. The only exception is when debugging your own code temporarily.


📞 Calling Functions

⭐ Critical Distinction: Definition vs. Invocation

Definition (using def): Creates the function but does not execute it

Invocation/Call (using function name with parentheses): Executes the function’s instructions


Analogy:

  • Definition = Writing a recipe in a cookbook
  • Invocation = Actually preparing the meal

Merely defining a function does not execute its instructions. The function must be explicitly called.

Example: Definition vs. Invocation

# DEFINITION - creates the function
def calculate_double(x):
    return x * 2

# At this point, nothing has been calculated yet

# INVOCATION - executes the function
result = calculate_double(5)
print("Result:", result)

🔗 Functions Calling Other Functions

Function Composition

Functions can invoke other functions, enabling the construction of complex programs from simpler components. This approach promotes code organization and reusability.

Principle: Decompose complex problems into smaller, manageable functions, then combine them to solve the larger problem.

This technique is called functional decomposition or modular programming.

Mathematical Function Composition

def square(x):
    """Calculate x²."""
    return x ** 2

def sum_of_squares(a, b):
    """Calculate a² + b²."""
    return square(a) + square(b)

import math 
def distance_from_origin(x, y):
    """Calculate distance from origin: √(x² + y²)."""
    sum_sq = sum_of_squares(x, y)
    distance = math.sqrt(sum_sq)
    return distance

point_x = 3
point_y = 4
dist = distance_from_origin(point_x, point_y)
print(f"Distance from origin to ({point_x}, {point_y}): {dist}")

Benefits of this approach:

  • Each function has a single, clear responsibility
  • Functions can be tested independently
  • Code reusability is maximized
  • Complex calculations are decomposed into comprehensible steps

Integrating Custom and Library Functions

import math

def calculate_distance(x1, y1, x2, y2):
    """Calculate Euclidean distance between two points."""
    dx = x2 - x1
    dy = y2 - y1
    distance = math.sqrt(dx**2 + dy**2)
    return distance

def calculate_circle_area(radius):
    """Calculate circle area using math.pi."""
    return math.pi * radius ** 2

# Test the functions
dist = calculate_distance(0, 0, 3, 4)
print("Distance:", dist)

area = calculate_circle_area(5)
print("Circle area:", round(area, 2))

Observation: Custom functions can utilize library functions to implement complex operations efficiently.

Frequently Used Python Standard Libraries:

Library Purpose Example Functions
math Mathematical operations sqrt(), sin(), pi, ceil()
random Random number generation randint(), choice(), shuffle()
statistics Statistical calculations mean(), median(), stdev()

Note: These are standard libraries included with Python. Additional third-party libraries (such as numpy, pandas, matplotlib) require separate installation.


🔍 Tracing Function Execution

⭐ Systematic Tracing Methodology

Tracing is the process of manually executing code step-by-step to understand program behavior. For functions, this requires tracking:

  1. Function invocation location
  2. Argument values passed
  3. Parameter-argument mapping
  4. Line-by-line execution within the function
  5. Return value
  6. Continuation point after function completes

Tracing procedure:

  1. Locate the function call
  2. Identify argument values
  3. Navigate to function definition
  4. Map arguments to parameters by position
  5. Execute function body line by line
  6. Record variable values in a trace table
  7. Identify return value
  8. Continue execution from call site

Trace Example 1: Basic Function

Code:

def multiply_and_add(x, y):
    product = x * y
    result = product + 10
    return result

a = 3
b = 4
final = multiply_and_add(a, b)
print(final)

Trace Table:

Step Line Action a b x y product result final
1 6 a = 3 3
2 7 b = 4 3 4
3 8 Call multiply_and_add(3, 4) 3 4
4 1 Enter function; parameters created 3 4 3 4
5 2 product = 3 * 4 3 4 3 4 12
6 3 result = 12 + 10 3 4 3 4 12 22
7 4 return 22 3 4 3 4 12 22
8 8 Exit function; final = 22 3 4 22
9 9 print(22) outputs 22 3 4 22

Key observations:

  • Blue rows (steps 4-7): Execution inside function; a and b are out of scope
  • White rows: Execution in main program; x, y, product, result are out of scope
  • Return value (22) bridges the two scopes

⚠️ Common Errors

Error 1: Omitting Parentheses in Function Calls

def greet():
    return "Hello!"

# INCORRECT - references function object
message = greet
print(message)  # Outputs: 

# CORRECT - parentheses invoke the function
message = greet()
print(message)  # Outputs: Hello!

Solution: Always include () when invoking a function.

Error 2: Argument Count Mismatch

def add(a, b):
    return a + b

# ERROR: Insufficient arguments
result = add(5)          # TypeError: missing 1 required positional argument

# ERROR: Excessive arguments
result = add(5, 3, 7)    # TypeError: takes 2 positional arguments but 3 were given

# CORRECT: Argument count matches parameter count
result = add(5, 3)       # Works correctly

Solution: Ensure argument count matches parameter count in function definition.

Error 3: Missing Return Statement

def double(x):
    result = x * 2
    # Missing return statement

answer = double(5)
print(answer)  # Outputs: None (not 10!)

# CORRECT version:
def double(x):
    result = x * 2
    return result

answer = double(5)
print(answer)  # Outputs: 10

Solution: If a function should produce a value, include a return statement.

Error 4: Incorrect Indentation

# INCORRECT: Function body not indented
def calculate(x):
result = x * 2    # IndentationError
return result

# CORRECT: Function body indented by 4 spaces
def calculate(x):
    result = x * 2
    return result

Solution: All function body statements must be indented consistently (standard: 4 spaces).

# INCORRECT: Function call indented inside the function
def calculate(x):
    result = x * 2
    return result
    print(calculate(2))  # WRONG! This line is indented -> Part of the function -> Will never produce a result

# CORRECT : Function call at the beginning of the line
def calculate(x):
    result = x * 2
    return result

print(calculate(2))

Solution: Function calls align with def, not with the function body.

Error 5: Accessing Local Variables Externally

def calculate(x):
    result = x * 2
    return result

answer = calculate(5)
print(answer)      # Works: 10
print(result)      # ERROR: NameError: name 'result' is not defined

Explanation: Variables result and x are local to the function. They do not exist in the main program scope.

Solution: Use the returned value; do not attempt to access function-internal variables.