Functions & Arguments

Difficulty: Beginner

Question

Explain Python functions, argument types, scope rules, and first-class function concepts.

Answer

Functions in Python are first-class objects - they can be assigned to variables, passed as arguments, and returned from other functions.

Argument types: - Positional: func(a, b) - Keyword: func(name='Alice') - Default: def func(x=10) - *args: variable positional arguments (tuple) - kwargs: variable keyword arguments (dict)

Scope follows LEGB rule: Local -> Enclosing -> Global -> Built-in.

Mutable default argument is a common Python gotcha - default values are evaluated once at function definition, not at each call.

Code examples

Argument Types and the Mutable Default Gotcha

# All argument types in one function
def example(a, b, c=10, *args, kwargs):
    print(f"a={a}, b={b}, c={c}")
    print(f"args={args}")
    print(f"kwargs={kwargs}")

example(1, 2, 3, 4, 5, name='Alice', age=30)

# GOTCHA: Mutable default argument
def append_to(element, target=[]):  # BUG!
    target.append(element)
    return target

print(append_to(1))  # [1]
print(append_to(2))  # [1, 2] - NOT [2]!

# Fix: use None as default
def append_to_fixed(element, target=None):
    if target is None:
        target = []
    target.append(element)
    return target

print(append_to_fixed(1))  # [1]
print(append_to_fixed(2))  # [2] - correct!

Default mutable arguments are shared across calls because they are created once at function definition time. Always use None as default for mutable types.

First-Class Functions

# Functions as arguments
def apply(func, value):
    return func(value)

print(apply(str.upper, 'hello'))  # 'HELLO'
print(apply(len, [1, 2, 3]))     # 3

# Functions as return values (closure)
def multiplier(factor):
    def multiply(x):
        return x * factor  # 'factor' is captured
    return multiply

double = multiplier(2)
triple = multiplier(3)
print(double(5))   # 10
print(triple(5))   # 15

# Storing functions in data structures
operations = {
    '+': lambda a, b: a + b,
    '-': lambda a, b: a - b,
    '*': lambda a, b: a * b,
    '/': lambda a, b: a / b,
}
print(operations['+'](10, 3))  # 13
print(operations['*'](10, 3))  # 30

Functions being first-class means they're just objects. Closures capture variables from the enclosing scope, enabling powerful patterns like factory functions.

LEGB Scope Rules

x = 'global'

def outer():
    x = 'enclosing'
    
    def inner():
        x = 'local'
        print(f"inner: {x}")   # local
    
    inner()
    print(f"outer: {x}")      # enclosing

outer()
print(f"global: {x}")         # global

# Using global and nonlocal
counter = 0
def increment():
    global counter
    counter += 1

def make_counter():
    count = 0
    def increment():
        nonlocal count  # Refers to enclosing scope variable
        count += 1
        return count
    return increment

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

LEGB: Local, Enclosing, Global, Built-in. Use 'global' to modify module-level variables, 'nonlocal' to modify enclosing function variables.

Key points

Concepts covered

Functions, Arguments, Default Values, Scope, First-Class Functions, Closures