Functions and Code Reuse#
Functions are the unit of decomposition in interview code. Clean signatures, correct parameter passing, predictable scope, and knowing when to extract a helper separate readable solutions from brittle ones.
How to use this page
Focus on parameters, scope, mutable defaults, and recursion. Advanced topics: Closures, Decorators.
At a glance
| Track | Python Basics |
| Sections | 10 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: Defining and calling functions · Parameters and arguments — complete model · Return values — patterns and conventions · Scope: LEGB rule in depth · Mutable default argument — must know cold · Functions as first-class objects · Docstrings and documentation · Organizing interview solutions · … (+2 more)
- Defining and calling functions
- Parameters and arguments — complete model
- Return values — patterns and conventions
- Scope: LEGB rule in depth
- Mutable default argument — must know cold
- Functions as first-class objects
- Docstrings and documentation
- Organizing interview solutions
- Recursion — theory and practice
- Partial application and functools
Defining and calling functions#
def greet(name: str, greeting: str = "Hello") -> str:
"""Return a greeting string."""
return f"{greeting}, {name}!"
greet("Alice") # positional
greet("Bob", greeting="Hi") # positional + keyword
greet(greeting="Hey", name="Carol") # all keyword — order free
What happens at def time:
- Function object is created.
- Default argument values are evaluated once and stored on the function object.
- Name
greetis bound to that function object.
Parameters and arguments — complete model#
Python binds arguments in this order:
- Positional parameters
- Keyword parameters matching remaining names
*argscollects extra positional- Keyword-only parameters (after
*or*args) **kwargscollects extra keyword
def full_example(a, b, c=0, /, d, e=0, *args, f, g=0, **kwargs):
"""
a, b — positional-only (before /)
c — positional or keyword
d — positional or keyword (after /)
*args — extra positional
f, g — keyword-only (after *args)
**kwargs — extra keyword
"""
pass
| Kind | Syntax | Call example |
|---|---|---|
| Positional | def f(a, b) |
f(1, 2) |
| Default | def f(a, b=0) |
f(1) |
| Positional-only | def f(a, /, b) |
f(1, 2) not f(a=1, b=2) for a |
| Keyword-only | def f(*, kw) |
f(kw=1) required |
*args |
def f(*args) |
extra positional → tuple |
**kwargs |
def f(**kwargs) |
extra keyword → dict |
Unpacking at call site#
def connect(host, port, timeout=30):
...
args = ("localhost", 8080)
connect(*args)
connect(*args, timeout=60)
config = {"host": "localhost", "port": 8080, "timeout": 5}
connect(**config)
# Mixed
connect("localhost", **{"port": 8080})
Argument passing semantics — critical for interviews#
Python is call by object reference (often called "pass by assignment"):
| Argument type | Reassigning parameter | Mutating parameter |
|---|---|---|
Immutable (int, str, tuple) |
Caller unaffected | N/A — can't mutate |
Mutable (list, dict, set) |
Caller unaffected | Caller sees change |
def rebind(lst):
lst = [99] # local rebinding only
def mutate(lst):
lst.append(99) # mutates caller's object
nums = [1, 2, 3]
rebind(nums) # nums still [1, 2, 3]
mutate(nums) # nums is [1, 2, 3, 99]
Three strategies when you must not mutate caller data:
def safe_process(nums: list[int]) -> list[int]:
# 1. Copy inside function
nums = nums.copy()
nums.sort()
return nums
def safe_process2(nums: list[int]) -> list[int]:
# 2. Build new list
return sorted(nums)
def safe_process3(nums: list[int]) -> None:
# 3. Document mutation intent clearly
nums.sort() # in-place by design
Deep dive: Parameter Unpacking.
Return values — patterns and conventions#
def min_max(nums: list[int]) -> tuple[int, int]:
return min(nums), max(nums)
lo, hi = min_max([3, 1, 4]) # tuple unpacking
| Pattern | When to use | Example |
|---|---|---|
| Single value | One result | return count |
| Tuple | Multiple related values | return (i, j) |
| List | Variable-length collection | return result |
| Early return | Invalid input / found answer | return -1 |
None |
Void / not found sentinel | return None |
| Raise exception | Invalid caller contract | raise ValueError |
Return nothing vs return None#
def log(msg):
print(msg)
# implicit return None
def find(nums, target):
for i, x in enumerate(nums):
if x == target:
return i
return None # explicit "not found"
Functions without return execute return None implicitly.
Multiple return paths — keep consistent types#
# Good — always returns list
def two_sum(nums, target):
...
return [i, j]
...
return []
# Risky — sometimes list, sometimes None
def two_sum_bad(nums, target):
...
return [i, j]
...
return None # caller must handle both
Scope: LEGB rule in depth#
Python resolves names in this order:
- Local — inside current function
- Enclosing — outer nested functions
- Global — module level
- Built-in —
len,print,Exception, …
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # local
inner()
print(x) # enclosing
outer()
print(x) # global
The UnboundLocalError trap#
Any assignment to a name in a function makes that name local for the entire function:
count = 0
def broken():
print(count) # UnboundLocalError — count is local due to +=
count += 1
def fixed_global():
global count
count += 1
def fixed_nonlocal():
count = 0
def inner():
nonlocal count
count += 1
inner()
| Declaration | Scope affected |
|---|---|
global x |
Module-level name |
nonlocal x |
Enclosing function's name (not global) |
Interview preference: pass state as parameters and return updates — avoid global/nonlocal unless implementing closures intentionally.
See Variable Scope and nonlocal Keywords.
Mutable default argument — must know cold#
Interview trap
Fix — sentinel pattern:
Alternative — use immutable sentinel:
Use None when None isn't a valid argument value.
Same trap applies to mutable class attributes — see Introdicion to OOPs.
Functions as first-class objects#
Functions are objects with attributes:
def greet(name):
return f"Hello, {name}"
greet("Alice")
greet.__name__ # 'greet'
greet.__doc__
callable(greet) # True
say_hi = greet # alias
operations = {"greet": greet, "len": len}
operations["greet"]("Bob")
Higher-order functions#
def apply(fn, value):
return fn(value)
apply(abs, -5)
apply(lambda x: x * 2, 10)
# sorted key
sorted(words, key=str.lower)
sorted(points, key=lambda p: p[0])
Details: Higher Order Functions, Lambda Functions.
Docstrings and documentation#
def merge_intervals(intervals: list[list[int]]) -> list[list[int]]:
"""Merge overlapping intervals.
Args:
intervals: List of [start, end] pairs, possibly unsorted.
Returns:
Merged non-overlapping intervals sorted by start.
Raises:
ValueError: If any interval has start > end.
"""
...
| Style | Use |
|---|---|
| One-liner | Simple helpers |
| Google/NumPy style | Multi-arg functions |
| Type hints | Contract in signature |
help(fn) and fn.__doc__ read the docstring. In timed interviews, one-line docstrings on non-trivial functions suffice.
Organizing interview solutions#
LeetCode-style class template#
class Solution:
def twoSum(self, nums: list[int], target: int) -> list[int]:
seen: dict[int, int] = {}
for i, n in enumerate(nums):
need = target - n
if need in seen:
return [seen[need], i]
seen[n] = i
return []
if __name__ == "__main__":
s = Solution()
assert s.twoSum([2, 7, 11, 15], 9) == [0, 1]
Functional style (no class)#
def two_sum(nums: list[int], target: int) -> list[int]:
...
if __name__ == "__main__":
assert two_sum([2, 7, 11, 15], 9) == [0, 1]
Use whichever the platform provides. Extract helpers when:
- Logic repeats 2+ times
- A block exceeds ~15 lines
- A nested loop needs a descriptive name (
is_valid,dfs,backtrack)
Recursion — theory and practice#
Structure: base case + recursive case that progresses toward base.
| Concern | Detail |
|---|---|
| Base case | Missing → RecursionError: maximum recursion depth exceeded |
| Stack depth | CPython default ~1000 frames |
| Tail recursion | Not optimized in CPython — don't rely on it |
| Memoization | @cache, @lru_cache, or explicit dict |
Fibonacci — three approaches#
# 1. Naive recursion O(2^n)
def fib_naive(n):
if n < 2:
return n
return fib_naive(n - 1) + fib_naive(n - 2)
# 2. Memoized recursion O(n)
from functools import cache
@cache
def fib_memo(n):
if n < 2:
return n
return fib_memo(n - 1) + fib_memo(n - 2)
# 3. Iterative O(n) space O(1)
def fib_iter(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
Prefer iterative or memoized in interviews — state complexity aloud.
Tree/graph DFS template#
def dfs(node: TreeNode | None) -> int:
if node is None:
return 0
left = dfs(node.left)
right = dfs(node.right)
return 1 + max(left, right) # example: max depth
Backtracking template#
def backtrack(index: int, path: list[int], ...):
if base_condition:
result.append(path[:]) # copy!
return
for choice in choices:
path.append(choice)
backtrack(index + 1, path, ...)
path.pop() # undo
Full treatment: Recursion, Backtracking.
Increasing recursion limit (avoid in interviews)#
Partial application and functools#
from functools import partial
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
square(5) # 25
See Partial Functions.
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
| Mutable default | Shared list/dict across calls | None sentinel |
| Mutating caller's list | Side effects | Copy or document intent |
global mutation |
Hidden coupling | Pass/return state |
| Missing return | Implicit None |
Explicit return on all paths |
| Shadowing builtins | sum = 0 breaks sum() |
Never reuse builtin names |
| UnboundLocalError | Assign makes name local | Declare nonlocal/global or rename |
| Recursion depth | Stack overflow | Memoize or iterate |
Forgetting path[:] in backtrack |
Shared mutable result | Append copy to results |
Mental model checklist#
- Are default arguments evaluated once or per call?
- What happens if you mutate a list passed to a function?
- What is LEGB order?
- Why does
count += 1inside a function fail withoutglobal? - When should you use recursion vs iteration?
- Why copy
pathbefore appending to results in backtracking?
What's next#
| Topic | Page |
|---|---|
| Exceptions in functions | Basic Error Handling |
| Closures and decorators | Closures |
| Type hints in depth | Function Annotations and Type Hints |
| Classes for state | Introdicion to OOPs |