Decorators · Generators · Iterators · Context Managers
Collections · Databases · GUI · NumPy · Testing
collections Module14Unit III covers the professional Python toolkit. Master decorators, generators and context managers first — they appear in almost every modern Python codebase. Then work through the mini projects: they combine everything from Units I–III. Every example is exam-ready and has been tested on Python 3.10+.
In Python, functions are first-class objects: they can be assigned to variables, passed as arguments, returned from other functions, and stored in data structures.
def greet(name):
return f"Hello, {name}"
# Assign to a variable
say_hi = greet
print(say_hi("Aarav")) # Hello, Aarav
# Store in a data structure
funcs = [greet, len, str.upper]
print(funcs[0]("Diya")) # Hello, Diya
# Pass as an argument
def apply(f, value):
return f(value)
print(apply(greet, "Kabir")) # Hello, Kabir
Closure: an inner function that remembers the values of variables from its enclosing scope, even after the outer function has finished executing.
def make_multiplier(n):
def multiply(x):
return x * n # 'n' is captured from the enclosing scope
return multiply
times3 = make_multiplier(3)
times5 = make_multiplier(5)
print(times3(10)) # 30
print(times5(10)) # 50
# Inspecting the closure
print(times3.__closure__[0].cell_contents) # 3
funcs = []
for i in range(3):
funcs.append(lambda: i) # all lambdas capture the SAME i
print([f() for f in funcs]) # [2, 2, 2] — NOT [0, 1, 2]
# Fix: use a default argument to capture the current value
funcs = []
for i in range(3):
funcs.append(lambda i=i: i)
print([f() for f in funcs]) # [0, 1, 2]
A decorator is a function that takes another function as input, extends or modifies its behaviour without changing its source code, and returns a new function.
def decorator(func): def wrapper(*args, **kwargs): # before result = func(*args, **kwargs) # after return result return wrapper
def my_decorator(func):
def wrapper(*args, **kwargs):
print("--- Before ---")
result = func(*args, **kwargs)
print("--- After ---")
return result
return wrapper
@my_decorator # equivalent to: say = my_decorator(say)
def say(message):
print(message)
say("Hello, World!")
# --- Before ---
# Hello, World!
# --- After ---
functools.wrapsWithout @wraps, the decorated function loses its original __name__ and __doc__:
print(say.__name__) # 'wrapper' — WRONG!
from functools import wraps
def my_decorator(func):
@wraps(func) # preserves metadata
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def say(message):
"""Print a greeting."""
print(message)
print(say.__name__) # 'say'
print(say.__doc__) # 'Print a greeting.'
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
print(slow_sum(1_000_000))
# slow_sum took 0.0231s
# 499999500000
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"CALL {func.__name__}({signature})")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a, b):
return a + b
print(add(3, 5))
# CALL add(3, 5)
# 8
To pass parameters to a decorator, you need three levels of nesting.
from functools import wraps
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def hello(name):
print(f"Hello, {name}!")
hello("Aarav")
# Hello, Aarav!
# Hello, Aarav!
# Hello, Aarav!
import time
from functools import wraps
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt} failed: {e}")
if attempt == max_attempts:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0)
def unstable():
import random
if random.random() < 0.7:
raise ConnectionError("Network glitch")
return "Success"
print(unstable())
| Decorator | Purpose | Applies to |
|---|---|---|
@staticmethod | No implicit first argument | Methods |
@classmethod | Receives cls instead of self | Methods |
@property | Turn a method into a read-only attribute | Methods |
@functools.wraps | Preserve metadata in a wrapper | Decorator inner function |
@functools.lru_cache | Memoize function results | Functions |
@dataclass | Auto-generate __init__, __repr__, __eq__ | Classes |
@property for computed attributesclass Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self): # getter
return self._radius
@radius.setter
def radius(self, value): # setter with validation
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self): # computed, read-only
return 3.14159 * self._radius ** 2
c = Circle(5)
print(c.radius) # 5
print(c.area) # 78.53975
c.radius = 10
print(c.area) # 314.159
try:
c.radius = -1
except ValueError as e:
print("Error:", e)
@dataclass for concise data classesfrom dataclasses import dataclass, field
@dataclass(order=True)
class Student:
name: str
roll: int
marks: list = field(default_factory=list)
def average(self):
return sum(self.marks) / len(self.marks) if self.marks else 0
s1 = Student("Aarav", 101, [88, 92, 79])
s2 = Student("Diya", 102, [95, 81, 90])
print(s1) # Student(name='Aarav', roll=101, marks=[88, 92, 79])
print(s1 == Student("Aarav", 101, [88, 92, 79])) # True
print(sorted([s1, s2])) # sorts by name, then roll
For decorator questions, always mention: (1) the three-level nesting if the decorator takes arguments, (2) @wraps to preserve metadata, (3) *args, **kwargs to accept any signature. A five-mark answer that includes a timing decorator with @wraps is usually full marks.
Python's for loop works on any object that implements the iteration protocol — i.e., provides an __iter__ method returning an iterator, and the iterator provides __next__.
Iterable: an object that can return an iterator (implements __iter__). Examples: list, tuple, set, dict, string, file.
Iterator: an object that produces the next value on demand (implements __next__) and raises StopIteration when exhausted.
nums = [10, 20, 30]
it = iter(nums) # get the iterator
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
# print(next(it)) # StopIteration
# A for loop does exactly this behind the scenes
for n in nums:
print(n)
class Countdown:
"""Iterator that counts down from n to 1."""
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for n in Countdown(5):
print(n, end=" ")
# 5 4 3 2 1
Iterators are lazy: they produce values one at a time and do not store the entire sequence in memory. This makes them ideal for large data streams, files and infinite sequences.
A generator is a function that uses yield instead of return. Calling it does not execute the body; it returns a generator object that produces values lazily.
return ends the function permanently. • yield pauses the function, remembers its state, and resumes on the next next().
def countdown(n):
while n > 0:
yield n
n -= 1
for value in countdown(5):
print(value, end=" ") # 5 4 3 2 1
print()
gen = countdown(3)
print(next(gen)) # 3
print(next(gen)) # 2
print(next(gen)) # 1
Like list comprehensions, but with parentheses — lazy and memory-efficient.
# List comprehension: builds the entire list
squares_list = [x ** 2 for x in range(1_000_000)] # ~8 MB
# Generator expression: computes on demand
squares_gen = (x ** 2 for x in range(1_000_000)) # tiny memory
print(sum(squares_gen)) # 333332833333500000
# Chaining generators
def read_lines(filename):
with open(filename) as f:
for line in f:
yield line.rstrip()
def non_empty(lines):
for line in lines:
if line:
yield line
# for line in non_empty(read_lines("data.txt")):
# print(line)
yield from and Generator Delegationdef chain(*iterables):
for it in iterables:
yield from it # delegates to each sub-iterable
print(list(chain([1, 2], "ab", (3, 4))))
# [1, 2, 'a', 'b', 3, 4]
def accumulator():
total = 0
while True:
value = yield total
if value is None:
break
total += value
acc = accumulator()
next(acc) # prime the generator
print(acc.send(10)) # 10
print(acc.send(20)) # 30
print(acc.send(5)) # 35
def fibonacci():
a, b = 0, 1
while True: # infinite!
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib), end=" ")
# 0 1 1 2 3 5 8 13 21 34
def read_large_file(filename):
"""Yield one line at a time without loading the whole file."""
with open(filename, "r") as f:
for line in f:
yield line.rstrip()
total_words = 0
for line in read_large_file("bigdata.txt"):
total_words += len(line.split())
print("Total words:", total_words)
def primes(limit):
"""Yield all primes up to 'limit' using the sieve idea lazily."""
for n in range(2, limit + 1):
if all(n % d != 0 for d in range(2, int(n ** 0.5) + 1)):
yield n
print(list(primes(30)))
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
import sys
list_comp = [x for x in range(10_000)]
gen_expr = (x for x in range(10_000))
print("List size :", sys.getsizeof(list_comp), "bytes") # ~85 KB
print("Gen size :", sys.getsizeof(gen_expr), "bytes") # ~200 bytes
| Concept | Implements | Reusable? | Memory |
|---|---|---|---|
| Iterable (list, tuple) | __iter__ | Yes | Stores all elements |
| Iterator | __iter__, __next__ | No — exhausts once | Produces on demand |
| Generator | Created by yield | No — exhausts once | Produces on demand |
When asked “why use generators?”, answer: lazy evaluation, low memory footprint, clean syntax for streaming data, and ability to represent infinite sequences. Contrast with lists which hold everything in memory.
A context manager is an object that defines the runtime context for a with block — guaranteeing that setup and cleanup happen automatically, even if an exception occurs.
with Statementwith context_manager as variable: body
# Without context manager - manual cleanup (error-prone)
f = open("data.txt")
try:
content = f.read()
finally:
f.close()
# With context manager - automatic cleanup
with open("data.txt") as f:
content = f.read()
Any class implementing __enter__ and __exit__ can be used with with.
| Method | Called when | Returns |
|---|---|---|
__enter__(self) | Entering the block | Value bound to as variable |
__exit__(self, exc_type, exc_val, tb) | Leaving the block (normal or exception) | True to suppress exception, False to propagate |
import time
class Timer:
def __init__(self, label="Elapsed"):
self.label = label
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, tb):
self.elapsed = time.perf_counter() - self.start
print(f"{self.label}: {self.elapsed:.4f}s")
return False # don't suppress exceptions
with Timer("Sum computation"):
total = sum(range(10_000_000))
print("Total =", total)
import sqlite3
class Transaction:
def __init__(self, db_path):
self.db_path = db_path
self.conn = None
def __enter__(self):
self.conn = sqlite3.connect(self.db_path)
return self.conn
def __exit__(self, exc_type, exc_val, tb):
if exc_type is None:
self.conn.commit()
print("Transaction committed.")
else:
self.conn.rollback()
print(f"Rolled back due to: {exc_val}")
self.conn.close()
return False
with Transaction("bank.db") as conn:
cur = conn.cursor()
cur.execute("CREATE TABLE IF NOT EXISTS accounts (id INTEGER, balance REAL)")
cur.execute("INSERT INTO accounts VALUES (1, 5000)")
cur.execute("UPDATE accounts SET balance = balance - 1000 WHERE id = 1")
contextlib ModulePython's contextlib provides helpers that avoid writing a full class.
@contextmanager decoratorfrom contextlib import contextmanager
import time
@contextmanager
def timer(label="Elapsed"):
start = time.perf_counter()
try:
yield start # everything before 'yield' is __enter__
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.4f}s") # everything after is __exit__
with timer("Loop"):
sum(range(1_000_000))
contextlib.suppressfrom contextlib import suppress
import os
with suppress(FileNotFoundError):
os.remove("does_not_exist.txt")
print("Continued without error.")
contextlib.ExitStack — dynamic resource managementfrom contextlib import ExitStack
filenames = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
files = [stack.enter_context(open(f, "w")) for f in filenames]
for i, f in enumerate(files):
f.write(f"File {i}\n")
# All files closed automatically on exit
contextlib.redirect_stdoutimport io
from contextlib import redirect_stdout
buffer = io.StringIO()
with redirect_stdout(buffer):
print("This goes to the buffer")
print("So does this")
print("Captured:")
print(buffer.getvalue())
| Context Manager | Purpose |
|---|---|
open() | File I/O with automatic close |
threading.Lock() | Acquire/release a thread lock |
sqlite3.connect() | Database transaction (commits on success) |
decimal.localcontext() | Temporary decimal precision |
unittest.mock.patch() | Temporary monkey-patching in tests |
warnings.catch_warnings() | Capture and manage warnings |
When asked about context managers, mention both flavours: class-based (__enter__/__exit__) and generator-based (@contextmanager with yield). The latter is more concise; the former gives more control.
collections ModuleThe collections module provides specialised container data types that outperform generic lists, dicts and tuples for specific use cases.
Counter — Frequency Countingfrom collections import Counter
text = "mississippi"
c = Counter(text)
print(c) # Counter({'i':4, 's':4, 'p':2, 'm':1})
print(c.most_common(2)) # [('i', 4), ('s', 4)]
print(c['i']) # 4
print(c['z']) # 0 (no KeyError!)
# Arithmetic
c1 = Counter("aabbc")
c2 = Counter("abcc")
print(c1 + c2) # Counter({'a':3, 'b':3, 'c':3})
print(c1 - c2) # Counter({'a':1, 'b':1})
print(c1 & c2) # Counter({'a':1, 'b':1, 'c':1})
print(c1 | c2) # Counter({'a':2, 'b':2, 'c':2})
from collections import Counter
import re
text = "the quick brown fox jumps over the lazy dog the fox"
words = re.findall(r"\w+", text.lower())
for word, n in Counter(words).most_common(3):
print(f"{word:<8} {n}")
defaultdict — Auto-initialising DictionaryLike a dictionary but inserts a default value automatically when a missing key is accessed.
from collections import defaultdict
# Group words by their first letter
words = ["apple", "banana", "avocado", "blueberry", "cherry"]
groups = defaultdict(list)
for w in words:
groups[w[0]].append(w)
print(dict(groups))
# {'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}
# Count with int default
counts = defaultdict(int)
for ch in "abracadabra":
counts[ch] += 1
print(dict(counts)) # {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}
defaultdict(list) creates a new list for each missing key. Plain dict would raise KeyError. Common defaults: int (0), list ([]), set (set()), lambda: 0.
OrderedDictPreserves insertion order (guaranteed since Python 3.7, so dict covers most cases). Still useful for: (1) explicit intent, (2) move_to_end, (3) order-sensitive equality.
from collections import OrderedDict
od = OrderedDict()
od["b"] = 2
od["a"] = 1
od["c"] = 3
print(list(od.keys())) # ['b', 'a', 'c']
od.move_to_end("b")
print(list(od.keys())) # ['a', 'c', 'b']
od.move_to_end("c", last=False)
print(list(od.keys())) # ['c', 'a', 'b']
namedtupleTuple subclass with named fields — combines tuple immutability with attribute access.
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y) # 3 4
print(p[0], p[1]) # 3 4 (still indexable)
print(p) # Point(x=3, y=4)
print(p._asdict()) # {'x': 3, 'y': 4}
print(p._replace(x=10)) # Point(x=10, y=4)
deque — Double-Ended QueueFast \(O(1)\) appends and pops from both ends. Ideal for queues, sliding windows and undo stacks.
from collections import deque
d = deque([1, 2, 3])
d.append(4) # right side
d.appendleft(0) # left side
print(d) # deque([0, 1, 2, 3, 4])
d.pop() # remove from right
d.popleft() # remove from left
print(d) # deque([1, 2, 3])
# Rotating
d.rotate(1)
print(d) # deque([3, 1, 2])
# Bounded deque — perfect for a sliding window
last3 = deque(maxlen=3)
for i in range(1, 7):
last3.append(i)
print(list(last3))
# [1] [1,2] [1,2,3] [2,3,4] [3,4,5] [4,5,6]
ChainMap — Layered Lookupsfrom collections import ChainMap
defaults = {"color": "black", "size": "M"}
user_pref = {"color": "red"}
combined = ChainMap(user_pref, defaults)
print(combined["color"]) # red (user_pref wins)
print(combined["size"]) # M (falls back to defaults)
collections| Class | Purpose | Replaces |
|---|---|---|
Counter | Count hashable objects | Manual dict counting |
defaultdict | Dict with default factory | dict.setdefault() |
OrderedDict | Order-aware dict | dict (mostly) |
namedtuple | Lightweight records | Plain tuples / small classes |
deque | Fast double-ended queue | List used as queue |
ChainMap | Layered dict lookups | Nested dict access |
Know at least three: Counter (frequency), defaultdict (grouping), deque (queue/stack with O(1) ends). Show a 3–4 line example for each.
Python's built-ins cover most needs, but interviews and DSA questions often require implementing classic data structures from scratch.
Last-In-First-Out. Push and pop from the same end.
class Stack:
def __init__(self):
self.items = []
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):
return self.items[-1] if self.items else None
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.peek()) # 3
print(s.pop()) # 3
print(len(s)) # 2
Classic application: balanced parentheses checker.
def is_balanced(expr):
pairs = {")": "(", "]": "[", "}": "{"}
stack = Stack()
for ch in expr:
if ch in "([{":
stack.push(ch)
elif ch in pairs:
if stack.is_empty() or stack.pop() != pairs[ch]:
return False
return stack.is_empty()
print(is_balanced("{[()]}")) # True
print(is_balanced("([)]")) # False
First-In-First-Out. Insert at the rear, remove from the front.
from collections import deque
class Queue:
def __init__(self):
self.items = deque()
def enqueue(self, item):
self.items.append(item)
def dequeue(self):
if self.is_empty():
raise IndexError("dequeue from empty queue")
return self.items.popleft()
def front(self):
return self.items[0] if self.items else None
def is_empty(self):
return len(self.items) == 0
q = Queue()
q.enqueue("A"); q.enqueue("B"); q.enqueue("C")
print(q.dequeue()) # A
print(q.front()) # B
list.pop(0) is \(O(n)\) because all elements shift. Use collections.deque for \(O(1)\) popleft.
import heapq
class PriorityQueue:
def __init__(self):
self.heap = []
self.counter = 0 # tie-breaker for equal priorities
def push(self, priority, item):
heapq.heappush(self.heap, (priority, self.counter, item))
self.counter += 1
def pop(self):
if not self.heap:
raise IndexError("pop from empty priority queue")
return heapq.heappop(self.heap)[2]
pq = PriorityQueue()
pq.push(3, "low")
pq.push(1, "high")
pq.push(2, "medium")
while pq.heap:
print(pq.pop())
# high
# medium
# low
A chain of nodes where each node holds a value and a reference to the next node. Enables \(O(1)\) insertion at the head.
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def append(self, data):
node = Node(data)
if not self.head:
self.head = node
return
current = self.head
while current.next:
current = current.next
current.next = node
def prepend(self, data):
node = Node(data)
node.next = self.head
self.head = node
def delete(self, data):
if not self.head:
return
if self.head.data == data:
self.head = self.head.next
return
current = self.head
while current.next and current.next.data != data:
current = current.next
if current.next:
current.next = current.next.next
def __iter__(self):
current = self.head
while current:
yield current.data
current = current.next
def __str__(self):
return " -> ".join(map(str, self))
ll = LinkedList()
ll.append(1); ll.append(2); ll.append(3)
ll.prepend(0)
print(ll) # 0 -> 1 -> 2 -> 3
ll.delete(2)
print(ll) # 0 -> 1 -> 3
Each node has at most two children: left values are smaller, right values are larger.
class BSTNode:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
class BST:
def __init__(self):
self.root = None
def insert(self, value):
self.root = self._insert(self.root, value)
def _insert(self, node, value):
if node is None:
return BSTNode(value)
if value < node.value:
node.left = self._insert(node.left, value)
elif value > node.value:
node.right = self._insert(node.right, value)
return node
def search(self, value):
return self._search(self.root, value)
def _search(self, node, value):
if node is None or node.value == value:
return node is not None
if value < node.value:
return self._search(node.left, value)
return self._search(node.right, value)
def inorder(self):
"""Returns sorted values."""
result = []
self._inorder(self.root, result)
return result
def _inorder(self, node, out):
if node:
self._inorder(node.left, out)
out.append(node.value)
self._inorder(node.right, out)
tree = BST()
for v in [50, 30, 70, 20, 40, 60, 80]:
tree.insert(v)
print(tree.inorder()) # [20, 30, 40, 50, 60, 70, 80]
print(tree.search(60)) # True
print(tree.search(99)) # False
| Structure | Search | Insert | Delete | Notes |
|---|---|---|---|---|
| Stack (list) | \(O(n)\) | \(O(1)\) push | \(O(1)\) pop | LIFO |
| Queue (deque) | \(O(n)\) | \(O(1)\) | \(O(1)\) | FIFO |
| Heap | \(O(n)\) | \(O(\log n)\) | \(O(\log n)\) pop-min | Priority queue |
| Linked List | \(O(n)\) | \(O(1)\) head | \(O(n)\) | No random access |
| BST (balanced) | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) | Inorder = sorted |
| BST (degenerate) | \(O(n)\) | \(O(n)\) | \(O(n)\) | Becomes a list |
When asked to "implement a stack/queue", give both the class definition and at least two operations with sample output. For linked lists, remember that __iter__ with yield makes traversal clean.
Break the problem into independent subproblems, solve recursively, and combine.
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(a, b):
result, i, j = [], 0, 0
while i < len(a) and j < len(b):
if a[i] <= b[j]:
result.append(a[i]); i += 1
else:
result.append(b[j]); j += 1
result.extend(a[i:])
result.extend(b[j:])
return result
print(merge_sort([38, 27, 43, 3, 9, 82, 10]))
# [3, 9, 10, 27, 38, 43, 82]
Break the problem into overlapping subproblems and store results to avoid recomputation. Two styles: top-down (memoization) and bottom-up (tabulation).
from functools import lru_cache
# Naive: O(2^n)
def fib_naive(n):
return n if n <= 1 else fib_naive(n-1) + fib_naive(n-2)
# Top-down DP: O(n)
@lru_cache(maxsize=None)
def fib_memo(n):
return n if n <= 1 else fib_memo(n-1) + fib_memo(n-2)
# Bottom-up DP: O(n), O(1) space
def fib_tab(n):
if n <= 1:
return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
print(fib_naive(10), fib_memo(50), fib_tab(50))
Given weights and values, select items to maximise value without exceeding capacity.
def knapsack(weights, values, capacity):
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(capacity + 1):
if weights[i-1] <= w:
dp[i][w] = max(
dp[i-1][w],
dp[i-1][w - weights[i-1]] + values[i-1]
)
else:
dp[i][w] = dp[i-1][w]
return dp[n][capacity]
weights = [1, 3, 4, 5]
values = [1, 4, 5, 7]
print(knapsack(weights, values, 7)) # 9
def lcs(s1, s2):
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
print(lcs("AGGTAB", "GXTXAYB")) # 4 (GTAB)
Make the locally optimal choice at each step, hoping it leads to a global optimum. Works only when the problem has the greedy-choice property.
def activity_selection(activities):
"""activities = list of (start, finish)."""
activities.sort(key=lambda x: x[1]) # sort by finish time
selected = [activities[0]]
last_finish = activities[0][1]
for start, finish in activities[1:]:
if start >= last_finish:
selected.append((start, finish))
last_finish = finish
return selected
acts = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11)]
print(activity_selection(acts))
# [(1, 4), (5, 7), (8, 11)]
def coin_change_greedy(coins, amount):
coins.sort(reverse=True)
result = []
for c in coins:
while amount >= c:
amount -= c
result.append(c)
return result if amount == 0 else None
print(coin_change_greedy([1, 2, 5, 10, 20, 50], 93))
# [50, 20, 20, 2, 1]
Caution: greedy works for canonical systems like ₹1,2,5,10,20,50. For arbitrary denominations (e.g., [1,3,4] with target 6), DP is required.
Explore all possibilities by building a solution incrementally and abandoning a path (backtracking) as soon as it cannot lead to a valid solution.
def solve_n_queens(n):
solutions = []
board = [-1] * n # board[row] = column of queen in that row
def is_safe(row, col):
for r in range(row):
c = board[r]
if c == col or abs(c - col) == abs(r - row):
return False
return True
def place(row):
if row == n:
solutions.append(board[:])
return
for col in range(n):
if is_safe(row, col):
board[row] = col
place(row + 1)
board[row] = -1 # backtrack
place(0)
return solutions
sols = solve_n_queens(4)
print(f"Found {len(sols)} solutions for 4-Queens.")
for s in sols:
print(s)
def solve_sudoku(board):
def is_valid(r, c, val):
for i in range(9):
if board[r][i] == val or board[i][c] == val:
return False
br, bc = 3 * (r // 3), 3 * (c // 3)
for i in range(br, br + 3):
for j in range(bc, bc + 3):
if board[i][j] == val:
return False
return True
def backtrack():
for r in range(9):
for c in range(9):
if board[r][c] == 0:
for val in range(1, 10):
if is_valid(r, c, val):
board[r][c] = val
if backtrack():
return True
board[r][c] = 0 # undo
return False
return True
return backtrack()
| Paradigm | Key idea | When it works | Example |
|---|---|---|---|
| Divide & Conquer | Split, solve, combine | Independent subproblems | Merge sort, quick sort |
| Dynamic Programming | Store results of overlapping subproblems | Optimal substructure + overlapping | Knapsack, LCS, Fibonacci |
| Greedy | Locally optimal choice | Greedy-choice property | Activity selection, Huffman |
| Backtracking | Explore, undo on dead-end | Constraint satisfaction | N-Queens, Sudoku, maze |
For DP questions, clearly state: (1) the state (what does dp[i][j] represent?), (2) the recurrence, (3) the base case, (4) the final answer. This structure alone earns most of the marks.
CSV is the simplest tabular format: rows are lines, columns are separated by commas. Python's csv module handles quoting, escaping and dialect differences.
import csv
rows = [
["Name", "Roll", "Marks"],
["Aarav", 101, 88],
["Diya", 102, 95],
["Kabir", 103, 72],
]
with open("students.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerows(rows)
import csv
with open("students.csv", "r") as f:
reader = csv.reader(f)
header = next(reader) # consume header row
for row in reader:
print(row) # ['Aarav', '101', '88']
DictReader and DictWriterimport csv
with open("students.csv", "r") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["Name"], "->", row["Marks"])
# Aarav -> 88
# Diya -> 95
# Kabir -> 72
# Writing with field names
data = [
{"Name": "Meera", "Roll": 104, "Marks": 81},
{"Name": "Zara", "Roll": 105, "Marks": 89},
]
with open("extra.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["Name", "Roll", "Marks"])
writer.writeheader()
writer.writerows(data)
newline=""
On Windows, opening a CSV file for writing without newline="" inserts extra blank lines. This is one of the most common file-handling bugs.
JSON is the standard format for web APIs and configuration. It maps naturally to Python dicts, lists, strings, numbers, booleans and None.
| JSON | Python |
|---|---|
object {...} | dict |
array [...] | list |
| string | str |
| number | int / float |
true / false | True / False |
null | None |
import json
data = {
"course": "INT108",
"students": [
{"name": "Aarav", "marks": [88, 92, 79]},
{"name": "Diya", "marks": [95, 81, 90]},
],
"credits": 4,
"active": True,
}
# --- Write ---
with open("course.json", "w") as f:
json.dump(data, f, indent=2)
# --- Read ---
with open("course.json", "r") as f:
loaded = json.load(f)
print(loaded["course"]) # INT108
print(loaded["students"][0]["name"]) # Aarav
# --- String conversions ---
json_string = json.dumps(data, indent=2)
parsed = json.loads(json_string)
print(parsed == data) # True
import json
DEFAULTS = {"theme": "light", "font_size": 11, "autosave": True}
def load_config(path):
try:
with open(path) as f:
user_cfg = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
user_cfg = {}
return {**DEFAULTS, **user_cfg} # user overrides defaults
cfg = load_config("settings.json")
print(cfg)
XML uses nested tags. Python's xml.etree.ElementTree provides a simple API for reading and writing.
import xml.etree.ElementTree as ET
# --- Build a tree ---
root = ET.Element("library")
book = ET.SubElement(root, "book", id="1")
ET.SubElement(book, "title").text = "Python 101"
ET.SubElement(book, "author").text = "A. Kumar"
ET.SubElement(book, "year").text = "2024"
tree = ET.ElementTree(root)
tree.write("library.xml", encoding="utf-8", xml_declaration=True)
# --- Parse a tree ---
tree = ET.parse("library.xml")
root = tree.getroot()
for book in root.findall("book"):
title = book.find("title").text
author = book.find("author").text
print(f"{book.get('id')}: {title} by {author}")
# 1: Python 101 by A. Kumar
import csv, json
records = []
with open("students.csv", "r") as f:
reader = csv.DictReader(f)
for row in reader:
row["Marks"] = int(row["Marks"]) # type conversion
records.append(row)
with open("students.json", "w") as f:
json.dump(records, f, indent=2)
print(f"Converted {len(records)} records.")
| Aspect | CSV | JSON | XML |
|---|---|---|---|
| Structure | Flat tables only | Nested objects/arrays | Nested tags with attributes |
| Readability | High for tables | High | Verbose |
| Types | All strings | Numbers, bools, null | All text (needs schema) |
| Python module | csv | json | xml.etree.ElementTree |
| Typical use | Spreadsheets, exports | Web APIs, config | Legacy systems, SOAP, RSS |
SQLite is a serverless, file-based relational database built into Python's standard library through the sqlite3 module. It is perfect for small to medium applications, prototypes and embedded systems.
import sqlite3
conn = sqlite3.connect("school.db") # creates the file if missing
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS students (
roll INTEGER PRIMARY KEY,
name TEXT NOT NULL,
marks REAL,
branch TEXT
)
""")
conn.commit()
conn.close()
| Operation | SQL | Python |
|---|---|---|
| Create | INSERT INTO ... | cur.execute(sql, params) |
| Read | SELECT ... FROM ... | cur.fetchall() / fetchone() |
| Update | UPDATE ... SET ... | cur.execute(...) then conn.commit() |
| Delete | DELETE FROM ... | cur.execute(...) then conn.commit() |
import sqlite3
conn = sqlite3.connect("school.db")
cur = conn.cursor()
# --- INSERT (parameterised — never use f-strings for user input) ---
cur.execute("INSERT INTO students VALUES (?, ?, ?, ?)",
(101, "Aarav", 88.5, "CSE"))
cur.execute("INSERT INTO students VALUES (?, ?, ?, ?)",
(102, "Diya", 95.0, "ECE"))
# Insert many at once
cur.executemany("INSERT INTO students VALUES (?, ?, ?, ?)", [
(103, "Kabir", 72.0, "CSE"),
(104, "Meera", 81.5, "MEC"),
])
conn.commit()
# --- SELECT ---
cur.execute("SELECT roll, name, marks FROM students ORDER BY marks DESC")
for row in cur.fetchall():
print(row)
# --- UPDATE ---
cur.execute("UPDATE students SET marks = ? WHERE roll = ?", (90.0, 103))
conn.commit()
# --- DELETE ---
cur.execute("DELETE FROM students WHERE roll = ?", (104,))
conn.commit()
conn.close()
Never build SQL with string formatting:
cur.execute(f"SELECT * FROM users WHERE name = '{name}'") # DANGEROUS
Use parameterised queries with ? placeholders:
cur.execute("SELECT * FROM users WHERE name = ?", (name,)) # SAFE
| Method | Returns |
|---|---|
fetchone() | First row as a tuple, or None |
fetchmany(n) | Next n rows as a list |
fetchall() | All remaining rows as a list of tuples |
row_factory = sqlite3.Row | Rows behave like dicts (access by column name) |
conn = sqlite3.connect("school.db")
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute("SELECT * FROM students")
for row in cur:
print(row["name"], row["marks"])
conn.close()
import sqlite3
conn = sqlite3.connect("bank.db")
try:
with conn: # transaction: commits on success, rolls back on exception
cur = conn.cursor()
cur.execute("UPDATE accounts SET balance = balance - 1000 WHERE id = 1")
cur.execute("UPDATE accounts SET balance = balance + 1000 WHERE id = 2")
except sqlite3.Error as e:
print("Database error:", e)
finally:
conn.close()
import sqlite3
class StudentDB:
def __init__(self, db="school.db"):
self.conn = sqlite3.connect(db)
self.conn.row_factory = sqlite3.Row
self._create_table()
def _create_table(self):
with self.conn:
self.conn.execute("""
CREATE TABLE IF NOT EXISTS students (
roll INTEGER PRIMARY KEY,
name TEXT NOT NULL,
marks REAL
)
""")
def add(self, roll, name, marks):
with self.conn:
self.conn.execute(
"INSERT INTO students VALUES (?, ?, ?)", (roll, name, marks))
def topper(self):
cur = self.conn.execute(
"SELECT * FROM students ORDER BY marks DESC LIMIT 1")
return cur.fetchone()
def average(self):
cur = self.conn.execute("SELECT AVG(marks) FROM students")
return cur.fetchone()[0]
def close(self):
self.conn.close()
db = StudentDB()
db.add(101, "Aarav", 88.5)
db.add(102, "Diya", 95.0)
db.add(103, "Kabir", 72.0)
print("Topper :", dict(db.topper()))
print(f"Average: {db.average():.2f}")
db.close()
| Feature | SQLite | MySQL / PostgreSQL |
|---|---|---|
| Setup | None — single file | Server installation required |
| Concurrency | Limited (file lock) | High |
| Python module | sqlite3 (built-in) | mysql-connector, psycopg2 |
| Use case | Prototypes, embedded apps | Production web apps |
For DB questions, always show: (1) connect, (2) create cursor, (3) execute with parameterised query, (4) commit for writes, (5) close. Mention the parameterised ? as protection against SQL injection.
Tkinter is Python's standard GUI toolkit, bundled with every Python installation. It is ideal for small desktop tools, educational projects and prototypes.
import tkinter as tkroot = tk.Tk()# add widgets hereroot.mainloop()
import tkinter as tk
root = tk.Tk()
root.title("My First GUI")
root.geometry("320x200")
label = tk.Label(root, text="Hello, Tkinter!", font=("Arial", 14))
label.pack(pady=20)
button = tk.Button(root, text="Quit", command=root.destroy)
button.pack()
root.mainloop()
| Widget | Purpose |
|---|---|
Label | Display static text or image |
Button | Clickable button with a callback |
Entry | Single-line text input |
Text | Multi-line text editor |
Frame | Container to group widgets |
Checkbutton | On/off toggle |
Radiobutton | One of a mutually exclusive set |
Listbox | Scrollable list of options |
Combobox | Dropdown list (ttk) |
Canvas | Drawing surface for shapes and graphics |
| Manager | Behaviour | Best for |
|---|---|---|
pack() | Stacks widgets in a direction | Simple vertical/horizontal layouts |
grid() | Places widgets in rows/columns | Form-like layouts |
place() | Absolute pixel coordinates | Precise placement (rarely) |
Using pack() and grid() for widgets in the same parent causes a TclError. Pick one and stick with it throughout the container.
import tkinter as tk
def calculate():
try:
result = eval(entry.get())
output.config(text=f"= {result}")
except Exception as e:
output.config(text="Error")
root = tk.Tk()
root.title("Calculator")
root.geometry("300x150")
tk.Label(root, text="Enter expression:").pack(pady=(10, 0))
entry = tk.Entry(root, font=("Consolas", 12), width=25)
entry.pack(pady=6)
tk.Button(root, text="Calculate", command=calculate).pack()
output = tk.Label(root, text="", font=("Arial", 12, "bold"), fg="teal")
output.pack(pady=8)
root.mainloop()
Caution: eval() is safe here only because it is a classroom demo. Never eval untrusted input in production.
import tkinter as tk
from tkinter import messagebox
def submit():
name = name_var.get().strip()
if not name:
messagebox.showwarning("Input", "Name cannot be empty")
return
messagebox.showinfo("Hello", f"Hello, {name}!")
root = tk.Tk()
root.title("Registration")
name_var = tk.StringVar()
branch_var = tk.StringVar(value="CSE")
tk.Label(root, text="Name:").grid(row=0, column=0, sticky="e", padx=8, pady=6)
tk.Entry(root, textvariable=name_var, width=25).grid(row=0, column=1)
tk.Label(root, text="Branch:").grid(row=1, column=0, sticky="e", padx=8)
for i, branch in enumerate(["CSE", "ECE", "MEC", "CIVIL"]):
tk.Radiobutton(root, text=branch, variable=branch_var,
value=branch).grid(row=1+i, column=1, sticky="w")
tk.Button(root, text="Submit", command=submit).grid(
row=6, column=0, columnspan=2, pady=10)
root.mainloop()
import tkinter as tk
from tkinter import messagebox
class TodoApp:
def __init__(self, root):
self.root = root
self.root.title("To-Do List")
self.tasks = []
self.entry = tk.Entry(root, width=40)
self.entry.pack(pady=8, padx=10)
tk.Button(root, text="Add Task", command=self.add).pack()
self.listbox = tk.Listbox(root, width=50, height=12)
self.listbox.pack(pady=8, padx=10)
tk.Button(root, text="Delete Selected",
command=self.delete).pack(pady=(0, 10))
def add(self):
task = self.entry.get().strip()
if task:
self.tasks.append(task)
self.listbox.insert(tk.END, task)
self.entry.delete(0, tk.END)
else:
messagebox.showwarning("Input", "Please enter a task.")
def delete(self):
selection = self.listbox.curselection()
if not selection:
return
idx = selection[0]
self.listbox.delete(idx)
self.tasks.pop(idx)
root = tk.Tk()
TodoApp(root)
root.mainloop()
For GUI questions, a compact 15–20 line example with at least two widgets and one event handler is usually sufficient. Mention the event loop (mainloop()) and the callback pattern (command=function).
NumPy (Numerical Python) provides the ndarray — a fast, memory-efficient, N-dimensional array. It is the foundation of the Python data-science stack (pandas, scikit-learn, TensorFlow).
| Feature | Python list | NumPy array |
|---|---|---|
| Element types | Mixed | Homogeneous (single dtype) |
| Memory | Pointer per element | Contiguous block |
| Speed | Interpreted loops | Vectorised C operations (10–100× faster) |
| Math operations | Element-by-element loops | Direct operators (a + b, a * 2) |
| Dimensions | Nested lists | N-dimensional ndarray |
import numpy as np
a = np.array([1, 2, 3, 4])
print(a) # [1 2 3 4]
print(a.dtype) # int64
print(a.shape) # (4,)
print(a.ndim) # 1
b = np.array([[1, 2, 3], [4, 5, 6]])
print(b.shape) # (2, 3)
# Convenience constructors
print(np.zeros((2, 3))) # 2x3 of 0.0
print(np.ones((2, 2))) # 2x2 of 1.0
print(np.full((2, 2), 7)) # 2x2 of 7
print(np.eye(3)) # 3x3 identity
print(np.arange(0, 10, 2)) # [0 2 4 6 8]
print(np.linspace(0, 1, 5)) # [0. 0.25 0.5 0.75 1.]
print(np.random.rand(2, 3)) # 2x3 uniform [0,1)
import numpy as np
a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])
print(a + b) # [11 22 33 44]
print(a * 2) # [ 2 4 6 8]
print(a ** 2) # [ 1 4 9 16]
print(np.sqrt(a)) # [1. 1.414 ... ]
# Aggregations
print(a.sum(), a.mean(), a.min(), a.max(), a.std())
# Boolean masking
print(a[a > 2]) # [3 4]
# Reshaping
m = np.arange(12).reshape(3, 4)
print(m.T) # transpose
print(m.flatten()) # 1-D copy
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print(A @ B) # matrix product (Python 3.5+)
print(np.dot(A, B)) # same result
# [[19 22]
# [43 50]]
Matplotlib is the standard plotting library for Python. Its pyplot interface works like MATLAB.
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
plt.figure(figsize=(8, 4))
plt.plot(x, y, label="sin(x)", color="teal", linewidth=2)
plt.plot(x, np.cos(x), label="cos(x)", linestyle="--", color="orange")
plt.title("Trigonometric Functions")
plt.xlabel("x (radians)")
plt.ylabel("value")
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
plt.savefig("trig.png", dpi=120)
plt.show()
import matplotlib.pyplot as plt
import numpy as np
# Histogram of normally distributed data
data = np.random.normal(loc=170, scale=8, size=1000) # heights in cm
plt.figure(figsize=(8, 4))
plt.hist(data, bins=30, color="steelblue", edgecolor="white")
plt.title("Height distribution")
plt.xlabel("Height (cm)")
plt.ylabel("Frequency")
plt.tight_layout()
plt.savefig("hist.png", dpi=120)
# Bar chart of categorical counts
subjects = ["Maths", "Physics", "Python", "Chemistry"]
marks = [92, 85, 98, 78]
plt.figure(figsize=(8, 4))
plt.bar(subjects, marks, color="teal")
plt.title("Marks by Subject")
plt.ylabel("Marks")
plt.tight_layout()
plt.savefig("bar.png", dpi=120)
import numpy as np
marks = np.array([88, 92, 79, 95, 81, 90, 72, 85])
print(f"Mean : {marks.mean():.2f}")
print(f"Median : {np.median(marks):.2f}")
print(f"Std dev : {marks.std():.2f}")
print(f"Max/Min : {marks.max()} / {marks.min()}")
print(f"Above 90: {marks[marks > 90]}")
print(f"Percentile 25, 75: {np.percentile(marks, [25, 75])}")
# Via pip
pip install numpy matplotlib
# Or via conda (recommended in Anaconda environments)
conda install numpy matplotlib
For data-science questions, remember: NumPy array creation (array, zeros, arange, linspace), vectorised ops, and matplotlib's plot/bar/hist/scatter. Show at least one plot with title, axis labels and legend.
assert StatementThe simplest form of testing: assert condition, message. If the condition is false, Python raises AssertionError.
def divide(a, b):
assert b != 0, "Denominator cannot be zero"
return a / b
print(divide(10, 2)) # 5.0
# divide(10, 0) # AssertionError: Denominator cannot be zero
Assertions are removed when Python runs with -O (optimised mode). Use them for internal sanity checks, not for validating user input — use if + raise for that.
unittest — The Standard Frameworkimport unittest
def add(a, b):
return a + b
def is_even(n):
return n % 2 == 0
class TestMathFunctions(unittest.TestCase):
def setUp(self):
"""Runs before every test method."""
self.values = [1, 2, 3, 4, 5]
def test_add_positive(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative(self):
self.assertEqual(add(-1, -1), -2)
def test_add_zero(self):
self.assertEqual(add(0, 7), 7)
def test_is_even(self):
self.assertTrue(is_even(4))
self.assertFalse(is_even(5))
def test_sum_of_list(self):
self.assertEqual(sum(self.values), 15)
if __name__ == "__main__":
unittest.main()
Run from the terminal:
python -m unittest test_math.py -v
unittest Assertions| Assertion | Checks |
|---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) / assertFalse(x) | Truth value |
assertIs(a, b) / assertIsNot | Identity |
assertIn(a, b) / assertNotIn | Membership |
assertRaises(Exc) | Exception is raised |
assertAlmostEqual(a, b) | Float equality within tolerance |
import unittest
def safe_divide(a, b):
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
class TestDivide(unittest.TestCase):
def test_valid(self):
self.assertEqual(safe_divide(10, 2), 5)
def test_zero_division(self):
with self.assertRaises(ZeroDivisionError):
safe_divide(10, 0)
if __name__ == "__main__":
unittest.main()
doctest — Testing DocumentationEmbed tests directly in docstrings; doctest extracts and runs them.
def factorial(n):
"""Return n! for a non-negative integer.
>>> factorial(0)
1
>>> factorial(5)
120
>>> factorial(3)
6
"""
if n <= 1:
return 1
return n * factorial(n - 1)
if __name__ == "__main__":
import doctest
doctest.testmod()
| Technique | When to use |
|---|---|
print() statements | Quick inspection of variables |
logging module | Structured, level-based output for real apps |
pdb (Python debugger) | Step through code interactively |
| IDE breakpoints | Visual debugging (VS Code, PyCharm) |
| Traceback analysis | Read from bottom up to find the actual error |
import pdb
def buggy(x):
pdb.set_trace() # execution pauses here
return x * 2 / (x - 3)
# In the debugger prompt:
# n - next line
# s - step into
# c - continue
# p var - print variable
# q - quit
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
def process(amount):
logging.debug(f"Received amount = {amount}")
if amount < 0:
logging.error("Negative amount!")
return 0
logging.info(f"Processing {amount}")
return amount * 2
process(100)
process(-5)
import cProfile
def slow_function():
total = 0
for i in range(1_000_000):
total += i ** 2
return total
cProfile.run("slow_function()")
Or from the command line:
python -m cProfile -s cumtime my_script.py
Arrange — set up data and objects.
Act — call the function under test.
Assert — verify the outcome.
def test_average():
# Arrange
marks = [80, 90, 100]
# Act
result = sum(marks) / len(marks)
# Assert
assert result == 90.0
For testing questions, show a unittest.TestCase with at least three test methods, one assertRaises and one assertAlmostEqual. Mention that tests are runnable via python -m unittest.
These projects combine concepts from Units I–III. They serve as portfolio pieces and practical-exam templates.
import sqlite3
from datetime import date
class ExpenseTracker:
def __init__(self, db="expenses.db"):
self.conn = sqlite3.connect(db)
self.conn.row_factory = sqlite3.Row
with self.conn:
self.conn.execute("""
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT, category TEXT, amount REAL, note TEXT
)
""")
def add(self, category, amount, note=""):
with self.conn:
self.conn.execute(
"INSERT INTO expenses (date, category, amount, note) VALUES (?, ?, ?, ?)",
(date.today().isoformat(), category, amount, note))
def total_by_category(self):
cur = self.conn.execute(
"SELECT category, SUM(amount) AS total FROM expenses GROUP BY category ORDER BY total DESC")
return [dict(r) for r in cur.fetchall()]
def monthly_total(self, month_prefix):
cur = self.conn.execute(
"SELECT SUM(amount) FROM expenses WHERE date LIKE ?",
(f"{month_prefix}%",))
return cur.fetchone()[0] or 0
def close(self):
self.conn.close()
t = ExpenseTracker()
t.add("Food", 250.0, "Lunch")
t.add("Transport", 80.0, "Auto")
t.add("Food", 320.0, "Dinner")
print("By category:", t.total_by_category())
print("This month :", t.monthly_total(date.today().strftime("%Y-%m")))
t.close()
import csv
import matplotlib.pyplot as plt
names, averages = [], []
with open("students.csv") as f:
for row in csv.DictReader(f):
marks = [int(row[c]) for c in ("Maths", "Physics", "Python")]
names.append(row["Name"])
averages.append(sum(marks) / len(marks))
plt.figure(figsize=(8, 4))
bars = plt.bar(names, averages, color="teal")
plt.title("Average Marks per Student")
plt.ylabel("Average")
plt.ylim(0, 100)
for bar, avg in zip(bars, averages):
plt.text(bar.get_x() + bar.get_width()/2, avg + 1,
f"{avg:.1f}", ha="center", fontsize=9)
plt.tight_layout()
plt.savefig("dashboard.png", dpi=120)
plt.show()
import json, re, hashlib
from functools import wraps
def log_action(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[LOG] {func.__name__} called")
return func(*args, **kwargs)
return wrapper
def check_strength(pwd):
score = 0
score += bool(re.search(r"[a-z]", pwd))
score += bool(re.search(r"[A-Z]", pwd))
score += bool(re.search(r"\d", pwd))
score += bool(re.search(r"[!@#$%^&*]", pwd))
score += len(pwd) >= 12
return ["Weak", "Weak", "Fair", "Good", "Strong", "Very Strong"][score]
class PasswordManager:
def __init__(self, path="vault.json"):
self.path = path
try:
with open(path) as f:
self.vault = json.load(f)
except FileNotFoundError:
self.vault = {}
@log_action
def add(self, site, password):
if site in self.vault:
raise ValueError(f"{site} already exists")
self.vault[site] = hashlib.sha256(password.encode()).hexdigest()
self._save()
return check_strength(password)
def verify(self, site, password):
hashed = hashlib.sha256(password.encode()).hexdigest()
return self.vault.get(site) == hashed
def _save(self):
with open(self.path, "w") as f:
json.dump(self.vault, f, indent=2)
pm = PasswordManager()
print("Strength:", pm.add("gmail", "Str0ng!Pass123"))
print("Verify :", pm.verify("gmail", "Str0ng!Pass123"))
print("Wrong :", pm.verify("gmail", "wrongpass"))
import re
def parse_questions(filename):
"""Generator that yields (question, answer) tuples lazily."""
pattern = re.compile(r"^Q:\s*(.+?)\s*\|\s*A:\s*(.+)$")
with open(filename) as f:
for line in f:
m = pattern.match(line.strip())
if m:
yield m.group(1), m.group(2)
def run_quiz(filename):
score = 0
total = 0
for question, answer in parse_questions(filename):
total += 1
user = input(f"{question} = ").strip()
if user.lower() == answer.lower():
score += 1
print(" Correct!")
else:
print(f" Wrong. Answer: {answer}")
print(f"\nScore: {score}/{total}")
# quiz.txt example:
# Q: 2 + 2 | A: 4
# Q: Capital of France | A: Paris
# run_quiz("quiz.txt")
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
days = np.arange(1, 31)
temps = 28 + 6 * np.sin(2 * np.pi * days / 30) + np.random.normal(0, 1.5, 30)
plt.figure(figsize=(9, 4))
plt.plot(days, temps, marker="o", color="teal", label="Daily temp")
plt.axhline(temps.mean(), color="orange", linestyle="--",
label=f"Mean = {temps.mean():.1f}°C")
plt.fill_between(days, temps.mean() - temps.std(),
temps.mean() + temps.std(),
alpha=0.15, color="teal", label="±1σ")
plt.title("Simulated Monthly Temperature")
plt.xlabel("Day")
plt.ylabel("Temperature (°C)")
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("weather.png", dpi=120)
plt.show()
In practical exams, structure your answer: (1) imports, (2) class or functions with docstrings, (3) main logic using the right data structure, (4) a small driver with sample input and expected output. Comments above each block earn extra marks. Always mention time/space complexity where relevant.
| Concept | Syntax |
|---|---|
| Simple decorator | def deco(f): @wraps(f); def wrapper(*a, **k): return f(*a, **k); return wrapper |
| Decorator with args | def deco(arg): def inner(f): ... return inner |
| Apply decorator | @deco above the function definition |
| Generator function | def g(): yield value |
| Generator expression | (expr for x in iter if cond) |
| Delegate | yield from iterable |
| Memoize | @lru_cache(maxsize=None) |
| Approach | When to use |
|---|---|
Class with __enter__/__exit__ | Complex resources, custom cleanup |
@contextmanager | Simple resources with a single yield |
contextlib.suppress | Ignore specific exceptions |
contextlib.ExitStack | Dynamic number of resources |
collections Cheat Sheet| Class | One-line purpose |
|---|---|
Counter | Counter("aab").most_common(1) → [('a', 2)] |
defaultdict | dd = defaultdict(list); dd[k].append(v) |
OrderedDict | Ordered mapping with move_to_end |
namedtuple | Point = namedtuple("Point", "x y") |
deque | d.appendleft(x), d.popleft(), deque(maxlen=3) |
ChainMap | Layered lookups: ChainMap(user, defaults) |
| Structure | Search | Insert | Delete |
|---|---|---|---|
| Stack (list) | \(O(n)\) | \(O(1)\) push | \(O(1)\) pop |
| Queue (deque) | \(O(n)\) | \(O(1)\) | \(O(1)\) |
| Heap | \(O(n)\) | \(O(\log n)\) | \(O(\log n)\) |
| Linked List | \(O(n)\) | \(O(1)\) head | \(O(n)\) |
| BST (balanced) | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) |
| Format | Module | Best for |
|---|---|---|
| CSV | csv | Flat tables, spreadsheets |
| JSON | json | Web APIs, nested config |
| XML | xml.etree.ElementTree | Legacy systems, RSS |
| SQLite | sqlite3 | Relational queries, persistence |
| Pickle | pickle | Python object snapshots |
| Library | Purpose | Key API |
|---|---|---|
collections | Specialised containers | Counter, deque, defaultdict |
functools | Higher-order helpers | wraps, lru_cache, reduce |
contextlib | Context manager helpers | contextmanager, suppress |
heapq | Heap operations | heappush, heappop |
csv / json | Structured data I/O | DictReader, dump/load |
sqlite3 | Embedded database | connect, execute |
tkinter | GUI toolkit | Tk, Label, Button, mainloop |
numpy | Numeric arrays | array, arange, linspace |
matplotlib | Plotting | plot, bar, hist |
unittest | Testing | TestCase, assertEqual |
@wraps in every decorator — it preserves __name__ and __doc__, and examiners look for it.with over manual close() in try/finally.collections classes well: Counter, defaultdict, deque. They appear in many questions.list.pop(0) for queues. Use collections.deque — \(O(1)\) vs \(O(n)\).? placeholders) to prevent SQL injection.mainloop() — it starts the event loop. Also mention you must never mix pack and grid in the same container.python -m unittest to run them.@uppercase that converts the return value of a function to uppercase. Apply it to a function returning a string.Easyprimes_up_to(n) that yields primes up to n. Print the first 10 primes.MediumDatabaseConnection that opens a SQLite connection, commits on success and rolls back on error.Mediumcollections.Counter to find the three most common words in a paragraph. Ignore case and punctuation.Easypush, pop, peek, is_empty and __len__. Use it to check whether an expression has balanced parentheses.Mediums1 = "ABCBDAB", s2 = "BDCABA".HardEntry for a number and a Button that displays its square in a Label.Mediumunittest test suite for a function is_palindrome(s) that should pass for "madam", "racecar", "noon" and fail for "hello", "python".Easy@retry decorator that retries a function up to 3 times on failure with a 0.5-second delay between attempts. Demonstrate it on a function that fails randomly.Hardfrom functools import wraps
def uppercase(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@uppercase
def greet(name):
return f"hello, {name}"
print(greet("aarav")) # HELLO, AARAV
def primes_up_to(n):
for num in range(2, n + 1):
if all(num % d != 0 for d in range(2, int(num ** 0.5) + 1)):
yield num
# Print the first 10 primes
count = 0
for prime in primes_up_to(100):
print(prime, end=" ")
count += 1
if count == 10:
break
print()
# 2 3 5 7 11 13 17 19 23 29
import sqlite3
class DatabaseConnection:
def __init__(self, db_path):
self.db_path = db_path
self.conn = None
def __enter__(self):
self.conn = sqlite3.connect(self.db_path)
return self.conn
def __exit__(self, exc_type, exc_val, tb):
if exc_type is None:
self.conn.commit()
print("Committed.")
else:
self.conn.rollback()
print(f"Rolled back: {exc_val}")
self.conn.close()
return False
with DatabaseConnection("test.db") as conn:
conn.execute("CREATE TABLE IF NOT EXISTS t (id INTEGER, v TEXT)")
conn.execute("INSERT INTO t VALUES (1, 'hello')")
import re
from collections import Counter
text = """
Python is great. Python is powerful and Python is easy to learn.
Data science with Python is a popular career path.
"""
words = re.findall(r"[a-z]+", text.lower())
top3 = Counter(words).most_common(3)
for word, n in top3:
print(f"{word}: {n}")
class Stack:
def __init__(self):
self.items = []
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):
return self.items[-1] if self.items else None
def is_empty(self):
return len(self.items) == 0
def __len__(self):
return len(self.items)
def is_balanced(expr):
pairs = {")": "(", "]": "[", "}": "{"}
stack = Stack()
for ch in expr:
if ch in "([{":
stack.push(ch)
elif ch in pairs:
if stack.is_empty() or stack.pop() != pairs[ch]:
return False
return stack.is_empty()
print(is_balanced("{[()]}")) # True
print(is_balanced("([)]")) # False
print(is_balanced("((()))")) # True
def lcs(s1, s2):
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n], dp
length, table = lcs("ABCBDAB", "BDCABA")
print("LCS length:", length) # 4
# DP table (rows = s1, columns = s2)
print(" '' B D C A B A")
for i, row in enumerate(table):
label = "''" if i == 0 else f" {s1[i-1]}"
print(f"{label:>4}", row)
Answer: LCS length = 4 (e.g., BCBA or BDAB).
import csv, json
# CSV -> JSON
records = []
with open("students.csv") as f:
for row in csv.DictReader(f):
row["Marks"] = int(row["Marks"])
records.append(row)
with open("students.json", "w") as f:
json.dump(records, f, indent=2)
# JSON -> CSV
with open("students.json") as f:
data = json.load(f)
with open("students_out.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["Name", "Roll", "Marks"])
writer.writeheader()
writer.writerows(data)
print(f"Roundtripped {len(data)} records.")
import tkinter as tk
def compute_square():
try:
n = float(entry.get())
result_label.config(text=f"Square = {n ** 2}")
except ValueError:
result_label.config(text="Please enter a valid number.")
root = tk.Tk()
root.title("Square Calculator")
root.geometry("300x160")
tk.Label(root, text="Enter a number:").pack(pady=(14, 2))
entry = tk.Entry(root, width=20)
entry.pack()
tk.Button(root, text="Compute Square", command=compute_square).pack(pady=10)
result_label = tk.Label(root, text="", font=("Arial", 12, "bold"), fg="teal")
result_label.pack()
root.mainloop()
import numpy as np
np.random.seed(0)
data = np.random.normal(loc=50, scale=10, size=1000)
print(f"Mean : {data.mean():.2f}")
print(f"Median : {np.median(data):.2f}")
print(f"Above 60: {(data > 60).sum()} samples")
print(f"Std dev : {data.std():.2f}")
Sample output: Mean ≈ 49.96, Median ≈ 49.97, Above 60 ≈ 158 samples, Std ≈ 9.95.
import unittest
def is_palindrome(s):
s = s.lower().replace(" ", "")
return s == s[::-1]
class TestPalindrome(unittest.TestCase):
def test_palindromes(self):
for word in ["madam", "racecar", "noon", "A man a plan a canal Panama"]:
with self.subTest(word=word):
self.assertTrue(is_palindrome(word))
def test_non_palindromes(self):
for word in ["hello", "python", "world"]:
with self.subTest(word=word):
self.assertFalse(is_palindrome(word))
def test_empty_string(self):
self.assertTrue(is_palindrome(""))
if __name__ == "__main__":
unittest.main()
# Class-based
class TimerClass:
def __enter__(self):
import time
self.start = time.perf_counter()
return self
def __exit__(self, *exc):
import time
print(f"Class : {time.perf_counter() - self.start:.4f}s")
# Generator-based
from contextlib import contextmanager
import time
@contextmanager
def timer_gen():
start = time.perf_counter()
try:
yield
finally:
print(f"Gen : {time.perf_counter() - start:.4f}s")
with TimerClass():
sum(range(100_000))
with timer_gen():
sum(range(100_000))
Key difference: the class-based version is more verbose but gives full control (e.g., you can return a value from __enter__, store state, and decide whether to suppress exceptions). The generator-based version is concise and ideal for simple setup/teardown.
import time, random
from functools import wraps
def retry(max_attempts=3, delay=0.5):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt}/{max_attempts} failed: {e}")
if attempt == max_attempts:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def flaky():
if random.random() < 0.7:
raise ConnectionError("Simulated network failure")
return "Success!"
random.seed(7)
try:
print(flaky())
except ConnectionError:
print("All attempts failed.")
| Code | Title | Author | Publisher |
|---|---|---|---|
| T-1 | Fundamentals of Python — First Programs | Kenneth A. Lambert | Cengage Learning |
| R-1 | Python Programming: Using Problem Solving Approach | Reema Thareja | Oxford University Press |
| R-2 | Fluent Python | Luciano Ramalho | O'Reilly |
| R-3 | Python Cookbook | David Beazley & Brian K. Jones | O'Reilly |
| Code | Resource | Purpose |
|---|---|---|
| RW-1 | docs.python.org/3/library/ | Official standard library reference |
| RW-2 | realpython.com | Deep-dive tutorials on decorators, generators |
| RW-3 | numpy.org/doc/ | NumPy documentation |
| RW-4 | matplotlib.org/stable/ | Matplotlib documentation |
| RW-5 | docs.python.org/3/library/tkinter.html | Tkinter reference |
| AV-1 | nptel.ac.in/courses/106106145 | Video lectures |
@functools.wraps to preserve metadata.yield; they are memory-efficient and can represent infinite sequences.__enter__/__exit__ or @contextmanager; always prefer with over manual cleanup.collections module provides Counter, defaultdict, deque, namedtuple, OrderedDict and ChainMap — each solving a specific pattern elegantly.mainloop() starts the event loop and callbacks respond to user actions.unittest and doctest catches regressions; use the AAA pattern and run with python -m unittest.| Course Outcome | Covered in Sections | Key Deliverables |
|---|---|---|
| CO3 — Functions and recursion | I, II, VI | Closures, decorators, generators, recursion, DP |
| CO4 — Core data structures | IV, V, VI | Collections module, stacks, queues, heaps, linked lists, BST |
| CO5 — Object-oriented programming | I, III, XII | Properties, dataclasses, context managers, mini projects |
| CO6 — File handling and regex | VII, VIII, IX, X, XI | CSV/JSON/XML, SQLite, Tkinter, NumPy, testing |
| Week | Focus | Sections |
|---|---|---|
| 1 | Decorators, generators | I, II |
| 2 | Context managers, collections | III, IV |
| 3 | Data structures & algorithms | V, VI |
| 4 | Structured data, databases | VII, VIII |
| 5 | GUI, NumPy, testing | IX, X, XI |
| 6–7 | Integrated mini projects | XII |
Course weightage: ATT 5 + CA 50 + ETP 45. Programming Practice requires solving at least 50% of the assigned coding problems and 50% of the MCQs to be eligible for marks. The coding contests test problem-solving speed — practice one DSA-style problem and one Python-specific problem daily.
Advanced Python Techniques & Applications
INT108 · L:T:P 3:0:2 · 4 Credits
“Simplicity is the ultimate sophistication.” — Leonardo da Vinci