Functions & Arguments
📖 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
1# ============================================================2# Basic function definition3# ============================================================4def greet(name: str, greeting: str = "Hello") -> str:5 """Return a greeting message.67 Args:8 name: The name of the person to greet.9 greeting: The greeting word. Defaults to "Hello".1011 Returns:12 A formatted greeting string.13 """14 return f"{greeting}, {name}!"1516print(greet("Alice")) # "Hello, Alice!"17print(greet("Bob", "Hey")) # "Hey, Bob!"18print(greet(greeting="Hi", name="Charlie")) # "Hi, Charlie!"1920# ============================================================21# *args and **kwargs22# ============================================================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 ""))2829log_message("INFO", "Server started", "on port", 8080)30# [INFO] Server started on port 80803132log_message("ERROR", "Connection failed", host="db.server.com", retry=3)33# [ERROR] Connection failed (host=db.server.com, retry=3)3435# ============================================================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 / b44 if round_to is not None:45 result = round(result, round_to)46 return result4748print(divide(10, 3)) # 3.3333...49print(divide(10, 3, round_to=2)) # 3.3350# divide(a=10, b=3) # TypeError: positional-only5152# ============================================================53# Functions as first-class objects54# ============================================================55def add(a, b):56 return a + b5758def subtract(a, b):59 return a - b6061# Assign to variable62operation = add63print(operation(5, 3)) # 86465# Store in data structures66operations = {67 "+": add,68 "-": subtract,69 "*": lambda a, b: a * b,70}71print(operations["*"](4, 5)) # 207273# Pass as argument74def apply_operation(func, a, b):75 return func(a, b)7677print(apply_operation(add, 10, 5)) # 157879# Return from function80def make_multiplier(factor):81 def multiply(x):82 return x * factor83 return multiply8485double = make_multiplier(2)86triple = make_multiplier(3)87print(double(5)) # 1088print(triple(5)) # 158990# ============================================================91# Type hints (PEP 484)92# ============================================================93from typing import Optional, Union9495def 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 user104 return None105106# Union types (Python 3.10+ can use | syntax)107def process(value: int | str) -> str:108 return str(value).upper()109110# Complex type hints111from typing import Callable112113def 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 raise120 return ""121122# ============================================================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 lst129130print(append_to_bad(1)) # [1]131print(append_to_bad(2)) # [1, 2] ← Unexpected! Same list!132print(append_to_bad(3)) # [1, 2, 3] ← Accumulating!133134# GOOD — use None as sentinel135def append_to_good(item, lst=None):136 if lst is None:137 lst = []138 lst.append(item)139 return lst140141print(append_to_good(1)) # [1]142print(append_to_good(2)) # [2] ← Correct! New list each time.143144# ============================================================145# Unpacking arguments146# ============================================================147def create_user(name, age, city):148 return {"name": name, "age": age, "city": city}149150# Unpack list/tuple as positional args151args = ["Alice", 30, "NYC"]152user = create_user(*args)153154# Unpack dict as keyword args155kwargs = {"name": "Bob", "age": 25, "city": "LA"}156user = create_user(**kwargs)157158# 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:
Write a function
safe_divide(a, b, /, *, default=0)that uses positional-only and keyword-only parameters. Returndefaulton division by zero.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.Implement a
retry(func, max_attempts=3, delay=1)function that retries a function on exception, with exponential backoff.Write a function with full type hints that accepts a list of dictionaries, filters by a key-value pair, and returns sorted results. Use
typingmodule.Demonstrate the mutable default argument bug. Then create a decorator that automatically fixes mutable defaults.
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 useNoneas 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
returnwithout a value (or no return statement) returnsNone. If your function should return a value, always include an explicitreturn.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