4.2. Function Design#

Good functions are small, named clearly, documented when needed, and composed with other functions. This section focuses on scope, composition, docstrings, and basic lambda expressions.

import sys
from pathlib import Path

# Find project root by looking for _config.yml
current = Path.cwd()
for parent in [current, *current.parents]:
    if (parent / '_config.yml').exists():
        project_root = parent
        break
else:
    project_root = Path.cwd().parent.parent

# Add project root to path
sys.path.insert(0, str(project_root))

# Import shared teaching helpers and cell magics
from shared import thinkpython, diagram, jupyturtle, structshape
from shared.download import download

4.2.1. Scope#

Variables created inside functions are local to that function:

def my_function():
    local_var = 10  # Only exists inside the function
    print(local_var)

my_function()     # Prints: 10
# print(local_var)  # Error: local_var doesn't exist here

Variables outside functions are global:

global_var = 100  # Accessible everywhere

def show_global():
    print(global_var)  # Can read global variable

show_global()  # Prints: 100

4.2.1.1. Variables and Parameters Are Local#

When you create a variable inside a function, it is local, which means it exists only inside the function. For example, the following function takes two arguments, concatenates them, and prints the result twice.

def greet():
    message = "Hello"
    print(message)

greet()

try:
    print(message)   ### cannot do this because message is local
except NameError as error:
    print(type(error).__name__)
Hello
NameError
def greet():
    global message        ### must declare first
    message = "Hello"
    print(message)

greet()

print(message)   ### can do! "message" variable is now global in the function definition.
Hello
Hello

4.2.2. Function Composition#

Function composition is the process of using the output of one function as the input to another.

def double(x):
    return x * 2

def square(x):
    return x ** 2

result = square(double(3))
print(result)
36
### === EXERCISE 7: Function Composition ===
### Write three functions:
### - calculate_area(length, width): returns the area
### - format_result(value): returns 'Area is X square units'
### Then write report_rectangle() that uses both to calculate and format.
### Your code starts here:






### Your code stops here.

Hide code cell source

### Solution:
def calculate_area(length, width):
    return length * width

def format_result(value):
    return f'Area is {value} square units'

### Function composition: one function calls another
def report_rectangle(length, width):
    area = calculate_area(length, width)
    return format_result(area)

### Test
print(report_rectangle(5, 3))
print(report_rectangle(10, 2))
Area is 15 square units
Area is 20 square units
### === EXERCISE 9: Variable Scopes ===
### Write a function called modify_variables() that:
### - Takes a parameter x
### - Creates a local variable local_var = x * 2
### - Prints both x and local_var inside the function
### Then call it and try to print local_var outside (it should fail).

### Your code starts here:

Hide code cell source

### Solution:
def modify_variables(x):
    local_var = x * 2
    print(f"Inside: x={x}, local_var={local_var}")

### Call the function
modify_variables(5)

### This will cause NameError because local_var doesn't exist outside
Inside: x=5, local_var=10

When we run print_verse, it calls first_two_lines, which calls repeat, which calls print. That’s a lot of functions.

Of course, we could have done the same thing with fewer functions, but the point of this example is to show how functions can work together.

4.2.3. Docstrings#

Docstrings (documentation strings) are used to document functions/methods, classes, and modules. They use triple quotes and should be the first statement after defining a function or class.

Always document your functions with docstrings:

### Function with docstring
def greet(name):
    """
    This function does xxx and yyy.         ### 1. what this function is about
    
    Args:                                   ### 2. input parameters
        name: The person's name (string)    
    
    Returns:                                ### 3. what the function returns
        A greeting message (string)
    """
    return f"Hello, {name}!"

message = greet("Homer")       ### call the function

As an example:

def calculate_bmi(weight, height):
    """
    Calculate Body Mass Index (BMI).
    
    Args:
        weight: Weight in kilograms (float)
        height: Height in meters (float)
    
    Returns:
        BMI value (float)
    
    Example:
        >>> calculate_bmi(70, 1.75)
        22.86
    """
    return weight / (height ** 2)

Good docstrings include:

  1. What the function does

  2. Parameters and their types

  3. What the function returns

  4. Usage examples (optional)

### Practical functions with docstrings

def calculate_discount(price, discount_percent):
    """
    Calculate the final price after applying a discount.
    
    Args:
        price: Original price (float or int)
        discount_percent: Discount percentage (0-100)
    
    Returns:
        Final price after discount (float)
    """
    discount_amount = price * (discount_percent / 100)
    final_price = price - discount_amount
    return final_price

def is_valid_email(email):
    """
    Check if email has basic valid format.
    
    Args:
        email: Email address to validate (str)
    
    Returns:
        True if email contains @ and ., False otherwise
    """
    return '@' in email and '.' in email

def celsius_to_fahrenheit(celsius):
    """
    Convert Celsius to Fahrenheit.
    
    Args:
        celsius: Temperature in Celsius
    
    Returns:
        Temperature in Fahrenheit
    """
    return (celsius * 9/5) + 32

### Test the functions
print("Testing calculate_discount:")
original = 100
discount = 20
final = calculate_discount(original, discount)
print(f"  ${original} with {discount}% off = ${final}")

print("\nTesting is_valid_email:")
print(f"  'user@example.com' is valid: {is_valid_email('user@example.com')}")
print(f"  'invalid-email' is valid: {is_valid_email('invalid-email')}")

print("\nTesting celsius_to_fahrenheit:")
print(f"  0°C = {celsius_to_fahrenheit(0)}°F")
print(f"  100°C = {celsius_to_fahrenheit(100)}°F")
print(f"  37°C = {celsius_to_fahrenheit(37):.1f}°F")
Testing calculate_discount:
  $100 with 20% off = $80.0

Testing is_valid_email:
  'user@example.com' is valid: True
  'invalid-email' is valid: False

Testing celsius_to_fahrenheit:
  0°C = 32.0°F
  100°C = 212.0°F
  37°C = 98.6°F
### === EXERCISE 10: Writing Good Docstrings ===
### Write a function called validate_password() that:
### - Takes a password string as a parameter
### - Checks if it's at least 8 characters long
### - Returns True if valid, False otherwise
### Include a comprehensive docstring with description, Args, and Returns sections.

### Your code starts here:

Hide code cell source

### Solution:
def validate_password(password):
    """
    Validate if a password meets minimum length requirements.
    
    Args:
        password (str): The password to validate
    
    Returns:
        bool: True if password is at least 8 characters, False otherwise
    """
    return len(password) >= 8

### Test the function
print(validate_password("short"))        ### False
print(validate_password("verylongpassword"))  ### True
False
True

4.2.4. Lambda Functions#

Syntax: lambda arguments: expression

Lambda functions are small, anonymous functions that can have any number of arguments but can only have one expression. They are useful for short, simple functions that you don’t want to define formally.

4.2.4.1. Lambda Limitations#

  • Can only contain expressions, not statements

  • Cannot contain assignments, print statements, or other statements

  • Best for simple, one-line functions

  • For complex logic, use regular functions (def)

If you find yourself writing a multi-line lambda, switch to a normal def function instead.

### Basic lambda function
square = lambda x: x ** 2
print(square(5))  # Output: 25

### Lambda with multiple arguments
add = lambda x, y: x + y
print(add(3, 4))  # Output: 7
25
7