There is a common way of "writing" functions that is really assembling them: search for a function that looks close, copy it, rename the variables, and hope. When it breaks, copy a different one. Code produced this way can pass a demo and still be unexplainable by the person who shipped it, and unexplainable code cannot be debugged under production pressure.
Consider a function assembled exactly that way:
def process_temperature_data(readings, window=10, threshold=None):
"""Process temperature readings with moving average."""
if threshold is None:
threshold = [] # Why is threshold a list? Nobody knows.
if not hasattr(process_temperature_data, 'history'):
process_temperature_data.history = [] # What does this do?
process_temperature_data.history.extend(readings) # Why does this matter?
result = [sum(readings[i:i+window])/window
for i in range(len(readings)-window+1)]
return result if result else readings
It "works" sometimes. This module builds the understanding needed to see exactly what is wrong with it: scope, return semantics, mutable defaults, and side effects.
What Functions Actually Are
1. Functions Are Not Just Named Code Blocks
# The naive view of functions:
def calculate_stuff():
# Some code here
x = 10
y = 20
print(x + y)
# What it misses: Functions create a new scope!
x = 100
calculate_stuff() # Prints 30, not 120
print(x) # Still 100, function didn't change global x
2. The Return vs Print Confusion
# A constant beginner mistake:
def get_average(numbers):
avg = sum(numbers) / len(numbers)
print(avg) # WRONG! This just displays
result = get_average([1, 2, 3])
print(result) # None! The function returned nothing
# The correct version:
def get_average(numbers):
avg = sum(numbers) / len(numbers)
return avg # Actually gives back the value
result = get_average([1, 2, 3]) # Now result = 2.0
3. The Mutable Default Argument Trap
# A bug that can haunt production for weeks:
def log_reading(value, history=[]): # DANGER!
history.append(value)
return history
# First sensor
sensor1_log = log_reading(100) # [100]
sensor1_log = log_reading(102) # [100, 102] - Expected!
# Second sensor - SURPRISE!
sensor2_log = log_reading(200) # [100, 102, 200] - WHAT?!
# Both sensors share the SAME list!
Functions in Industrial Systems
Pure Functions vs Side Effects
# PURE FUNCTION - Predictable, testable
def celsius_to_fahrenheit(celsius):
"""Always returns same output for same input."""
return (celsius * 9/5) + 32
# SIDE EFFECTS - Harder to test, can cause bugs
sensor_state = {'alerts': [], 'readings': []}
def check_temperature(temp):
"""Modifies external state - side effect!"""
if temp > 1500:
sensor_state['alerts'].append(f"High temp: {temp}") # Side effect
send_email_alert() # Another side effect
sensor_state['readings'].append(temp) # And another
return temp > 1500
Function Composition
# Small, focused functions
def read_sensor(sensor_id):
"""Read single sensor value."""
return sensor_readings[sensor_id]
def validate_reading(value, min_val=0, max_val=2000):
"""Check if reading is valid."""
return min_val <= value <= max_val
def convert_to_celsius(fahrenheit):
"""Convert F to C."""
return (fahrenheit - 32) * 5/9
def process_sensor(sensor_id):
"""Compose smaller functions."""
raw_value = read_sensor(sensor_id)
if validate_reading(raw_value):
return convert_to_celsius(raw_value)
return None
The Core Idea:
Functions are contracts. The signature promises what goes in, the return promises what comes out, and side effects are the fine print. Break the contract, or fail to read the fine print, and production breaks with it.
Functions are contracts. The signature promises what goes in, the return promises what comes out, and side effects are the fine print. Break the contract, or fail to read the fine print, and production breaks with it.
Exercise: Function Mastery
Build a Temperature Alert System
Each of these functions has a real bug. Fix all four, then combine them into a working alert system:
# Fix these broken functions:
def calculate_average(readings, last_n):
"""Calculate average of last n readings."""
# BUG: What if readings has fewer than last_n elements?
return sum(readings[-last_n:]) / last_n
def add_alert(message, alerts=[]):
"""Add alert to list."""
# BUG: Mutable default argument!
alerts.append(message)
return alerts
def check_sensor_health(readings):
"""Check if sensor is working properly."""
avg = sum(readings) / len(readings)
print(f"Average: {avg}")
# BUG: No return statement!
def process_batch(sensor_data):
"""Process batch of sensor data."""
for reading in sensor_data:
if reading > 1500:
alert = True
else:
alert = False
return alert # BUG: Only returns last value!
# Your challenge: Fix all bugs and create a working system
The Function Mastery Checklist
On exit from this module you should be able to say:
✓ I can explain what parameters vs arguments are
✓ I understand scope and namespace
✓ I know when to use return vs print
✓ I can identify and fix side effects
✓ I understand *args and **kwargs
✓ I can write pure functions
✓ I know why mutable defaults are dangerous
✓ I can explain what parameters vs arguments are
✓ I understand scope and namespace
✓ I know when to use return vs print
✓ I can identify and fix side effects
✓ I understand *args and **kwargs
✓ I can write pure functions
✓ I know why mutable defaults are dangerous