Functions, Recursion & Error Handling
Learn to write reusable code, handle errors gracefully, and work with files in Python.
Functions in Python
# Default parameters def greet(name="Guest"): print("Hello", name) # *args — variable positional arguments def add_all(*args): return sum(args) # **kwargs — keyword variable arguments def student_info(**kwargs): for k, v in kwargs.items(): print(k, ":", v) # Lambda (one-line function) square = lambda x: x**2
print() displays output but can't be reused. return sends value back to the caller. Always prefer return in DSA functions.Scope, Namespaces & Docstrings
A namespace is a mapping from names to objects. Python keeps several at once, and resolves a name by searching them in a fixed order known as LEGB: Local, then Enclosing, then Global, then Built-in.
x = 'global' # G def outer(): x = 'enclosing' # E def inner(): x = 'local' # L print(x) # local wins inner() print(x) # enclosing outer() print(x) # global print(len) # B — built-in, found last
Assignment creates a local name, which is why reading a global then assigning to it raises UnboundLocalError. Two keywords opt out: global rebinds the module-level name, nonlocal rebinds the nearest enclosing one.
count = 0 def bump(): global count # without this: UnboundLocalError count += 1 def counter(): n = 0 def step(): nonlocal n # rebinds counter's n, not a new local n += 1 return n return step
global is usually a design smell — it means a function is communicating through module state rather than arguments and return values. nonlocal is different: it is the ordinary way a closure keeps state.A docstring is a string literal as the first statement of a module, class or function. Unlike a # comment it survives into the object as __doc__, which is what help(), IDE tooltips and documentation generators read.
def area(radius): """Return the area of a circle. Args: radius: Distance from centre to edge, in metres. Returns: The area in square metres. Raises: ValueError: If radius is negative. """ if radius < 0: raise ValueError('radius must be non-negative') return 3.14159 * radius ** 2 help(area) # prints the docstring area.__doc__ # the same text, as a string
Recursion in Python
Recursion — a function calls itself to solve a smaller version of the same problem. Every recursive function needs a Base Case and a Recursive Case.
For factorial(3):
factorial(3) = 3 * factorial(2)
= 3 * 2 * factorial(1)
= 3 * 2 * 1
= 6
# Stack grows then shrinks:
# Push: factorial(3) → factorial(2) → factorial(1)
# Pop: returns 1 → 2 → 6
def factorial(n): if n == 1: return 1 # Base case return n * factorial(n - 1) # Recursive case def fibonacci(n): if n == 0: return 0 if n == 1: return 1 return fibonacci(n-1) + fibonacci(n-2) def sum_digits(n): if n == 0: return 0 return (n % 10) + sum_digits(n // 10) print(sum_digits(1234)) # 10
| Aspect | Recursion | Iteration |
|---|---|---|
| Approach | Function calls itself | Loops (for, while) |
| Memory | Uses stack O(n) | More efficient O(1) |
| Best For | Trees, Graphs, Backtracking | Simple repetition |
| Risk | Stack overflow | Infinite loops |
| Readability | Cleaner for complex problems | Simpler for basic tasks |
# 1. What is recursion? A function calling itself to solve smaller subproblems. # 2. Key components? Base Case (stops) + Recursive Case (calls itself) # 3. What happens without base case? Stack Overflow Error (memory exhausted) # 4. Time complexity? Depends on number of recursive calls. Factorial: O(n), Fibonacci (naive): O(2ⁿ) # 5. What is tail recursion? Recursive call is LAST operation. Easier to optimize. Python does NOT optimize tail recursion. # 6. What is memoization? Caching results to avoid recomputation. Reduces Fibonacci from O(2ⁿ) to O(n). # 7. When to avoid recursion? - High memory usage risk - Stack overflow possible - Simple loop works better
Exception Handling
Structure: try → except → else → finally. The finally block always runs — perfect for cleanup.
| Exception | Meaning |
|---|---|
ZeroDivisionError | Dividing by zero |
ValueError | Invalid value (e.g. int("abc")) |
TypeError | Wrong data type |
IndexError | Invalid list index |
KeyError | Invalid dictionary key |
FileNotFoundError | File does not exist |
NameError | Variable not defined |
File Handling in Python
with open("data.txt", "w") as file: file.write("Hello World\n") with open("data.txt", "r") as file: for line in file: print(line.strip()) # File existence check import os if os.path.exists("data.txt"): os.remove("data.txt")
with statement — it automatically closes the file even on errors, preventing memory leaks.