Functions & Arguments

0/4 in this phase0/54 across the roadmap

📖 Concept

Functions are the fundamental building blocks of Python programs. Python functions are first-class objects — they can be assigned to variables, passed as arguments, returned from other functions, and stored in data structures.

Function definition:

def function_name(param1, param2="default"):
    """Docstring describing the function."""
    return result

Argument types:

Type Syntax Description
Positional f(a, b) Matched by position
Keyword f(a=1, b=2) Matched by name
Default def f(x=10) Fallback value if not provided
*args def f(*args) Variable positional args (tuple)
**kwargs def f(**kwargs) Variable keyword args (dict)
Positional-only def f(x, /) Must be passed positionally (3.8+)
Keyword-only def f(*, x) Must be passed as keyword

Type hints (PEP 484) add optional type annotations. They don't enforce types at runtime but help with documentation, IDE support, and static analysis with mypy.

Docstrings (PEP 257) document what a function does. Use triple quotes, describe parameters and return values. The first line should be a concise summary.

Key principle: Functions should do one thing well. If a function name needs "and" in it (e.g., validate_and_save), consider splitting it into two functions.

💻 Code Example

codeTap to expand ⛶
1# ============================================================
2# Basic function definition
3# ============================================================
4def greet(name: str, greeting: str = "Hello") -> str:
5 """Return a greeting message.
6
7 Args:
8 name: The name of the person to greet.
9 greeting: The greeting word. Defaults to "Hello".
10
11 Returns:
12 A formatted greeting string.
13 """
14 return f"{greeting}, {name}!"
15
16print(greet("Alice")) # "Hello, Alice!"
17print(greet("Bob", "Hey")) # "Hey, Bob!"
18print(greet(greeting="Hi", name="Charlie")) # "Hi, Charlie!"
19
20# ============================================================
21# *args and **kwargs
22# ============================================================
23def log_message(level: str, *messages, **metadata):
24 """Log messages with metadata."""
25 combined = " ".join(str(m) for m in messages)
26 meta_str = ", ".join(f"{k}={v}" for k, v in metadata.items())
27 print(f"[{level}] {combined}" + (f" ({meta_str})" if meta_str else ""))
28
29log_message("INFO", "Server started", "on port", 8080)
30# [INFO] Server started on port 8080
31
32log_message("ERROR", "Connection failed", host="db.server.com", retry=3)
33# [ERROR] Connection failed (host=db.server.com, retry=3)
34
35# ============================================================
36# Positional-only and keyword-only parameters (Python 3.8+)
37# ============================================================
38def divide(a, b, /, *, round_to=None):
39 """
40 a, b: positional-only (before /)
41 round_to: keyword-only (after *)
42 """
43 result = a / b
44 if round_to is not None:
45 result = round(result, round_to)
46 return result
47
48print(divide(10, 3)) # 3.3333...
49print(divide(10, 3, round_to=2)) # 3.33
50# divide(a=10, b=3) # TypeError: positional-only
51
52# ============================================================
53# Functions as first-class objects
54# ============================================================
55def add(a, b):
56 return a + b
57
58def subtract(a, b):
59 return a - b
60
61# Assign to variable
62operation = add
63print(operation(5, 3)) # 8
64
65# Store in data structures
66operations = {
67 "+": add,
68 "-": subtract,
69 "*": lambda a, b: a * b,
70}
71print(operations["*"](4, 5)) # 20
72
73# Pass as argument
74def apply_operation(func, a, b):
75 return func(a, b)
76
77print(apply_operation(add, 10, 5)) # 15
78
79# Return from function
80def make_multiplier(factor):
81 def multiply(x):
82 return x * factor
83 return multiply
84
85double = make_multiplier(2)
86triple = make_multiplier(3)
87print(double(5)) # 10
88print(triple(5)) # 15
89
90# ============================================================
91# Type hints (PEP 484)
92# ============================================================
93from typing import Optional, Union
94
95def find_user(
96 user_id: int,
97 include_inactive: bool = False,
98) -> Optional[dict]:
99 """Find user by ID. Returns None if not found."""
100 users = {1: {"name": "Alice", "active": True}}
101 user = users.get(user_id)
102 if user and (include_inactive or user.get("active")):
103 return user
104 return None
105
106# Union types (Python 3.10+ can use | syntax)
107def process(value: int | str) -> str:
108 return str(value).upper()
109
110# Complex type hints
111from typing import Callable
112
113def retry(func: Callable[..., str], attempts: int = 3) -> str:
114 for i in range(attempts):
115 try:
116 return func()
117 except Exception:
118 if i == attempts - 1:
119 raise
120 return ""
121
122# ============================================================
123# Default argument pitfall (CRITICAL!)
124# ============================================================
125# BAD — mutable default argument is shared across calls!
126def append_to_bad(item, lst=[]):
127 lst.append(item)
128 return lst
129
130print(append_to_bad(1)) # [1]
131print(append_to_bad(2)) # [1, 2]Unexpected! Same list!
132print(append_to_bad(3)) # [1, 2, 3]Accumulating!
133
134# GOOD — use None as sentinel
135def append_to_good(item, lst=None):
136 if lst is None:
137 lst = []
138 lst.append(item)
139 return lst
140
141print(append_to_good(1)) # [1]
142print(append_to_good(2)) # [2]Correct! New list each time.
143
144# ============================================================
145# Unpacking arguments
146# ============================================================
147def create_user(name, age, city):
148 return {"name": name, "age": age, "city": city}
149
150# Unpack list/tuple as positional args
151args = ["Alice", 30, "NYC"]
152user = create_user(*args)
153
154# Unpack dict as keyword args
155kwargs = {"name": "Bob", "age": 25, "city": "LA"}
156user = create_user(**kwargs)
157
158# Forwarding args (common in decorators)
159def wrapper(*args, **kwargs):
160 print(f"Called with args={args}, kwargs={kwargs}")
161 return create_user(*args, **kwargs)

🏋️ Practice Exercise

Exercises:

  1. Write a function safe_divide(a, b, /, *, default=0) that uses positional-only and keyword-only parameters. Return default on division by zero.

  2. Create a make_validator(min_val, max_val) function that returns a validator function. The returned function should check if a value is within range.

  3. Implement a retry(func, max_attempts=3, delay=1) function that retries a function on exception, with exponential backoff.

  4. Write a function with full type hints that accepts a list of dictionaries, filters by a key-value pair, and returns sorted results. Use typing module.

  5. Demonstrate the mutable default argument bug. Then create a decorator that automatically fixes mutable defaults.

  6. Build a function pipe(*functions) that takes multiple functions and returns a new function that applies them in sequence: pipe(f, g, h)(x) = h(g(f(x))).

⚠️ Common Mistakes

  • Using mutable default arguments: def f(lst=[]) shares the same list across all calls. Always use None as default and create new objects inside the function.

  • Not understanding argument order: positional → *args → keyword-only → **kwargs. The full signature order is: def f(pos_only, /, normal, *args, kw_only, **kwargs).

  • Forgetting that return without a value (or no return statement) returns None. If your function should return a value, always include an explicit return.

  • Writing functions that are too long or do too many things. If you need to scroll to read the whole function, it's too long. Split into smaller, well-named functions.

  • Not using type hints in production code. While optional, type hints catch bugs early with mypy, improve IDE autocompletion, and serve as documentation.

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Functions & Arguments