Skip to content
beginner Phase 1 · Python Basics

Functions & Scope

Define functions, arguments, return values, and understand variable scope.

1h 15m
0 problems
Topic Progress 0%

Defining Functions

Function Basics

# Simple function
def greet(name):
    return f'Hello, {name}!'

# Function with multiple parameters
def calculate_area(length, width):
    area = length * width
    return area

# Function with no return value (returns None)
def print_hello():
    print('Hello!')

result = print_hello()
print(result)  # None

Docstrings

def calculate_average(numbers):
    """Calculate the average of a list of numbers.
    
    Args:
        numbers: A list of numeric values.
    
    Returns:
        The average of the numbers.
    
    Raises:
        ValueError: If the list is empty.
    """
    if not numbers:
        raise ValueError('Cannot calculate average of empty list')
    return sum(numbers) / len(numbers)

# Access docstring
calculate_average.__doc__
help(calculate_average)

Multiple Return Values

# Functions can return multiple values
def get_min_max(numbers):
    return min(numbers), max(numbers)

minimum, maximum = get_min_max([3, 1, 4, 1, 5])
print(f'Min: {minimum}, Max: {maximum}')  # Min: 1, Max: 5

# Return as tuple
result = get_min_max([3, 1, 4, 1, 5])
print(result)  # (1, 5)
print(type(result))  # <class 'tuple'>

Parameters & Arguments

Default Arguments

def greet(name, greeting='Hello'):
    return f'{greeting}, {name}!'

greet('Alice')           # 'Hello, Alice!'
greet('Alice', 'Hi')    # 'Hi, Alice!'

⚠️ Mutable Default Arguments

# WRONG: Mutable default argument
items = []  # Shared across calls!
def add_item(item, items=[]):
    items.append(item)
    return items

add_item(1)  # [1]
add_item(2)  # [1, 2] - unexpected!

# RIGHT: Use None as default
items = []
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

add_item(1)  # [1]
add_item(2)  # [2] - correct!

Keyword Arguments

def create_user(name, age, email):
    return {'name': name, 'age': age, 'email': email}

# Positional arguments
create_user('Alice', 30, 'alice@example.com')

# Keyword arguments (order doesn't matter)
create_user(email='alice@example.com', name='Alice', age=30)

# Mix (positional must come first)
create_user('Alice', email='alice@example.com', age=30)

*args and **kwargs

# *args - variable positional arguments
def calculate_sum(*args):
    return sum(args)

calculate_sum(1, 2, 3)      # 6
calculate_sum(1, 2, 3, 4, 5)  # 15

# **kwargs - variable keyword arguments
def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f'{key}: {value}')

print_info(name='Alice', age=30)
# name: Alice
# age: 30

# Combined
def func(required, *args, **kwargs):
    print(f'Required: {required}')
    print(f'Args: {args}')
    print(f'Kwargs: {kwargs}')

func('hello', 1, 2, 3, key1='value1', key2='value2')
# Required: hello
# Args: (1, 2, 3)
# Kwargs: {'key1': 'value1', 'key2': 'value2'}

Unpacking Arguments

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

numbers = [1, 2, 3]
add(*numbers)  # 6 (unpacks list)

kwargs = {'a': 1, 'b': 2, 'c': 3}
add(**kwargs)  # 6 (unpacks dictionary)

Variable Scope

LEGB Rule

Python resolves variables using the LEGB rule:

  1. Local - inside the function
  2. Enclosing - in enclosing function (nested)
  3. Global - at module level
  4. Built-in - Python's built-in names
# Global scope
global_var = 'I am global'

def outer():
    # Enclosing scope
    outer_var = 'I am enclosing'
    
    def inner():
        # Local scope
        inner_var = 'I am local'
        print(global_var)   # ✅ Access global
        print(outer_var)    # ✅ Access enclosing
        print(inner_var)    # ✅ Access local
    
    inner()

outer()

global Keyword

count = 0

def increment():
    global count  # Declare use of global variable
    count += 1

increment()
print(count)  # 1

# ⚠️ Avoid when possible - use return values instead
def increment_better(current_count):
    return current_count + 1

nonlocal Keyword

def counter():
    count = 0
    
    def increment():
        nonlocal count  # Access enclosing variable
        count += 1
        return count
    
    return increment

c = counter()
print(c())  # 1
print(c())  # 2
print(c())  # 3

Closures

def make_multiplier(factor):
    def multiplier(x):
        return x * factor  # 'factor' is captured
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15

Common Scope Pitfalls

# ⚠️ UnboundLocalError
x = 10
def func():
    print(x)  # UnboundLocalError!
    x = 20    # Python thinks x is local

# Fix: Use global or pass as argument
def func():
    global x
    print(x)  # 10

First-Class Functions

# Functions are objects - can be:

# 1. Assigned to variables
def add(a, b):
    return a + b

my_func = add
print(my_func(2, 3))  # 5

# 2. Passed as arguments
def apply(func, a, b):
    return func(a, b)

print(apply(add, 2, 3))  # 5

# 3. Returned from functions
def get_operation(op):
    if op == 'add':
        return lambda a, b: a + b
    elif op == 'mul':
        return lambda a, b: a * b

add_op = get_operation('add')
print(add_op(2, 3))  # 5

# 4. Stored in data structures
operations = {
    'add': lambda a, b: a + b,
    'sub': lambda a, b: a - b,
}
print(operations['add'](5, 3))  # 8