01. Python Refresher — Classes, Exceptions, Timing, Recursion
2413 words
12 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# 01. Python Refresher — Classes, Exceptions, Timing, Recursion > **What problem does this solve?** This course assumes you can write Python.

01. Python Refresher — Classes, Exceptions, Timing, Recursion
What problem does this solve? This course assumes you can write Python. This module bridges the gap between basic Python and the DSA-focused Python used throughout the rest of the course: classes for implementing data structures, exception handling for robust code, timing for performance measurement, and recursion as the foundation of divide-and-conquer algorithms.
1. Classes & Objects — The Building Blocks of Data Structures
Mental Model
A class is a blueprint. An object is an instance of that blueprint — it lives in memory with its own copy of the class's data. Think of a class as a cookie cutter and objects as the cookies.
Syntax Reference
python# runnable class Node: """A node in a linked list.""" def __init__(self, data): self.data = data # instance variable self.next = None # default value def __repr__(self): return f"Node({self.data})" # Create objects n1 = Node(10) n2 = Node(20) n1.next = n2 print(n1) # Node(10) print(n1.next) # Node(20)
5 Progressively Complex Examples
Example 1: Simple Counter Class
python# runnable class Counter: def __init__(self): self.count = 0 def increment(self): self.count += 1 def reset(self): self.count = 0 def value(self): return self.count c = Counter() c.increment() c.increment() print(c.value()) # 2 c.reset() print(c.value()) # 0
Example 2: Stack Class (LIFO)
python# runnable class Stack: def __init__(self): self._items = [] # underscore = "private by convention" def push(self, item): self._items.append(item) def pop(self): if self.is_empty(): raise IndexError("pop from empty stack") return self._items.pop() def peek(self): if self.is_empty(): raise IndexError("peek from empty stack") return self._items[-1] def is_empty(self): return len(self._items) == 0 def __len__(self): return len(self._items) s = Stack() s.push(1) s.push(2) s.push(3) print(s.pop()) # 3 print(len(s)) # 2 print(s.peek()) # 2
Example 3: Class with
__str__ and __repr__python# runnable class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Point({self.x}, {self.y})" def __str__(self): return f"({self.x}, {self.y})" def distance_from_origin(self): return (self.x**2 + self.y**2) ** 0.5 p = Point(3, 4) print(repr(p)) # Point(3, 4) print(str(p)) # (3, 4) print(p) # (3, 4) — calls __str__ print(p.distance_from_origin()) # 5.0
Example 4: Inheritance for Specialized Nodes
python# runnable class TreeNode: def __init__(self, value): self.value = value self.left = None self.right = None class BSTNode(TreeNode): """TreeNode with BST property.""" def insert(self, value): if value < self.value: if self.left is None: self.left = BSTNode(value) else: self.left.insert(value) elif value > self.value: if self.right is None: self.right = BSTNode(value) else: self.right.insert(value) # Equal → don't insert (no duplicates) root = BSTNode(10) root.insert(5) root.insert(15) root.insert(3) print(root.left.value) # 5 print(root.left.left.value) # 3
Example 5: Dunder Methods for a Custom List-like Class
python# runnable class ArrayList: def __init__(self): self._data = [] def __getitem__(self, index): return self._data[index] def __setitem__(self, index, value): self._data[index] = value def __len__(self): return len(self._data) def append(self, value): self._data.append(value) def __repr__(self): return f"ArrayList({self._data})" arr = ArrayList() arr.append(10) arr.append(20) arr.append(30) print(arr[1]) # 20 arr[1] = 99 print(arr) # ArrayList([10, 99, 30]) print(len(arr)) # 3
Comparison: Class vs dict vs namedtuple
| Feature | Class | dict | namedtuple |
|---|---|---|---|
| Attribute access | obj.field | d["key"] | obj.field |
| Method definitions | Yes | No | No |
| Mutable | Yes | Yes | No (tuple-like) |
| Memory overhead | Moderate | Low | Low |
| Serialization | Manual | Built-in JSON | Manual |
| Best for | DS with behavior | Simple key-value | Lightweight records |
Common Bugs
python# BUG 1: Forgetting self class Bad: def __init__(self, x): self.x = x def double(): # Missing self! return self.x * 2 # TypeError: double() takes 0 positional arguments but 1 was given # FIX: class Good: def __init__(self, x): self.x = x def double(self): return self.x * 2 # BUG 2: Mutable default arguments class BuggyStack: def __init__(self, items=[]): # Same list shared by all instances! self.items = items # >>> a = BuggyStack(); b = BuggyStack() # >>> a.items.append(1); print(b.items) # [1] — WRONG! # FIX: class CorrectStack: def __init__(self, items=None): self.items = items if items is not None else [] # BUG 3: Modifying class variable through instance creates instance variable class Node: count = 0 # class variable def __init__(self): Node.count += 1 n1 = Node() n2 = Node() n1.count = 99 # Creates INSTANCE variable — doesn't change class var print(Node.count) # 2 (correct count) print(n1.count) # 99 (shadowed)
2. Exception Handling
Mental Model
When an error occurs, Python raises an exception. If you don't catch it, the program crashes. You catch exceptions with
try/except blocks, similar to how you'd catch a ball thrown at you.Syntax Reference
pythontry: risky_operation() except SomeError as e: # Handle the error print(f"Error: {e}") finally: # Always runs (cleanup) cleanup_code()
5 Examples
Example 1: Basic try/except
python# runnable def divide(a, b): try: result = a / b return result except ZeroDivisionError: return "Cannot divide by zero!" print(divide(10, 2)) # 5.0 print(divide(10, 0)) # Cannot divide by zero!
Example 2: Multiple Exception Types
python# runnable def safe_int_conversion(value): try: return int(value) except ValueError: return f"'{value}' is not a valid integer" except TypeError: return f"'{value}' has wrong type ({type(value).__name__})" print(safe_int_conversion("42")) # 42 print(safe_int_conversion("abc")) # 'abc' is not a valid integer print(safe_int_conversion([1,2])) # '[1, 2]' has wrong type (list)
Example 3: try/except/else/finally
python# runnable def read_config(filename): try: f = open(filename, 'r') except FileNotFoundError: return None else: # Only runs if no exception occurred content = f.read() return content finally: # Always runs if 'f' in locals() and not f.closed: f.close() print(read_config("nonexistent.txt")) # None
Example 4: Raising Custom Exceptions
python# runnable class EmptyStackError(Exception): """Raised when trying to pop from an empty stack.""" pass class Stack: def __init__(self): self._data = [] def pop(self): if not self._data: raise EmptyStackError("Cannot pop from empty stack") return self._data.pop() s = Stack() try: s.pop() except EmptyStackError as e: print(f"Error: {e}") # Error: Cannot pop from empty stack
Example 5: Context Manager (
with statement)python# runnable # The 'with' statement automatically handles cleanup with open("demo.txt", "w") as f: f.write("Hello, exceptions!") # File is automatically closed even if an error occurs with open("demo.txt", "r") as f: print(f.read()) # Hello, exceptions!
Common Bugs
python# BUG 1: Catching too broadly try: user_input = input("Enter a number: ") result = 10 / int(user_input) except Exception: # Catches EVERYTHING — hides bugs pass # Never know what went wrong! # FIX: Be specific try: result = 10 / int(user_input) except ValueError: print("Not a number!") except ZeroDivisionError: print("Can't divide by zero!") # BUG 2: Forgetting that except order matters try: risky_code() except Exception: print("Caught") except ValueError: # This is unreachable! print("ValueError") # ValueError is a subclass of Exception, caught by first block # BUG 3: Not finally for resource cleanup f = open("file.txt") try: f.read() except: return # Returns without closing f! finally: f.close() # This always runs
3. Timing Code Execution
Mental Model
To measure an algorithm's performance, we don't use wall-clock time (too variable). Instead, we count basic operations or use Python's
timeit module which runs code multiple times and gives statistical results.Syntax Reference
pythonimport time start = time.time() # code to time end = time.time() elapsed = end - start # OR using timeit (more accurate): import timeit timeit.timeit("code_string", number=1000)
3 Examples
Example 1: Manual Timing
python# runnable import time def sum_upto_n(n): total = 0 for i in range(n + 1): total += i return total start = time.time() result = sum_upto_n(1000000) end = time.time() print(f"Result: {result}, Time: {end - start:.4f} seconds")
Example 2: Using timeit
python# runnable import timeit # Time list creation setup = "n = 1000" stmt1 = "[i**2 for i in range(n)]" stmt2 = "list(map(lambda i: i**2, range(n)))" t1 = timeit.timeit(stmt1, setup, number=10000) t2 = timeit.timeit(stmt2, setup, number=10000) print(f"List comprehension: {t1:.3f}s") print(f"map + lambda: {t2:.3f}s")
Example 3: Timing Sorting Algorithms (Preview)
python# runnable import time, random def timer(func, arr): start = time.time() func(arr) return time.time() - start data = [random.randint(0, 1000) for _ in range(1000)] # We'll see these sorting algorithms in detail later
4. Recursion — The Foundation of DSA
Mental Model
Recursion is when a function calls itself to solve a smaller instance of the same problem. Like Russian nesting dolls: each doll contains a smaller version of itself. Every recursive function needs:
- Base case — when to stop
- Recursive case — the function calls itself on a smaller input
Syntax Reference
pythondef recursive_function(n): # Base case if n <= 1: return 1 # Recursive case return n * recursive_function(n - 1)
5 Progressively Complex Examples
Example 1: Factorial
python# runnable def factorial(n): """Return n! = n * (n-1) * ... * 1""" if n <= 1: # Base case return 1 return n * factorial(n - 1) # Recursive case print(factorial(5)) # 120 # Trace: # factorial(5) = 5 * factorial(4) # = 5 * 4 * factorial(3) # = 5 * 4 * 3 * factorial(2) # = 5 * 4 * 3 * 2 * factorial(1) # = 5 * 4 * 3 * 2 * 1 = 120
Example 2: Fibonacci (Inefficient)
python# runnable def fib(n): """Return the nth Fibonacci number (0-indexed).""" if n <= 1: return n return fib(n - 1) + fib(n - 2) print(fib(10)) # 55 # Problem: fib(5) calls fib(4) and fib(3), which overlap massively! # Complexity: O(2^n) — exponential! We'll fix this with DP later.
Example 3: Binary Search (Recursive)
python# runnable def binary_search(arr, target, left=0, right=None): """Return index of target in sorted arr, or -1 if not found.""" if right is None: right = len(arr) - 1 if left > right: # Base case: empty range return -1 mid = (left + right) // 2 if arr[mid] == target: # Found return mid elif arr[mid] > target: # Search left half return binary_search(arr, target, left, mid - 1) else: # Search right half return binary_search(arr, target, mid + 1, right) print(binary_search([1, 3, 5, 7, 9, 11, 13], 7)) # 3 print(binary_search([1, 3, 5, 7, 9, 11, 13], 4)) # -1
Example 4: Towers of Hanoi
python# runnable def hanoi(n, source, target, auxiliary): """Move n disks from source to target using auxiliary.""" if n == 1: print(f"Move disk 1 from {source} to {target}") return hanoi(n - 1, source, auxiliary, target) print(f"Move disk {n} from {source} to {target}") hanoi(n - 1, auxiliary, target, source) print("Solution for 3 disks:") hanoi(3, 'A', 'C', 'B') # Output shows all 7 moves for 3 disks
Example 5: Recursive vs Iterative — Call Stack Visualization
python# runnable import sys def recursive_sum(n): """Recursive sum of 0..n.""" if n == 0: return 0 return n + recursive_sum(n - 1) def iterative_sum(n): """Iterative sum of 0..n.""" total = 0 for i in range(n + 1): total += i return total print(f"Recursive: {recursive_sum(100)}") # 5050 print(f"Iterative: {iterative_sum(100)}") # 5050 # Note: recursive_sum(1000) might hit RecursionError due to stack depth!
Comparison: Recursion vs Iteration
| Aspect | Recursion | Iteration |
|---|---|---|
| Code clarity | Elegant for self-similar problems | Straightforward |
| Memory | Uses call stack (O(n) memory) | Usually O(1) memory |
| Performance | Function call overhead | Faster (no overhead) |
| Infinite loops | Stack overflow (crash) | Infinite loop (hang) |
| Base case | Needed to terminate | Loop condition |
| Best for | Trees, graphs, divide & conquer | Linear operations |
Common Bugs
python# BUG 1: Missing base case — infinite recursion def bad_factorial(n): return n * bad_factorial(n - 1) # No base case! # >>> bad_factorial(5) # RecursionError: maximum recursion depth exceeded # BUG 2: Base case never reached def bad_power(x, n): if n == 1: return x return x * bad_power(x, n) # n never decreases! # BUG 3: Not returning the recursive result def bad_sum(n): if n <= 1: return n bad_sum(n - 1) + n # Missing return! # Returns None for n > 1
Practice Questions
Q1. Write a class
BankAccount with deposit(), withdraw(), and get_balance() methods. Include proper exception handling for insufficient funds.
Q2. What is the output?pythondef mystery(n): if n <= 0: return 0 return n + mystery(n - 2) print(mystery(6))
Q3. Fix the bug:
pythonclass Team: members = [] # What's wrong? def __init__(self, name): self.name = name def add_member(self, person): self.members.append(person)
Q4. Write a recursive function
is_palindrome(s) that checks if a string reads the same forward and backward.
Q5. Time the following two functions using timeit:pythondef method1(n): return sum(range(n)) def method2(n): return n*(n-1)//2
Which is faster and why?
Q6. What does this exception handler print?
pythontry: print(1/0) except ZeroDivisionError: print("A") except ArithmeticError: print("B") except: print("C")
Q7. Write a class
Queue using a list with enqueue(item) and dequeue() methods. Raise IndexError on empty dequeue.
Q8. Trace the recursive calls for hanoi(2, 'A', 'C', 'B').
Q9. Convert this recursive function to iterative:pythondef countdown(n): if n <= 0: return print(n) countdown(n - 1)
Q10. Explain why
recursive_sum(2000) might fail while iterative_sum(2000) works fine.AnswersA1.pythonclass BankAccount: def __init__(self): self._balance = 0 def deposit(self, amount): if amount <= 0: raise ValueError("Deposit must be positive") self._balance += amount def withdraw(self, amount): if amount <= 0: raise ValueError("Withdrawal must be positive") if amount > self._balance: raise ValueError("Insufficient funds") self._balance -= amount def get_balance(self): return self._balanceA2.12(6 + 4 + 2 + 0 = 12)A3.membersis a class variable shared by all instances. Move it into__init__asself.members = [].A4.pythondef is_palindrome(s): if len(s) <= 1: return True if s[0] != s[-1]: return False return is_palindrome(s[1:-1])A5.method2is O(1),method1is O(n). For large n, method2 is dramatically faster.A6.A—ZeroDivisionErroris caught first.ArithmeticErroris its parent class but the child exception handler matched first.A7.pythonclass Queue: def __init__(self): self._data = [] def enqueue(self, item): self._data.append(item) def dequeue(self): if not self._data: raise IndexError("dequeue from empty queue") return self._data.pop(0)A8.sqlMove disk 1 from A to B Move disk 2 from A to C Move disk 1 from B to CA9.pythondef countdown_iter(n): while n > 0: print(n) n -= 1A10. Python has a recursion limit (default ~1000). Each recursive call consumes stack frame memory.recursive_sum(2000)exceeds this limit. The iterative version uses a single while loop with no stack growth. Join Discord Next02. Algorithm Analysis & Big-O Notation