Tuples · Dictionaries · Sets · Functions · Recursion
OOP · File Handling · Regex · Modules · Exceptions
Unit II builds on the foundations from Unit I. Focus on the comparison tables (tuple vs list, dict vs set) and the OOP hierarchy diagrams. Every code snippet is exam-ready — practice typing them out. The practice questions at the end are graded Easy Medium Hard with complete solutions.
A tuple is an ordered, immutable sequence enclosed in parentheses. Once created, its elements cannot be added, removed or replaced. Tuples are faster, use less memory, and provide data integrity compared to lists.
point = (3, 5)
rgb = (255, 128, 0)
mixed = (1, "two", 3.0, True)
nested = ((1, 2), (3, 4))
empty = ()
single = (42,) # NOTE the trailing comma
not_a_tuple = (42) # this is just the integer 42
from_list = tuple([1, 2, 3])
from_str = tuple("abc") # ('a', 'b', 'c')
(42) is an integer, not a tuple. You must write (42,) with a trailing comma. Verify with type((42,)) → <class 'tuple'>.
t = (10, 20, 30, 40, 50)
print(t[0]) # 10
print(t[-1]) # 50
print(t[1:4]) # (20, 30, 40)
print(t[::-1]) # (50, 40, 30, 20, 10)
print(len(t)) # 5
print(20 in t) # True
# t[0] = 99 # TypeError: 'tuple' object does not support item assignment
t = a, b, c (packing) • a, b, c = t (unpacking)
# Packing
person = "Aarav", 19, "CSE"
# Unpacking
name, age, branch = person
print(name, age, branch) # Aarav 19 CSE
# Swapping via tuple packing
a, b = 1, 2
a, b = b, a
print(a, b) # 2 1
# Starred unpacking
first, *middle, last = (1, 2, 3, 4, 5)
print(first, middle, last) # 1 [2, 3, 4] 5
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([4, 9, 1, 7])
print("Min:", low, "Max:", high) # Min: 1 Max: 9
| Operation | Example | Result |
|---|---|---|
count(x) | (1,2,2,3).count(2) | 2 |
index(x) | ('a','b','c').index('b') | 1 |
| Concatenation | (1,2) + (3,4) | (1,2,3,4) |
| Repetition | (0,) * 3 | (0,0,0) |
| Membership | 3 in (1,2,3) | True |
len(), max(), min(), sum() | sum((1,2,3)) | 6 |
| Conversion | list((1,2)) | [1, 2] |
| Sorting (new list) | sorted((3,1,2)) | [1, 2, 3] |
| Feature | Tuple | List |
|---|---|---|
| Syntax | (1, 2, 3) | [1, 2, 3] |
| Mutability | Immutable | Mutable |
| Methods available | Only count(), index() | Many (append, sort, …) |
| Performance | Faster, less memory | Slower, more memory |
| Usable as dict key | Yes (if hashable elements) | No |
| Typical use | Fixed records, coordinates, function returns | Collections that change during execution |
locations = {
(28.61, 77.21): "Delhi",
(19.07, 72.87): "Mumbai",
(12.97, 77.59): "Bengaluru"
}
print(locations[(28.61, 77.21)]) # Delhi
votes = ("A", "B", "A", "C", "A", "B")
for candidate in set(votes):
print(candidate, "->", votes.count(candidate), "votes")
from collections import namedtuple
Student = namedtuple("Student", ["name", "roll", "cgpa"])
s1 = Student("Aarav", 101, 8.7)
print(s1.name, s1.roll, s1.cgpa) # Aarav 101 8.7
print(s1[0]) # Aarav (indexable)
print(s1._asdict()) # OrderedDict form
Named tuples combine the immutability of tuples with the readability of attribute access — ideal for records.
If asked “why use tuples over lists?”, answer: immutability guarantees data integrity, allows use as dictionary keys, provides faster access and lower memory overhead, and signals intent that the data is a fixed record.
A dictionary is an unordered (insertion-ordered since Python 3.7), mutable collection of key–value pairs. Keys must be unique and hashable (immutable types); values may be of any type.
student = {"name": "Aarav", "age": 19, "cgpa": 8.7}
empty = {}
from_pairs = dict([("a", 1), ("b", 2)])
squares = {x: x ** 2 for x in range(1, 5)} # {1:1, 2:4, 3:9, 4:16}
from_keys = dict.fromkeys(["a","b","c"], 0) # {'a':0,'b':0,'c':0}
| Operation | Syntax | Behaviour |
|---|---|---|
| Access by key | d["name"] | Raises KeyError if the key is missing |
| Safe access | d.get("name") | Returns None (or a default) if missing |
| Default access | d.get("x", 0) | Returns 0 if "x" is absent |
| Insert / update | d["city"] = "Delhi" | Adds or overwrites |
| Delete | del d["age"] | Removes the pair |
| Pop | d.pop("age") | Removes and returns the value |
| Clear | d.clear() | Empties the dictionary |
| Membership | "name" in d | Tests keys, not values |
| Length | len(d) | Number of key–value pairs |
student = {"name": "Aarav", "age": 19}
student["cgpa"] = 8.7 # add a new key
student["age"] = 20 # update an existing key
print(student.get("name")) # Aarav
print(student.get("city", "N/A")) # N/A (no KeyError)
print(len(student)) # 3
del student["age"]
print(student) # {'name': 'Aarav', 'cgpa': 8.7}
| Method | Returns | Example |
|---|---|---|
keys() | View of all keys | dict_keys(['name','cgpa']) |
values() | View of all values | dict_values(['Aarav', 8.7]) |
items() | View of (key, value) tuples | dict_items([('name','Aarav'), ...]) |
update(other) | Merge another dict | d.update({"x": 1}) |
setdefault(k, v) | Get or insert a default | d.setdefault("z", 0) |
popitem() | Remove last inserted pair | d.popitem() |
marks = {"Maths": 92, "Physics": 85, "Python": 98}
for subject in marks.keys():
print(subject)
for score in marks.values():
print(score)
for subject, score in marks.items(): # most common pattern
print(f"{subject}: {score}")
students = {
"S101": {"name": "Aarav", "marks": [88, 92, 79]},
"S102": {"name": "Diya", "marks": [95, 81, 90]}
}
for roll, info in students.items():
avg = sum(info["marks"]) / len(info["marks"])
print(roll, info["name"], f"Average = {avg:.2f}")
squares = {x: x ** 2 for x in range(1, 6)}
print(squares) # {1:1, 2:4, 3:9, 4:16, 5:25}
# Invert a dictionary
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
print(inverted) # {1:'a', 2:'b', 3:'c'}
# Filter by value
high = {k: v for k, v in original.items() if v > 1}
print(high) # {'b': 2, 'c': 3}
text = "programming"
freq = {}
for ch in text:
freq[ch] = freq.get(ch, 0) + 1
print(freq)
# {'p': 1, 'r': 2, 'o': 1, 'g': 2, 'a': 1, 'm': 2, 'i': 1, 'n': 1}
The idiom freq.get(ch, 0) + 1 avoids a KeyError for first-time characters.
sentence = "the quick brown fox jumps over the lazy dog the fox"
words = sentence.split()
counts = {}
for w in words:
counts[w] = counts.get(w, 0) + 1
for word, n in sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])):
print(f"{word:<8} {n}")
Top output: the 3, fox 2, then the remaining words once each.
marks = {"Aarav": 88, "Diya": 95, "Kabir": 72, "Meera": 60}
topper = max(marks, key=marks.get)
average = sum(marks.values()) / len(marks)
print("Topper :", topper, marks[topper])
print(f"Average: {average:.2f}")
print("Passed :", [n for n, m in marks.items() if m >= 40])
d1 = {"a": 1, "b": 2}
d2 = {"b": 20, "c": 30}
merged = {**d1, **d2} # d2 overwrites duplicate keys
print(merged) # {'a': 1, 'b': 20, 'c': 30}
merged2 = d1 | d2 # Python 3.9+ pipe operator
print(merged2) # {'a': 1, 'b': 20, 'c': 30}
Questions that require counting occurrences, mapping names to values, or looking up a value by an identifier are almost always dictionary questions. Prefer get(key, default) over direct indexing to avoid KeyError.
A set is an unordered, mutable collection of unique, hashable elements. Duplicates are automatically discarded. Sets are implemented with hash tables, giving average \(O(1)\) membership testing.
s1 = {1, 2, 3, 4}
s2 = set([3, 4, 5, 6])
empty = set() # NOTE: {} creates an empty DICT, not a set
chars = set("banana") # {'b', 'a', 'n'}
print({1, 2, 2, 3, 3, 3}) # {1, 2, 3} - duplicates removed
{} creates an empty dictionary. To create an empty set you must write set().
| Operation | Operator | Method | Example (A={1,2,3}, B={3,4,5}) | Result |
|---|---|---|---|---|
| Union | | | A.union(B) | A | B | {1,2,3,4,5} |
| Intersection | & | A.intersection(B) | A & B | {3} |
| Difference | - | A.difference(B) | A - B | {1,2} |
| Symmetric difference | ^ | A.symmetric_difference(B) | A ^ B | {1,2,4,5} |
| Subset | <= | A.issubset(B) | {1,2} <= A | True |
| Superset | >= | A.issuperset(B) | A >= {1,2} | True |
| Disjoint | — | A.isdisjoint(B) | {1} vs {2} | True |
| Method | Purpose | Example |
|---|---|---|
add(x) | Add a single element | s.add(9) |
update(iter) | Add multiple elements | s.update([7, 8]) |
remove(x) | Remove x; raises KeyError if absent | s.remove(3) |
discard(x) | Remove x; silent if absent | s.discard(99) |
pop() | Remove and return an arbitrary element | s.pop() |
clear() | Remove all elements | s.clear() |
copy() | Shallow copy | t = s.copy() |
An immutable version of a set. Because it is hashable, it can be used as a dictionary key or as an element of another set.
fs = frozenset([1, 2, 3])
# fs.add(4) # AttributeError: 'frozenset' object has no attribute 'add'
permissions = {frozenset({"read", "write"}): "editor"}
squares = {x ** 2 for x in range(1, 6)}
print(squares) # {1, 4, 9, 16, 25}
evens = {x for x in range(20) if x % 2 == 0}
print(evens) # {0, 2, 4, 6, 8, 10, 12, 14, 16, 18}
data = [1, 2, 2, 3, 4, 4, 5, 1]
unique = list(set(data))
print(unique) # order not guaranteed, e.g. [1, 2, 3, 4, 5]
aarav = {"Maths", "Physics", "Python", "Chemistry"}
diya = {"Python", "Chemistry", "Biology", "English"}
print("Common :", aarav & diya)
print("All :", aarav | diya)
print("Only Aarav:", aarav - diya)
print("Not common :", aarav ^ diya)
Common : {'Python', 'Chemistry'}
All : {'Maths', 'Physics', 'Python', 'Chemistry', 'Biology', 'English'}
Only Aarav: {'Maths', 'Physics'}
Not common : {'Maths', 'Physics', 'Biology', 'English'}
all_rolls = set(range(1, 11))
present = {1, 2, 4, 5, 7, 10}
absent = all_rolls - present
print("Absent:", sorted(absent)) # [3, 6, 8, 9]
vowels = set("aeiou")
word = "programming"
found = {ch for ch in word if ch in vowels}
print(found) # {'o', 'a', 'i'}
| Structure | Syntax | Ordered | Mutable | Duplicates | Indexed |
|---|---|---|---|---|---|
| List | [1,2] | Yes | Yes | Yes | Yes |
| Tuple | (1,2) | Yes | No | Yes | Yes |
| Set | {1,2} | No | Yes | No | No |
| Dictionary | {"a":1} | Yes (3.7+) | Yes | Keys unique | By key |
Use sets when you need uniqueness or fast membership testing. Use dictionaries when you need key-based lookup. Use tuples for fixed records. Use lists for general ordered collections.
A function is a named block of reusable code that performs a specific task. Functions support code reuse, modularity, readability and easier debugging.
def function_name(parameters): """docstring""" body return value
def greet(name):
"""Return a greeting message for the given name."""
return f"Hello, {name}!"
message = greet("Aarav")
print(message) # Hello, Aarav!
help(greet) # displays the docstring
Parameter: the variable listed in the function definition (a placeholder).
Argument: the actual value supplied when the function is called.
def add(a, b): # a, b are PARAMETERS
return a + b
print(add(3, 5)) # 3, 5 are ARGUMENTS
| Type | Description | Example |
|---|---|---|
| Positional | Matched by order | def f(a,b): ... → f(1,2) |
| Keyword | Matched by name | f(b=2, a=1) |
| Default | Used when the argument is omitted | def f(a, b=10): |
Variable-length (*args) | Extra positional args as a tuple | def f(*nums): |
Variable-length (**kwargs) | Extra keyword args as a dict | def f(**opts): |
In a definition, non-default parameters must come before default parameters. def f(a=1, b) is a SyntaxError.
def power(base, exponent=2):
return base ** exponent
print(power(5)) # 25 (default exponent)
print(power(2, 10)) # 1024
print(power(exponent=3, base=4)) # 64 (keyword arguments)
def total(*numbers):
return sum(numbers)
print(total(1, 2, 3, 4)) # 10
def profile(**info):
for k, v in info.items():
print(f"{k}: {v}")
profile(name="Aarav", age=19, branch="CSE")
return immediately exits the function and passes a value back.return (or with bare return) returns None.def divide(a, b):
if b == 0:
return None # explicit "no result"
return a / b
print(divide(10, 2)) # 5.0
print(divide(10, 0)) # None
def statistics(nums):
return min(nums), max(nums), sum(nums) / len(nums)
lo, hi, avg = statistics([4, 8, 15, 16, 23, 42])
print(lo, hi, avg)
Python resolves names in this order: Local → Enclosing → Global → Built-in.
| Scope | Where defined | Accessible |
|---|---|---|
| Local | Inside the current function | Only within that function |
| Enclosing | In an outer function (closures) | Inner functions |
| Global | At module level | Everywhere in the module |
| Built-in | Python's built-in namespace | Everywhere (len, print, …) |
counter = 0 # global
def increment():
global counter # declare intent to modify the global
counter += 1
increment(); increment()
print(counter) # 2
def outer():
x = 10
def inner():
nonlocal x # refers to outer's x, not a new local
x += 5
inner()
return x
print(outer()) # 15
Assigning to a name inside a function makes it local for the whole function. Reading it before assignment raises UnboundLocalError. Use global or nonlocal when you intend to modify an outer variable.
A lambda is a small anonymous function written as a single expression.
lambda parameters : expression
square = lambda x: x * x
print(square(6)) # 36
add = lambda a, b: a + b
print(add(3, 4)) # 7
# Commonly used with sorted(), map(), filter()
nums = [5, 2, 9, 1]
print(sorted(nums, key=lambda n: -n)) # [9, 5, 2, 1]
print(list(map(lambda x: x ** 2, nums))) # [25, 4, 81, 1]
print(list(filter(lambda x: x > 3, nums)))# [5, 9]
map, filter, reduce| Function | Purpose | Example |
|---|---|---|
map(f, it) | Apply f to every element | list(map(str, [1,2])) → ['1','2'] |
filter(f, it) | Keep elements where f is True | list(filter(None, [0,1,2])) → [1,2] |
reduce(f, it) | Fold iterable to a single value | reduce(lambda a,b: a+b, [1,2,3]) → 6 |
from functools import reduce
nums = [1, 2, 3, 4, 5]
print(reduce(lambda a, b: a * b, nums)) # 120 (factorial of 5)
def area(length: float, width: float) -> float:
"""Compute the area of a rectangle.
Args:
length: the length of the rectangle
width: the width of the rectangle
Returns:
The product of length and width.
"""
return length * width
print(area.__doc__)
print(area.__annotations__) # {'length': float, 'width': float, 'return': float}
def add(a, b): return a + b
def subtract(a, b): return a - b
def multiply(a, b): return a * b
def divide(a, b):
if b == 0:
return "Error: division by zero"
return a / b
operations = {"+": add, "-": subtract, "*": multiply, "/": divide}
a, op, b = 12, "*", 5
result = operations[op](a, b)
print(f"{a} {op} {b} = {result}") # 12 * 5 = 60
def student_report(name, *marks, **details):
print(f"Student: {name}")
print(f"Marks : {marks}")
print(f"Average: {sum(marks) / len(marks):.2f}")
for k, v in details.items():
print(f"{k:<8}: {v}")
student_report("Aarav", 88, 92, 79, branch="CSE", year=2)
Student: Aarav
Marks : (88, 92, 79)
Average: 86.33
branch : CSE
year : 2
Recursion is a technique in which a function calls itself to solve a smaller instance of the same problem. Every recursive function must have:
def f(n): if base_condition(n): return base_value return combine(n, f(smaller(n)))
def factorial(n):
if n <= 1: # base case
return 1
return n * factorial(n - 1) # recursive case
print(factorial(5)) # 120
print(factorial(0)) # 1
Trace for n = 4: \(4 \times factorial(3) \to 4 \times 3 \times factorial(2) \to 4 \times 3 \times 2 \times factorial(1) \to 4 \times 3 \times 2 \times 1 = 24\).
def fib(n):
if n <= 1: # base cases: fib(0)=0, fib(1)=1
return n
return fib(n - 1) + fib(n - 2)
for i in range(8):
print(fib(i), end=" ") # 0 1 1 2 3 5 8 13
Note: naive recursion is exponential, \(O(2^n)\). Use iteration or memoization for large n.
def digit_sum(n):
if n == 0:
return 0
return n % 10 + digit_sum(n // 10)
print(digit_sum(12345)) # 15
def binary_search(arr, low, high, target):
if low > high:
return -1
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search(arr, mid + 1, high, target)
else:
return binary_search(arr, low, mid - 1, target)
data = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
print(binary_search(data, 0, len(data) - 1, 23)) # 5
def hanoi(n, source, aux, dest, moves=None):
if moves is None:
moves = []
if n == 1:
moves.append(f"{source} -> {dest}")
return moves
hanoi(n - 1, source, dest, aux, moves)
moves.append(f"{source} -> {dest}")
hanoi(n - 1, aux, source, dest, moves)
return moves
for move in hanoi(3, "A", "B", "C"):
print(move)
Minimum moves: \(2^n - 1\). For \(n=3\), 7 moves.
| Aspect | Recursion | Iteration |
|---|---|---|
| Definition | Function calls itself | Loop repeats a block |
| Termination | Base case | Loop condition |
| Memory | Uses call stack — more memory | Constant memory |
| Speed | Slower (function-call overhead) | Faster |
| Readability | Elegant for tree/divide-and-conquer problems | Straightforward for linear problems |
| Risk | RecursionError for deep recursion | Infinite loop if condition never fails |
Python's default recursion depth limit is 1000. Exceeding it raises RecursionError: maximum recursion depth exceeded. Check with sys.getrecursionlimit() and change with sys.setrecursionlimit(n) — but prefer iteration for deep problems.
functools.lru_cacheMemoization caches the results of expensive function calls to avoid recomputation.
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
print(fib(50)) # instant, unlike naive recursion
print(fib.cache_info()) # CacheInfo(hits=48, misses=51, ...)
Always identify and write the base case first in recursion questions. A recursion without a base case causes RecursionError, and marks are usually lost if the base case is missing or wrong.
Object-Oriented Programming (OOP) organises a program around objects — self-contained entities that combine data (attributes) and behaviour (methods). Python supports all four pillars: encapsulation, abstraction, inheritance and polymorphism.
Class: a blueprint or template that defines attributes and methods.
Object (instance): a concrete entity created from a class, with its own copy of instance attributes.
self: the first parameter of every instance method; it refers to the object on which the method was called.
class ClassName: def __init__(self, ...): self.attribute = value def method(self, ...):
class Student:
"""Represents a student with a name and marks."""
def __init__(self, name, marks): # constructor
self.name = name # instance attribute
self.marks = marks
def average(self): # instance method
return sum(self.marks) / len(self.marks)
def display(self):
print(f"{self.name}: average = {self.average():.2f}")
# Creating objects
s1 = Student("Aarav", [88, 92, 79])
s2 = Student("Diya", [95, 81, 90])
s1.display() # Aarav: average = 86.33
s2.display() # Diya: average = 88.67
print(s1.name) # accessing an attribute -> Aarav
__init__ Constructor__init__ is a special (dunder) method that runs automatically when an object is created. It initialises instance attributes. It does not return a value.
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def distance_from_origin(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __str__(self): # string representation
return f"Point({self.x}, {self.y})"
p = Point(3, 4)
print(p) # Point(3, 4)
print(p.distance_from_origin()) # 5.0
| Aspect | Instance variable | Class variable |
|---|---|---|
| Defined | Inside __init__ with self.x | Inside the class body, outside any method |
| Scope | Unique to each object | Shared by all objects |
| Access | obj.x | ClassName.x or obj.x |
| Use | Per-object state (name, roll no) | Common constants or counters |
class Employee:
company = "LPU Tech" # class variable (shared)
count = 0 # class-level counter
def __init__(self, name):
self.name = name # instance variable (per object)
Employee.count += 1
e1 = Employee("Aarav")
e2 = Employee("Diya")
print(e1.company, e2.company) # LPU Tech LPU Tech
print("Total employees:", Employee.count) # 2
Employee.company = "LPU Innovations"
print(e1.company) # LPU Innovations (both changed)
| Type | Decorator | First parameter | Purpose |
|---|---|---|---|
| Instance method | none | self | Operates on a specific object |
| Class method | @classmethod | cls | Operates on the class itself (alternate constructors) |
| Static method | @staticmethod | none | Utility function logically belonging to the class |
class Circle:
pi = 3.14159
def __init__(self, radius):
self.radius = radius
def area(self): # instance method
return Circle.pi * self.radius ** 2
@classmethod
def from_diameter(cls, diameter): # alternate constructor
return cls(diameter / 2)
@staticmethod
def is_valid_radius(r): # utility
return r > 0
c = Circle.from_diameter(10)
print(c.radius) # 5.0
print(Circle.is_valid_radius(3)) # True
Special methods with double underscores let user-defined objects integrate with Python's built-in syntax.
| Dunder | Triggered by | Purpose |
|---|---|---|
__init__ | Class() | Constructor |
__str__ | print(obj), str(obj) | Human-readable string |
__repr__ | repr(obj) | Unambiguous string (debugging) |
__len__ | len(obj) | Number of items |
__add__ | a + b | Addition |
__eq__ | a == b | Equality |
__lt__ | a < b | Less than |
__getitem__ | obj[key] | Indexing |
__iter__ | for x in obj | Iteration |
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 * 3) # Vector(3, 6)
print(abs(v2)) # 5.0
print(v1 == Vector(1,2))# True
Encapsulation bundles data and the methods that operate on it, and restricts direct access from outside. Python uses naming conventions rather than strict keywords.
| Convention | Meaning | Access |
|---|---|---|
name | Public | Freely accessible |
_name | Protected (by convention) | Accessible, but “internal use only” |
__name | Private | Name-mangled to _ClassName__name |
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance # private attribute
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return True
return False
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
return True
return False
def get_balance(self): # controlled accessor
return self.__balance
acc = BankAccount("Aarav", 5000)
acc.deposit(2500)
acc.withdraw(1000)
print(acc.get_balance()) # 6500
# print(acc.__balance) # AttributeError
print(acc._BankAccount__balance) # 6500 - name mangling still allows access
Key idea: data hiding prevents accidental corruption; access is only through validated methods.
Inheritance lets a class (child/derived) acquire the attributes and methods of another class (parent/base), promoting code reuse and an “is-a” relationship.
class Child(Parent): … • super().__init__(...) calls the parent constructor
| Type | Structure | Example |
|---|---|---|
| Single | A → B | class Dog(Animal) |
| Multilevel | A → B → C | Vehicle → Car → ElectricCar |
| Hierarchical | One parent, many children | Animal → Dog, Cat, Cow |
| Multiple | Two or more parents | class C(A, B) |
| Hybrid | Combination of the above | Diamond-shaped hierarchies |
super()class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "Some generic sound"
def info(self):
print(f"I am {self.name}")
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # call parent constructor
self.breed = breed
def speak(self): # override
return "Woof!"
d = Dog("Bruno", "Labrador")
d.info() # I am Bruno
print(d.speak()) # Woof!
print(isinstance(d, Animal)) # True
class Vehicle:
def __init__(self, brand):
self.brand = brand
def describe(self):
print(f"Brand: {self.brand}")
class Car(Vehicle):
def __init__(self, brand, seats):
super().__init__(brand)
self.seats = seats
def describe(self):
super().describe()
print(f"Seats: {self.seats}")
class ElectricCar(Car):
def __init__(self, brand, seats, range_km):
super().__init__(brand, seats)
self.range_km = range_km
def describe(self):
super().describe()
print(f"Range: {self.range_km} km")
e = ElectricCar("Tesla", 5, 500)
e.describe()
Brand: Tesla
Seats: 5
Range: 500 km
class Flyer:
def move(self):
return "flies"
class Swimmer:
def move(self):
return "swims"
class Duck(Flyer, Swimmer):
pass
d = Duck()
print(d.move()) # flies (Flyer comes first in MRO)
print(Duck.__mro__) # (Duck, Flyer, Swimmer, object)
The MRO (Method Resolution Order) decides which parent's method is used when several define the same name. It is computed by the C3 linearisation algorithm.
Polymorphism means “many forms” — the same interface behaves differently for different types.
class Shape:
def area(self):
raise NotImplementedError
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r ** 2
class Rectangle(Shape):
def __init__(self, l, b): self.l, self.b = l, b
def area(self): return self.l * self.b
for shape in (Circle(5), Rectangle(4, 6)):
print(f"{shape.__class__.__name__}: {shape.area():.2f}")
Circle: 78.54
Rectangle: 24.00
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v = Vector(1, 2) + Vector(3, 4)
print(v) # Vector(4, 6)
Python doesn't require inheritance for polymorphism — if an object has the right method, it works. “If it walks like a duck and quacks like a duck, it's a duck.”
class File:
def save(self): print("Saved to file")
class Database:
def save(self): print("Saved to database")
class Cloud:
def save(self): print("Uploaded to cloud")
for storage in (File(), Database(), Cloud()):
storage.save()
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self, amount):
pass
class CreditCard(Payment):
def pay(self, amount):
print(f"Paid Rs.{amount} by credit card")
class UPI(Payment):
def pay(self, amount):
print(f"Paid Rs.{amount} via UPI")
for method in (CreditCard(), UPI()):
method.pay(1500)
An abstract class cannot be instantiated; it only declares the interface that subclasses must implement.
| Pillar | Meaning | Python mechanism |
|---|---|---|
| Encapsulation | Bundle data + methods; hide internals | __private, _protected, getters/setters |
| Abstraction | Expose only what is necessary | abc.ABC, @abstractmethod |
| Inheritance | Reuse via “is-a” relationship | class Child(Parent), super() |
| Polymorphism | Same call, different behaviour | Method overriding, duck typing, dunder methods |
For a 5-mark OOP question, define all four pillars, give one Python code example for each, and mention the keyword/decorator used. Keep the answer structured with sub-headings.
Files provide persistent storage: data survives after the program terminates. Python's built-in open() function returns a file object used for reading and writing.
file_object = open("filename", "mode") … file_object.close()
| Mode | Meaning | File exists | File missing | Position |
|---|---|---|---|---|
'r' | Read only (default) | Reads from start | FileNotFoundError | Beginning |
'w' | Write only | Truncates to empty | Creates new file | Beginning |
'a' | Append | Adds at end | Creates new file | End |
'r+' | Read and write | Yes | FileNotFoundError | Beginning |
'w+' | Write and read | Truncates | Creates new file | Beginning |
'a+' | Append and read | Yes | Creates new file | End |
'rb' | Read binary | Yes | FileNotFoundError | Beginning |
'wb' | Write binary | Truncates | Creates new file | Beginning |
'ab' | Append binary | Adds at end | Creates new file | End |
Opening an existing file in 'w' mode erases all its contents immediately, even before you write anything. Use 'a' when you want to preserve existing data.
| Method | Returns |
|---|---|
read() | Entire file contents as one string |
read(n) | Next n characters |
readline() | One line (including the trailing \n) |
readlines() | List of all lines |
# Method 1: read everything
f = open("data.txt", "r")
content = f.read()
print(content)
f.close()
# Method 2: line by line (memory-efficient) - recommended
with open("data.txt", "r") as f:
for line in f:
print(line.rstrip()) # rstrip() removes the newline
# Method 3: readlines()
with open("data.txt", "r") as f:
lines = f.readlines()
print(len(lines), "lines")
# Write (overwrites)
with open("output.txt", "w") as f:
f.write("First line\n")
f.write("Second line\n")
f.writelines(["Third\n", "Fourth\n"])
# Append
with open("output.txt", "a") as f:
f.write("Fifth line (appended)\n")
with
The with statement (context manager) closes the file automatically, even if an exception occurs. It is the recommended style and saves marks in exams.
tell() and seek()with open("data.txt", "r") as f:
print(f.tell()) # 0 - at the start
first = f.read(5)
print(f.tell()) # 5 - after reading 5 characters
f.seek(0) # move back to the beginning
print(f.read(5)) # reads the same first 5 characters
f.seek(0, 2) # offset 0 from end (2 = SEEK_END)
print(f.tell()) # file size
pickle ModuleBinary files store data in bytes. Python's pickle module serialises (converts) Python objects into a byte stream and back.
| Function | Purpose |
|---|---|
pickle.dump(obj, file) | Write a Python object to a binary file |
pickle.load(file) | Read a Python object from a binary file |
import pickle
# --- Writing records ---
records = [("Aarav", 101), ("Diya", 102), ("Kabir", 103)]
with open("students.dat", "wb") as f:
pickle.dump(records, f)
# --- Searching for a roll number ---
roll_to_find = int(input("Enter roll number to search: "))
with open("students.dat", "rb") as f:
data = pickle.load(f)
found = False
for name, roll in data:
if roll == roll_to_find:
print("Name:", name)
found = True
break
if not found:
print("Roll number not found")
with open("sample.txt", "r") as f:
for line_no, line in enumerate(f, start=1):
print(f"{line_no}: {line.rstrip()}")
vowels = consonants = upper = lower = 0
with open("sample.txt", "r") as f:
text = f.read()
for ch in text:
if ch.isupper():
upper += 1
if ch.islower():
lower += 1
if ch.isalpha():
if ch.lower() in "aeiou":
vowels += 1
else:
consonants += 1
print("Vowels :", vowels)
print("Consonants :", consonants)
print("Uppercase :", upper)
print("Lowercase :", lower)
with open("source.txt", "r") as fin, open("target.txt", "w") as fout:
for line in fin:
if "a" not in line:
fout.write(line)
print("Lines containing 'a' were removed.")
with open("sample.txt", "r") as f:
for line in f:
words = line.split()
print("#".join(words))
freq = {}
with open("phishing.txt", "r") as f:
for line in f:
for word in line.lower().split():
word = word.strip(".,!?;:()\"'")
if word:
freq[word] = freq.get(word, 0) + 1
most_common = max(freq, key=freq.get)
print("Most common word:", most_common, "->", freq[most_common])
Always guard file operations when the file may be missing:
try:
with open("data.txt", "r") as f:
print(f.read())
except FileNotFoundError:
print("The file does not exist.")
except PermissionError:
print("Access denied.")
finally:
print("Operation attempted.")
A regular expression (regex) is a sequence of characters that defines a search pattern. Python's re module provides the tools to search, match, extract and replace text using these patterns.
import re
| Symbol | Meaning | Example | Matches |
|---|---|---|---|
. | Any character except newline | a.c | abc, a c, a9c |
^ | Start of string | ^Hello | Hello world |
$ | End of string | end$ | the end |
* | Zero or more repetitions | ab* | a, ab, abbb |
+ | One or more repetitions | ab+ | ab, abbb (not a) |
? | Zero or one (optional) | colou?r | color, colour |
{n} | Exactly n repetitions | \d{4} | 2024 |
{n,m} | Between n and m repetitions | \d{2,4} | 12, 1234 |
[] | Character class | [aeiou] | any vowel |
| | Alternation (OR) | cat|dog | cat or dog |
() | Grouping / capture | (ab)+ | ab, abab |
\ | Escape a metacharacter | \. | a literal dot |
| Sequence | Matches | Opposite |
|---|---|---|
\d | Any digit [0-9] | \D — non-digit |
\w | Word character [A-Za-z0-9_] | \W — non-word |
\s | Whitespace (space, tab, newline) | \S — non-whitespace |
\b | Word boundary | \B — non-boundary |
\A / \Z | Start / end of the entire string | — |
Write patterns as raw strings: r"\d{3}" rather than "\\d{3}". The r prefix stops Python from interpreting backslash escapes before re sees them.
re Module Functions| Function | Purpose | Returns |
|---|---|---|
re.match(p, s) | Match only at the beginning of the string | Match object or None |
re.search(p, s) | Find the first occurrence anywhere | Match object or None |
re.findall(p, s) | All non-overlapping matches | List of strings (or tuples) |
re.finditer(p, s) | All matches as an iterator | Iterator of match objects |
re.sub(p, repl, s) | Replace matches | New string |
re.split(p, s) | Split by pattern | List of strings |
re.compile(p) | Pre-compile a pattern for reuse | Pattern object |
re.match("bc", "abcde") returns None because the pattern must start at index 0. re.search("bc", "abcde") succeeds because it scans the whole string.
| Method | Returns |
|---|---|
group() | The matched substring |
group(n) | The n-th captured group |
groups() | Tuple of all captured groups |
start() / end() | Start / end index of the match |
span() | Tuple (start, end) |
import re
text = "My phone number is 9876543210 and office is 011-2345678"
print(re.findall(r"\d{10}", text))
# ['9876543210']
print(re.search(r"\d+", text).group())
# 9876543210
print(re.match(r"My", text) is not None) # True
print(re.match(r"phone", text) is None) # True (not at the start)
import re
pattern = r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
emails = ["aarav@lpu.in", "bad@@mail", "diya.s@co.org", "no-at-sign.com"]
for e in emails:
status = "Valid" if re.match(pattern, e) else "Invalid"
print(f"{e:<20} {status}")
aarav@lpu.in Valid
bad@@mail Invalid
diya.s@co.org Valid
no-at-sign.com Invalid
import re
pattern = r"^[6-9]\d{9}$" # starts with 6-9, then exactly 9 digits
for number in ["9876543210", "1234567890", "98123", "9999999999"]:
print(number, "->", bool(re.match(pattern, number)))
9876543210 -> True
1234567890 -> False
98123 -> False
9999999999 -> True
import re
log = "2024-08-15 10:32:45 ERROR Disk full"
pattern = r"(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\w+)"
m = re.search(pattern, log)
if m:
year, month, day, hh, mm, ss, level = m.groups()
print("Date :", f"{day}-{month}-{year}")
print("Time :", f"{hh}:{mm}:{ss}")
print("Level:", level)
Date : 15-08-2024
Time : 10:32:45
Level: ERROR
import re
text = "Python is fun"
# Collapse multiple spaces into one
clean = re.sub(r"\s+", " ", text)
print(clean) # Python is fun
# Mask all digits
print(re.sub(r"\d", "#", "PIN is 4321")) # PIN is ####
# Split on any non-alphanumeric run
print(re.split(r"\W+", "a,b;c d")) # ['a', 'b', 'c', 'd']
import re
from collections import Counter
with open("sample.txt", "r") as f:
text = f.read().lower()
words = re.findall(r"\b[a-z]+\b", text) # only alphabetic words
common = Counter(words).most_common(5)
for word, count in common:
print(f"{word}: {count}")
For pattern-matching questions, always state the pattern in a raw string and briefly explain each token. Common exam patterns: email, mobile number, PIN code, date (dd/mm/yyyy), and password strength. Memorise \d, \w, \s, +, *, {n,m} and anchors ^ $.
A module is a .py file containing Python code — functions, classes or variables — that can be imported into other programs.
import module_namefrom module_name import function_nameimport module_name as alias
| Standard Module | Purpose |
|---|---|
math | Mathematical functions (sqrt, pi, sin) |
random | Random numbers and choices |
os | Operating system interface (paths, directories) |
sys | Interpreter variables (argv, version) |
datetime | Date and time manipulation |
re | Regular expressions |
json | JSON encoding/decoding |
csv | CSV file reading/writing |
collections | Counter, defaultdict, namedtuple |
functools | reduce, lru_cache |
import math
print(math.sqrt(25)) # 5.0
print(math.pi) # 3.141592653589793
from random import randint, choice
print(randint(1, 6)) # 1-6
print(choice(['a', 'b', 'c'])) # random element
import datetime as dt
print(dt.datetime.now())
# File: mymath.py
def square(x):
return x * x
def cube(x):
return x ** 3
PI = 3.14159
# File: main.py
import mymath
print(mymath.square(4)) # 16
print(mymath.cube(3)) # 27
print(mymath.PI) # 3.14159
from mymath import square
print(square(5)) # 25
__name__ GuardThis idiom lets a file work both as a module and as a standalone script:
# mymodule.py
def main():
print("Running as a script")
if __name__ == "__main__":
main()
When imported, __name__ equals the module name, so main() is not called. When run directly, __name__ equals "__main__", so main() executes.
A package is a directory of modules containing an __init__.py file. It organises large projects.
mypackage/
__init__.py
geometry/
__init__.py
area.py
perimeter.py
statistics/
__init__.py
mean.py
from mypackage.geometry import area
print(area.circle(5))
An exception is an event that disrupts the normal flow of a program. Python's exception handling lets you catch and recover from errors instead of crashing.
try: risky codeexcept ExceptionType: handle itelse: runs if no exceptionfinally: always runs
| Clause | Purpose |
|---|---|
try | Enclose the code that may raise an exception |
except | Catch and handle specific exception types |
else | Runs only if no exception occurred |
finally | Always runs — used for cleanup (closing files, connections) |
raise | Manually trigger an exception |
def safe_divide():
try:
a = int(input("Numerator : "))
b = int(input("Denominator : "))
result = a / b
except ValueError:
print("Please enter valid integers.")
except ZeroDivisionError:
print("Cannot divide by zero.")
else:
print(f"Result = {result}")
finally:
print("--- Attempt complete ---")
safe_divide()
class InvalidAgeError(Exception):
"""Raised when age is outside the valid range."""
pass
def validate_age(age):
if age < 0 or age > 120:
raise InvalidAgeError(f"Invalid age: {age}")
return True
try:
validate_age(150)
except InvalidAgeError as e:
print("Caught:", e)
| Exception | Typical cause |
|---|---|
SyntaxError | Missing colon, unbalanced parentheses |
IndentationError | Inconsistent indentation |
NameError | Using an undefined variable |
TypeError | Unsupported operation between types |
ValueError | Right type, wrong value |
IndexError | Sequence index out of range |
KeyError | Missing dictionary key |
ZeroDivisionError | Division by zero |
AttributeError | Calling a missing method |
FileNotFoundError | Opening a missing file in read mode |
RecursionError | Recursion depth limit exceeded |
UnboundLocalError | Reading a local before assignment |
Prefer catching specific exceptions over a blanket except Exception. Also use finally when resources must be released regardless of success.
CO4 requires you to “store, process and sort the data”. Python provides built-in sorting tools, and understanding the underlying algorithms is frequently examined.
sort() versus sorted()| Aspect | list.sort() | sorted(iterable) |
|---|---|---|
| Returns | None (sorts in place) | A new sorted list |
| Original changed? | Yes | No |
| Works on | Lists only | Any iterable |
| Example | L.sort() | new = sorted(L) |
key and reversewords = ["banana", "apple", "cherry"]
print(sorted(words)) # alphabetical
print(sorted(words, key=len)) # by length
print(sorted(words, reverse=True)) # descending
students = [("Aarav", 88), ("Diya", 95), ("Kabir", 72)]
print(sorted(students, key=lambda s: s[1])) # by marks asc
print(sorted(students, key=lambda s: s[1], reverse=True)) # desc
records = [{"name": "A", "cgpa": 8.7},
{"name": "B", "cgpa": 9.2},
{"name": "C", "cgpa": 8.1}]
records.sort(key=lambda r: r["cgpa"])
print(records)
Python's sort is stable: items that compare equal keep their original relative order. This makes multi-key sorting possible — sort by the secondary key first, then by the primary key.
Repeatedly compare adjacent elements and swap them if they are out of order.
Best \(O(n)\) (already sorted, with flag) • Average / Worst \(O(n^{2})\) • Space \(O(1)\) • Stable: Yes
def bubble_sort(arr):
n = len(arr)
for i in range(n - 1):
swapped = False
for j in range(n - 1 - i):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped: # already sorted - early exit
break
return arr
print(bubble_sort([64, 34, 25, 12, 22, 11, 90]))
# [11, 12, 22, 25, 34, 64, 90]
def selection_sort(arr):
n = len(arr)
for i in range(n - 1):
min_idx = i
for j in range(i + 1, n):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
result, i, j = [], 0, 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
| Algorithm | Best | Average | Worst | Space | Stable |
|---|---|---|---|---|---|
| Bubble Sort | \(O(n)\) | \(O(n^2)\) | \(O(n^2)\) | \(O(1)\) | Yes |
| Selection Sort | \(O(n^2)\) | \(O(n^2)\) | \(O(n^2)\) | \(O(1)\) | No |
| Insertion Sort | \(O(n)\) | \(O(n^2)\) | \(O(n^2)\) | \(O(1)\) | Yes |
| Merge Sort | \(O(n\log n)\) | \(O(n\log n)\) | \(O(n\log n)\) | \(O(n)\) | Yes |
Tim Sort (sorted) | \(O(n)\) | \(O(n\log n)\) | \(O(n\log n)\) | \(O(n)\) | Yes |
def linear_search(arr, target):
for i, value in enumerate(arr):
if value == target:
return i
return -1
print(linear_search([4, 9, 2, 7], 7)) # 3
def binary_search(arr, target):
low, high = 0, len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
data = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
print(binary_search(data, 23)) # 5
students = [
("Aarav", "CSE", 88),
("Diya", "ECE", 95),
("Kabir", "CSE", 95),
("Meera", "ECE", 88),
]
# Secondary key first: name ascending
students.sort(key=lambda s: s[0])
# Primary key: marks descending (stable keeps name order within equal marks)
students.sort(key=lambda s: s[2], reverse=True)
for name, branch, marks in students:
print(f"{name:<7} {branch} {marks}")
Diya ECE 95
Kabir CSE 95
Aarav CSE 88
Meera ECE 88
These projects integrate multiple concepts from Unit II — data structures, functions, OOP, file handling and regex. Use them as templates for practical exams.
import pickle
class Student:
def __init__(self, roll, name, marks):
self.roll = roll
self.name = name
self.marks = marks
def average(self):
return sum(self.marks) / len(self.marks)
def __str__(self):
return f"{self.roll:<6} {self.name:<10} Avg={self.average():.2f}"
class StudentManager:
def __init__(self, filename="students.dat"):
self.filename = filename
self.students = self._load()
def _load(self):
try:
with open(self.filename, "rb") as f:
return pickle.load(f)
except (FileNotFoundError, EOFError):
return []
def _save(self):
with open(self.filename, "wb") as f:
pickle.dump(self.students, f)
def add(self, student):
self.students.append(student)
self._save()
def find(self, roll):
for s in self.students:
if s.roll == roll:
return s
return None
def topper(self):
return max(self.students, key=lambda s: s.average()) if self.students else None
def list_all(self):
for s in sorted(self.students, key=lambda s: s.average(), reverse=True):
print(s)
# --- Usage ---
mgr = StudentManager()
mgr.add(Student(101, "Aarav", [88, 92, 79]))
mgr.add(Student(102, "Diya", [95, 81, 90]))
mgr.add(Student(103, "Kabir", [72, 65, 80]))
print("All students (ranked):")
mgr.list_all()
print("\nTopper:", mgr.topper())
print("Find roll 102:", mgr.find(102))
import re
from collections import Counter
def analyse(filename):
with open(filename, "r") as f:
text = f.read()
emails = re.findall(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", text)
phones = re.findall(r"\+91-\d{10}|\b[6-9]\d{9}\b", text)
words = re.findall(r"\b[a-zA-Z]{3,}\b", text.lower())
print(f"Unique emails : {len(set(emails))}")
print(f"Unique phones : {len(set(phones))}")
print("\nTop 5 words:")
for word, n in Counter(words).most_common(5):
print(f" {word:<12} {n}")
analyse("inbox.txt")
import json
class Inventory:
def __init__(self, filename="inventory.json"):
self.filename = filename
self.items = self._load()
def _load(self):
try:
with open(self.filename, "r") as f:
return json.load(f)
except FileNotFoundError:
return {}
def _save(self):
with open(self.filename, "w") as f:
json.dump(self.items, f, indent=2)
def add_stock(self, name, qty):
self.items[name] = self.items.get(name, 0) + qty
self._save()
def remove_stock(self, name, qty):
if name not in self.items or self.items[name] < qty:
raise ValueError(f"Insufficient stock for {name}")
self.items[name] -= qty
self._save()
def low_stock(self, threshold=5):
return {k: v for k, v in self.items.items() if v < threshold}
inv = Inventory()
inv.add_stock("Laptop", 10)
inv.add_stock("Mouse", 3)
inv.add_stock("Laptop", 5)
print("Inventory:", inv.items)
print("Low stock:", inv.low_stock())
class Question:
def __init__(self, text, options, answer):
self.text = text
self.options = options
self.answer = answer
def is_correct(self, choice):
return choice.strip().upper() == self.answer
class Quiz:
def __init__(self, questions):
self.questions = questions
self.score = 0
def run(self):
for i, q in enumerate(self.questions, 1):
print(f"\nQ{i}. {q.text}")
for opt in q.options:
print(" ", opt)
try:
choice = input("Your answer: ")
except EOFError:
break
if q.is_correct(choice):
self.score += 1
print(" Correct!")
else:
print(f" Wrong. Answer was {q.answer}")
print(f"\nFinal score: {self.score}/{len(self.questions)}")
questions = [
Question("Capital of India?", ["A. Mumbai", "B. Delhi", "C. Chennai"], "B"),
Question("2 + 2 * 3 = ?", ["A. 12", "B. 8", "C. 6"], "B"),
]
Quiz(questions).run()
In practical exams, structure your answer: (1) class definition with __init__, (2) file I/O methods with exception handling, (3) main logic using the right data structure, (4) a small driver at the bottom demonstrating usage. Comments above each block earn extra marks.
| Structure | Syntax | Ordered | Mutable | Duplicates | Use when |
|---|---|---|---|---|---|
| Tuple | (1,2) | Yes | No | Yes | Fixed records, dict keys |
| Dictionary | {"a":1} | Yes (3.7+) | Yes | Keys unique | Key-based lookup, counting |
| Set | {1,2} | No | Yes | No | Uniqueness, fast membership |
| Frozenset | frozenset({1,2}) | No | No | No | Hashable set, dict keys |
| Concept | Syntax / Method |
|---|---|
| Define function | def f(a, b=1, *args, **kwargs): |
| Lambda | f = lambda x: x * 2 |
| Return multiple | return min(x), max(x) |
| Global scope | global counter |
| Enclosing scope | nonlocal x |
| Map / filter / reduce | map(f, it), filter(f, it), reduce(f, it) |
| Memoization | @lru_cache(maxsize=None) |
| Concept | Python |
|---|---|
| Class | class C(Base): |
| Constructor | def __init__(self, ...): |
| Call parent | super().__init__(...) |
| Private attribute | self.__x = value |
| Class variable | count = 0 (in class body) |
| Class method | @classmethod with cls |
| Static method | @staticmethod |
| Abstract class | from abc import ABC, abstractmethod |
| Operator overload | def __add__(self, o): |
| String representation | def __str__(self): |
| Operation | Code |
|---|---|
| Open for read | with open("f.txt", "r") as f: |
| Open for write | with open("f.txt", "w") as f: |
| Open for append | with open("f.txt", "a") as f: |
| Read all | f.read() |
| Read line | f.readline() |
| Read all lines | f.readlines() |
| Write string | f.write("text\n") |
| Write many | f.writelines(list) |
| Position | f.tell() / f.seek(n) |
| Binary pickle write | pickle.dump(obj, f) |
| Binary pickle read | pickle.load(f) |
| Pattern | Matches |
|---|---|
\d | Digit |
\w | Word character |
\s | Whitespace |
+ / * / ? | 1+, 0+, 0 or 1 repetitions |
{n,m} | Between n and m repetitions |
^ / $ | Start / end of string |
[abc] | Any of a, b, c |
(...) | Capture group |
a|b | Alternation |
re.findall(p, s) | All matches |
re.sub(p, r, s) | Replace |
| Operation | List | Dict / Set |
|---|---|---|
| Index / key access | \(O(1)\) | \(O(1)\) average |
Search (in) | \(O(n)\) | \(O(1)\) average |
| Insert at end | \(O(1)\) amortised | \(O(1)\) average |
| Insert at front | \(O(n)\) | — |
| Delete | \(O(n)\) | \(O(1)\) average |
| Sort | \(O(n\log n)\) | — |
| Exception | Typical cause |
|---|---|
SyntaxError | Missing colon, unbalanced parentheses |
IndentationError | Inconsistent indentation |
NameError | Using an undefined variable |
TypeError | Unsupported operation between types |
ValueError | Right type, wrong value |
IndexError | Sequence index out of range |
KeyError | Missing dictionary key |
ZeroDivisionError | Division by zero |
AttributeError | Calling a missing method |
FileNotFoundError | Opening a missing file in read mode |
RecursionError | Recursion depth limit (1000) exceeded |
UnboundLocalError | Reading a local before assignment |
t[0] = x; build a new tuple instead.dict.get(key, default) instead of d[key] when the key may be absent — avoids KeyError.list(set(x)) to deduplicate, but never rely on ordering.RecursionError. Always state it first.super().__init__() in child classes to call the parent constructor.with. Examiners award marks for resource management.r"\d+", not "\\d+".ValueError, ZeroDivisionError) rather than a blanket except Exception.Library with methods add_book(), issue_book() and return_book(). Store books in a dictionary. Handle the case of issuing a book that is not available.HardShape and subclasses Circle, Rectangle, Triangle.Mediumtry, except, else, finally and a custom exception.Medium| Feature | Tuple | List |
|---|---|---|
| Mutability | Immutable | Mutable |
| Syntax | (1, 2, 3) | [1, 2, 3] |
| Methods | count(), index() only | Many (append, pop, sort, …) |
| Performance | Faster, less memory | Slower, more memory |
| Dictionary key | Yes (if hashable) | No |
Scenario: storing (latitude, longitude) coordinates or returning multiple values from a function — these should not change, so a tuple guarantees integrity and can be used as a dictionary key.
import re
freq = {}
with open("input.txt", "r") as f:
for line in f:
for word in re.findall(r"[a-zA-Z]+", line.lower()):
freq[word] = freq.get(word, 0) + 1
top3 = sorted(freq.items(), key=lambda x: -x[1])[:3]
for word, count in top3:
print(f"{word}: {count}")
A = {1, 2, 3, 4}
B = {3, 4, 5, 6}
print("Union :", A | B) # {1, 2, 3, 4, 5, 6}
print("Intersection :", A & B) # {3, 4}
print("Difference :", A - B) # {1, 2}
print("Sym Diff :", A ^ B) # {1, 2, 5, 6}
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
print(fib(5)) # 5
Recursion tree for n = 5:
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
/ \
fib(1) fib(0)
Result = 5. Naive recursion has exponential time \(O(2^n)\); use memoization for efficiency.
class Library:
def __init__(self):
self.books = {} # title -> [available_count, issued_count]
def add_book(self, title, count=1):
if title in self.books:
self.books[title][0] += count
else:
self.books[title] = [count, 0]
print(f"Added {count} copies of '{title}'.")
def issue_book(self, title):
if title not in self.books or self.books[title][0] == 0:
raise ValueError(f"'{title}' is not available.")
self.books[title][0] -= 1
self.books[title][1] += 1
print(f"Issued '{title}'.")
def return_book(self, title):
if title not in self.books or self.books[title][1] == 0:
raise ValueError(f"'{title}' was not issued.")
self.books[title][0] += 1
self.books[title][1] -= 1
print(f"Returned '{title}'.")
lib = Library()
lib.add_book("Python 101", 3)
lib.add_book("Data Structures", 1)
lib.issue_book("Python 101")
lib.issue_book("Data Structures")
try:
lib.issue_book("Data Structures")
except ValueError as e:
print("Error:", e)
lib.return_book("Python 101")
print(lib.books)
# 1. Encapsulation: bundling data + methods, hiding internals
class Account:
def __init__(self, balance):
self.__balance = balance # private
def deposit(self, amt):
if amt > 0:
self.__balance += amt
def get_balance(self):
return self.__balance
# 2. Abstraction: hiding complexity behind a simple interface
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self): pass
# 3. Inheritance: reuse via "is-a"
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14 * self.r ** 2
class Rectangle(Shape):
def __init__(self, l, b): self.l, self.b = l, b
def area(self): return self.l * self.b
# 4. Polymorphism: same call, different behaviour
for shape in [Circle(5), Rectangle(4, 6)]:
print(type(shape).__name__, "->", shape.area())
rows = []
with open("data.csv", "r") as f:
for line in f:
parts = line.strip().split(",")
if len(parts) >= 2:
rows.append(tuple(parts))
rows.sort(key=lambda r: r[1])
with open("sorted.csv", "w") as f:
for row in rows:
f.write(",".join(row) + "\n")
print(f"Sorted {len(rows)} rows by column 2.")
import re
pattern = r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"
with open("input.txt", "r") as fin:
text = fin.read()
emails = sorted(set(re.findall(pattern, text)))
with open("emails.txt", "w") as fout:
for email in emails:
fout.write(email + "\n")
print(f"Extracted {len(emails)} unique email(s).")
import math
class Shape:
def area(self):
raise NotImplementedError("Subclass must implement area()")
def describe(self):
print(f"{type(self).__name__}: area = {self.area():.2f}")
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return math.pi * self.r ** 2
class Rectangle(Shape):
def __init__(self, l, b): self.l, self.b = l, b
def area(self): return self.l * self.b
class Triangle(Shape):
def __init__(self, b, h): self.b, self.h = b, h
def area(self): return 0.5 * self.b * self.h
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]
for shape in shapes:
shape.describe()
Circle: area = 78.54
Rectangle: area = 24.00
Triangle: area = 12.00
import json
class Inventory:
def __init__(self, filename="inventory.json"):
self.filename = filename
self.items = self._load()
def _load(self):
try:
with open(self.filename, "r") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}
def _save(self):
with open(self.filename, "w") as f:
json.dump(self.items, f, indent=2)
def add(self, name, qty):
self.items[name] = self.items.get(name, 0) + qty
self._save()
def remove(self, name, qty):
if name not in self.items or self.items[name] < qty:
raise ValueError(f"Cannot remove {qty} of {name}.")
self.items[name] -= qty
self._save()
def low_stock(self, threshold=5):
return {k: v for k, v in self.items.items() if v < threshold}
inv = Inventory()
inv.add("Laptop", 10)
inv.add("Mouse", 3)
inv.add("Laptop", 2)
print("Stock :", inv.items)
print("Low stock:", inv.low_stock())
class NegativeNumberError(Exception):
"""Raised when a negative number is passed."""
pass
def sqrt_safe(n):
if n < 0:
raise NegativeNumberError(f"Negative input: {n}")
return n ** 0.5
try:
result = sqrt_safe(16)
except NegativeNumberError as e:
print("Custom error:", e)
except Exception as e:
print("Unexpected:", e)
else:
print("Result =", result)
finally:
print("Operation complete")
Result = 4.0
Operation complete
marks = {"Aarav": 88, "Diya": 95, "Kabir": 72, "Meera": 35, "Zara": 40}
topper = max(marks, key=marks.get)
average = sum(marks.values()) / len(marks)
failed = [name for name, m in marks.items() if m < 40]
print("Topper :", topper, "-", marks[topper])
print(f"Average: {average:.2f}")
print("Failed :", failed) # ['Meera']
| 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 |
| Code | Resource | Purpose |
|---|---|---|
| RW-1 | datacamp.com — Intro to Python for Data Science | Guided Python course |
| RW-2 | w3schools.com/python/python_tuples.asp | Tuples and sequence reference |
| RW-3 | coursera.org/learn/python | Python from basics |
| AV-1 | nptel.ac.in/courses/106106145 | Video lectures on Python |
| SW-1 | python.org/downloads | Python interpreter |
| SW-2 | anaconda.org/anaconda/python | Anaconda distribution |
get() avoids KeyError, and comprehensions build them concisely.*args and **kwargs arguments; the LEGB rule governs name resolution.lru_cache provides memoization for recursive calls.self, dunder methods, and the four pillars — encapsulation, abstraction, inheritance, polymorphism.open() with modes r/w/a/rb/wb/ab, with for auto-close, and pickle for binary object storage; tell()/seek() control position.re module provide powerful pattern matching through metacharacters, character classes, quantifiers and groups.__name__ == "__main__" distinguishes script from library use.try / except / else / finally; catch specific exceptions and use custom exception classes for domain errors.| Course Outcome | Covered in Sections | Key Deliverables |
|---|---|---|
| CO3 — Functions and recursion | IV, V | def, parameters, arguments, scope, lambda, recursion, memoization |
| CO4 — Core data structures | I, II, III, XI | Tuples, dictionaries, sets, sorting, searching |
| CO5 — Object-oriented programming | VI, VII, XII | Classes, objects, inheritance, polymorphism, encapsulation, abstraction |
| CO6 — File handling and regex | VIII, IX, X, XII | Text/binary files, pickle, JSON, re patterns, modules, exceptions |
| Practical | Program | Covered in |
|---|---|---|
| 7 | Factorial using recursion | Example 5.1 |
| 8 | Count vowels/consonants/upper/lower in a file | Example 8.3, Solution 6 |
| 9 | Binary file with name and roll; search | Example 8.1, Solution 7 |
| 10 | Read a file line by line and print it | Example 8.2 |
| 11 | Remove lines containing 'a' into another file | Example 8.4 |
| 12 | Random dice simulator (1–6) | Unit I · Example 5.8 |
| 13 | Stack using a list | Unit I · Example 7.5, Solution 12 |
| 14 | Most common word in a phishing email file | Example 8.6, Example 9.6 |
| 15 | Print each word separated by '#' | Example 8.5 |
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, which are then prorated against the proctored coding contests.
Python Programming · Advanced Concepts & Applications
INT108 · L:T:P 3:0:2 · 4 Credits
“Programs must be written for people to read, and only incidentally for machines to execute.” — Harold Abelson