INT108 · Python Programming

Python Programming
Advanced Concepts & Applications

Unit II

Tuples · Dictionaries · Sets · Functions · Recursion

OOP · File Handling · Regex · Modules · Exceptions

Course CodeINT108
Course TitlePython Programming
L : T : P3 : 0 : 2
Credits4
WeightageATT 5 · CA 50 · ETP 45
FocusEmployability · Skill Development
Course Outcomes Mapped to Unit II

Table of Contents

ITuples — Immutable Sequences3
IIDictionaries — Key–Value Mapping6
IIISets — Unordered Unique Collections10
IVFunctions — Parameters, Arguments & Scope13
VRecursion — Concept & Applications18
VIObject-Oriented Programming — Classes & Objects21
VIIInheritance, Polymorphism & Encapsulation26
VIIIFile Handling — Text & Binary Files31
IXRegular Expressions & Pattern Matching36
XModules, Packages & Exception Handling40
XISorting, Searching & Data Processing44
XIIIntegrated Programs & Mini Projects47
XIIISummary & Quick Reference Sheet50
XIVExam Tips & Practice Questions52
XVFull Solutions to Practice Questions55
XVIReferences, Key Takeaways & CO Mapping59
How to use these notes

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.

I. Tuples — Immutable Sequences

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')
Single-element tuple

(42) is an integer, not a tuple. You must write (42,) with a trailing comma. Verify with type((42,))<class 'tuple'>.

1.1 Accessing Elements

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

1.2 Packing and Unpacking

Packing and unpacking

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

1.3 Tuples as Return Values

Example 1.1 — Returning multiple values
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

1.4 Tuple Methods and Operations

OperationExampleResult
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)
Membership3 in (1,2,3)True
len(), max(), min(), sum()sum((1,2,3))6
Conversionlist((1,2))[1, 2]
Sorting (new list)sorted((3,1,2))[1, 2, 3]

1.5 Tuple versus List

FeatureTupleList
Syntax(1, 2, 3)[1, 2, 3]
MutabilityImmutableMutable
Methods availableOnly count(), index()Many (append, sort, …)
PerformanceFaster, less memorySlower, more memory
Usable as dict keyYes (if hashable elements)No
Typical useFixed records, coordinates, function returnsCollections that change during execution
Example 1.2 — Tuple as a dictionary key
locations = {
    (28.61, 77.21): "Delhi",
    (19.07, 72.87): "Mumbai",
    (12.97, 77.59): "Bengaluru"
}
print(locations[(28.61, 77.21)])   # Delhi
Example 1.3 — Counting occurrences in a tuple
votes = ("A", "B", "A", "C", "A", "B")
for candidate in set(votes):
    print(candidate, "->", votes.count(candidate), "votes")
Example 1.4 — Named tuples for structured records
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.

Exam tip

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.

II. Dictionaries — Key–Value Mapping

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}

2.1 Accessing and Modifying Values

OperationSyntaxBehaviour
Access by keyd["name"]Raises KeyError if the key is missing
Safe accessd.get("name")Returns None (or a default) if missing
Default accessd.get("x", 0)Returns 0 if "x" is absent
Insert / updated["city"] = "Delhi"Adds or overwrites
Deletedel d["age"]Removes the pair
Popd.pop("age")Removes and returns the value
Cleard.clear()Empties the dictionary
Membership"name" in dTests keys, not values
Lengthlen(d)Number of key–value pairs
Example 2.1 — Building and updating a record
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}

2.2 Dictionary Views and Traversal

MethodReturnsExample
keys()View of all keysdict_keys(['name','cgpa'])
values()View of all valuesdict_values(['Aarav', 8.7])
items()View of (key, value) tuplesdict_items([('name','Aarav'), ...])
update(other)Merge another dictd.update({"x": 1})
setdefault(k, v)Get or insert a defaultd.setdefault("z", 0)
popitem()Remove last inserted paird.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}")

2.3 Nested Dictionaries

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}")

2.4 Dictionary Comprehension

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}
Example 2.2 — Character frequency counter
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.

Example 2.3 — Word frequency sorted by count
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.

Example 2.4 — Student result processing
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])
Example 2.5 — Merging dictionaries (Python 3.9+)
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}
Exam tip

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.

III. Sets — Unordered Unique Collections

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
Common mistake

{} creates an empty dictionary. To create an empty set you must write set().

3.1 Set Operations

OperationOperatorMethodExample (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} <= ATrue
Superset>=A.issuperset(B)A >= {1,2}True
DisjointA.isdisjoint(B){1} vs {2}True

3.2 Set Methods

MethodPurposeExample
add(x)Add a single elements.add(9)
update(iter)Add multiple elementss.update([7, 8])
remove(x)Remove x; raises KeyError if absents.remove(3)
discard(x)Remove x; silent if absents.discard(99)
pop()Remove and return an arbitrary elements.pop()
clear()Remove all elementss.clear()
copy()Shallow copyt = s.copy()

3.3 Frozenset

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"}

3.4 Set Comprehension

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}
Example 3.1 — Remove duplicates from a list
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]
Example 3.2 — Common and distinct subjects between two students
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'}
Example 3.3 — Finding missing roll numbers
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]
Example 3.4 — Fast membership testing
vowels = set("aeiou")
word = "programming"
found = {ch for ch in word if ch in vowels}
print(found)         # {'o', 'a', 'i'}

3.5 List vs Tuple vs Set vs Dictionary

StructureSyntaxOrderedMutableDuplicatesIndexed
List[1,2]YesYesYesYes
Tuple(1,2)YesNoYesYes
Set{1,2}NoYesNoNo
Dictionary{"a":1}Yes (3.7+)YesKeys uniqueBy key
Exam tip

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.

IV. Functions — Parameters, Arguments & Scope

A function is a named block of reusable code that performs a specific task. Functions support code reuse, modularity, readability and easier debugging.

4.1 Defining and Calling a Function

Syntax

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

4.2 Parameters versus Arguments

Definition

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

4.3 Types of Arguments

TypeDescriptionExample
PositionalMatched by orderdef f(a,b): ...f(1,2)
KeywordMatched by namef(b=2, a=1)
DefaultUsed when the argument is omitteddef f(a, b=10):
Variable-length (*args)Extra positional args as a tupledef f(*nums):
Variable-length (**kwargs)Extra keyword args as a dictdef f(**opts):
Ordering rule

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")

4.4 Return Values

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)

4.5 Scope of Variables — the LEGB Rule

Python resolves names in this order: Local → Enclosing → Global → Built-in.

ScopeWhere definedAccessible
LocalInside the current functionOnly within that function
EnclosingIn an outer function (closures)Inner functions
GlobalAt module levelEverywhere in the module
Built-inPython's built-in namespaceEverywhere (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
UnboundLocalError

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.

4.6 Lambda Functions

A lambda is a small anonymous function written as a single expression.

Syntax

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]

4.7 Higher-Order Functions — map, filter, reduce

FunctionPurposeExample
map(f, it)Apply f to every elementlist(map(str, [1,2]))['1','2']
filter(f, it)Keep elements where f is Truelist(filter(None, [0,1,2]))[1,2]
reduce(f, it)Fold iterable to a single valuereduce(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)

4.8 Docstrings and Function Annotations

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}
Example 4.1 — Calculator with function per operation
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
Example 4.2 — Variable-length arguments
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

V. Recursion — Concept & Applications

Definition

Recursion is a technique in which a function calls itself to solve a smaller instance of the same problem. Every recursive function must have:

5.1 Anatomy of a Recursive Function

Template

def f(n):
    if base_condition(n):
        return base_value
    return combine(n, f(smaller(n)))

Example 5.1 — Factorial by recursion
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\).

Example 5.2 — Fibonacci by recursion
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.

Example 5.3 — Sum of digits by recursion
def digit_sum(n):
    if n == 0:
        return 0
    return n % 10 + digit_sum(n // 10)

print(digit_sum(12345))     # 15
Example 5.4 — Binary search by recursion
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
Example 5.5 — Tower of Hanoi
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.

5.2 Recursion versus Iteration

AspectRecursionIteration
DefinitionFunction calls itselfLoop repeats a block
TerminationBase caseLoop condition
MemoryUses call stack — more memoryConstant memory
SpeedSlower (function-call overhead)Faster
ReadabilityElegant for tree/divide-and-conquer problemsStraightforward for linear problems
RiskRecursionError for deep recursionInfinite loop if condition never fails
Recursion limit

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.

5.3 Memoization with functools.lru_cache

Memoization 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, ...)
Exam tip

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.

VI. Object-Oriented Programming — Classes & Objects

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.

6.1 Classes and Objects

Definitions

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 syntax

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

6.2 The __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

6.3 Instance Variables versus Class Variables

AspectInstance variableClass variable
DefinedInside __init__ with self.xInside the class body, outside any method
ScopeUnique to each objectShared by all objects
Accessobj.xClassName.x or obj.x
UsePer-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)

6.4 Methods: Instance, Class and Static

TypeDecoratorFirst parameterPurpose
Instance methodnoneselfOperates on a specific object
Class method@classmethodclsOperates on the class itself (alternate constructors)
Static method@staticmethodnoneUtility 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

6.5 Dunder (Magic) Methods

Special methods with double underscores let user-defined objects integrate with Python's built-in syntax.

DunderTriggered byPurpose
__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 + bAddition
__eq__a == bEquality
__lt__a < bLess than
__getitem__obj[key]Indexing
__iter__for x in objIteration
Example 6.1 — A complete Vector class with operators
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

VII. Inheritance, Polymorphism & Encapsulation

7.1 Encapsulation and Data Hiding

Encapsulation bundles data and the methods that operate on it, and restricts direct access from outside. Python uses naming conventions rather than strict keywords.

ConventionMeaningAccess
namePublicFreely accessible
_nameProtected (by convention)Accessible, but “internal use only”
__namePrivateName-mangled to _ClassName__name
Example 7.1 — Encapsulated bank account
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.

7.2 Inheritance

Inheritance lets a class (child/derived) acquire the attributes and methods of another class (parent/base), promoting code reuse and an “is-a” relationship.

Syntax

class Child(Parent): …   •   super().__init__(...) calls the parent constructor

Types of Inheritance

TypeStructureExample
SingleA → Bclass Dog(Animal)
MultilevelA → B → CVehicle → Car → ElectricCar
HierarchicalOne parent, many childrenAnimal → Dog, Cat, Cow
MultipleTwo or more parentsclass C(A, B)
HybridCombination of the aboveDiamond-shaped hierarchies
Example 7.2 — Single inheritance with 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
Example 7.3 — Multilevel inheritance
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
Example 7.4 — Multiple inheritance and MRO
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.

7.3 Polymorphism

Polymorphism means “many forms” — the same interface behaves differently for different types.

(a) Method Overriding (Runtime Polymorphism)

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

(b) Operator Overloading

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)

(c) Duck Typing

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()

7.4 Abstraction with Abstract Classes

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.

7.5 The Four Pillars of OOP

PillarMeaningPython mechanism
EncapsulationBundle data + methods; hide internals__private, _protected, getters/setters
AbstractionExpose only what is necessaryabc.ABC, @abstractmethod
InheritanceReuse via “is-a” relationshipclass Child(Parent), super()
PolymorphismSame call, different behaviourMethod overriding, duck typing, dunder methods
Exam tip

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.

VIII. File Handling — Text & Binary Files

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.

Syntax

file_object = open("filename", "mode")  …  file_object.close()

8.1 File Modes

ModeMeaningFile existsFile missingPosition
'r'Read only (default)Reads from startFileNotFoundErrorBeginning
'w'Write onlyTruncates to emptyCreates new fileBeginning
'a'AppendAdds at endCreates new fileEnd
'r+'Read and writeYesFileNotFoundErrorBeginning
'w+'Write and readTruncatesCreates new fileBeginning
'a+'Append and readYesCreates new fileEnd
'rb'Read binaryYesFileNotFoundErrorBeginning
'wb'Write binaryTruncatesCreates new fileBeginning
'ab'Append binaryAdds at endCreates new fileEnd
Destructive mode

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.

8.2 Reading from a Text File

MethodReturns
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")

8.3 Writing to a Text File

# 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")
Use 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.

8.4 File Position — 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

8.5 Binary Files and the pickle Module

Binary files store data in bytes. Python's pickle module serialises (converts) Python objects into a byte stream and back.

FunctionPurpose
pickle.dump(obj, file)Write a Python object to a binary file
pickle.load(file)Read a Python object from a binary file
Example 8.1 — Create a binary file of names and roll numbers
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")

8.6 Worked File-Handling Programs

Example 8.2 — Read a file line by line and print it
with open("sample.txt", "r") as f:
    for line_no, line in enumerate(f, start=1):
        print(f"{line_no}: {line.rstrip()}")
Example 8.3 — Count vowels, consonants, upper and lower case in a file
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)
Example 8.4 — Remove lines containing 'a' into another file
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.")
Example 8.5 — Display each word separated by '#'
with open("sample.txt", "r") as f:
    for line in f:
        words = line.split()
        print("#".join(words))
Example 8.6 — Find the most common word in a file
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])
Exception handling with files

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.")

IX. Regular Expressions & Pattern Matching

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

9.1 Metacharacters

SymbolMeaningExampleMatches
.Any character except newlinea.cabc, a c, a9c
^Start of string^HelloHello world
$End of stringend$the end
*Zero or more repetitionsab*a, ab, abbb
+One or more repetitionsab+ab, abbb (not a)
?Zero or one (optional)colou?rcolor, 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|dogcat or dog
()Grouping / capture(ab)+ab, abab
\Escape a metacharacter\.a literal dot

9.2 Special Sequences

SequenceMatchesOpposite
\dAny digit [0-9]\D — non-digit
\wWord character [A-Za-z0-9_]\W — non-word
\sWhitespace (space, tab, newline)\S — non-whitespace
\bWord boundary\B — non-boundary
\A / \ZStart / end of the entire string
Always use raw strings

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.

9.3 The re Module Functions

FunctionPurposeReturns
re.match(p, s)Match only at the beginning of the stringMatch object or None
re.search(p, s)Find the first occurrence anywhereMatch object or None
re.findall(p, s)All non-overlapping matchesList of strings (or tuples)
re.finditer(p, s)All matches as an iteratorIterator of match objects
re.sub(p, repl, s)Replace matchesNew string
re.split(p, s)Split by patternList of strings
re.compile(p)Pre-compile a pattern for reusePattern object
match() versus search()

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.

9.4 Match Object Methods

MethodReturns
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)

9.5 Worked Examples

Example 9.1 — Basic searching
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)
Example 9.2 — Validating an email address
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
Example 9.3 — Validating an Indian mobile number
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
Example 9.4 — Extraction using groups
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
Example 9.5 — Substitution and splitting
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']
Example 9.6 — Finding the most common word in a text file
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}")
Exam tip

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 ^ $.

X. Modules, Packages & Exception Handling

10.1 Modules

A module is a .py file containing Python code — functions, classes or variables — that can be imported into other programs.

Import syntax

import module_name
from module_name import function_name
import module_name as alias

Standard ModulePurpose
mathMathematical functions (sqrt, pi, sin)
randomRandom numbers and choices
osOperating system interface (paths, directories)
sysInterpreter variables (argv, version)
datetimeDate and time manipulation
reRegular expressions
jsonJSON encoding/decoding
csvCSV file reading/writing
collectionsCounter, defaultdict, namedtuple
functoolsreduce, 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())

10.2 Creating Your Own Module

# 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

10.3 The __name__ Guard

This 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.

10.4 Packages

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))

10.5 Exception Handling

Definition

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.

Syntax

try:
    risky code
except ExceptionType:
    handle it
else:
    runs if no exception
finally:
    always runs

ClausePurpose
tryEnclose the code that may raise an exception
exceptCatch and handle specific exception types
elseRuns only if no exception occurred
finallyAlways runs — used for cleanup (closing files, connections)
raiseManually trigger an exception
Example 10.1 — Handling multiple exception types
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()
Example 10.2 — Custom exception
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)

10.6 Common Built-in Exceptions

ExceptionTypical cause
SyntaxErrorMissing colon, unbalanced parentheses
IndentationErrorInconsistent indentation
NameErrorUsing an undefined variable
TypeErrorUnsupported operation between types
ValueErrorRight type, wrong value
IndexErrorSequence index out of range
KeyErrorMissing dictionary key
ZeroDivisionErrorDivision by zero
AttributeErrorCalling a missing method
FileNotFoundErrorOpening a missing file in read mode
RecursionErrorRecursion depth limit exceeded
UnboundLocalErrorReading a local before assignment
Exam tip

Prefer catching specific exceptions over a blanket except Exception. Also use finally when resources must be released regardless of success.

XI. Sorting, Searching & Data Processing

CO4 requires you to “store, process and sort the data”. Python provides built-in sorting tools, and understanding the underlying algorithms is frequently examined.

11.1 sort() versus sorted()

Aspectlist.sort()sorted(iterable)
ReturnsNone (sorts in place)A new sorted list
Original changed?YesNo
Works onLists onlyAny iterable
ExampleL.sort()new = sorted(L)

11.2 Sorting with key and reverse

words = ["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)
Stability

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.

11.3 Bubble Sort

Repeatedly compare adjacent elements and swap them if they are out of order.

Complexity

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]

11.4 Selection Sort

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

11.5 Insertion Sort

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

11.6 Merge Sort (Divide and Conquer)

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

11.7 Comparison of Sorting Algorithms

AlgorithmBestAverageWorstSpaceStable
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

11.8 Searching

Linear Search — \(O(n)\)

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

Binary Search — \(O(\log n)\), requires a sorted array

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
Example 11.1 — Sorting a student list by two keys
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

XII. Integrated Programs & Mini Projects

These projects integrate multiple concepts from Unit II — data structures, functions, OOP, file handling and regex. Use them as templates for practical exams.

12.1 Student Management System

Project 1 — Student record manager with file persistence
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))

12.2 Text Analytics with Regex

Project 2 — Email and phone extractor with frequency report
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")

12.3 Inventory Tracker

Project 3 — Dictionary-based inventory with file I/O
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())

12.4 Quiz Application

Project 4 — MCQ quiz with OOP and exceptions
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()
Project exam strategy

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.

XIII. Summary & Quick Reference Sheet

13.1 Data Structure Comparison

StructureSyntaxOrderedMutableDuplicatesUse when
Tuple(1,2)YesNoYesFixed records, dict keys
Dictionary{"a":1}Yes (3.7+)YesKeys uniqueKey-based lookup, counting
Set{1,2}NoYesNoUniqueness, fast membership
Frozensetfrozenset({1,2})NoNoNoHashable set, dict keys

13.2 Function Reference

ConceptSyntax / Method
Define functiondef f(a, b=1, *args, **kwargs):
Lambdaf = lambda x: x * 2
Return multiplereturn min(x), max(x)
Global scopeglobal counter
Enclosing scopenonlocal x
Map / filter / reducemap(f, it), filter(f, it), reduce(f, it)
Memoization@lru_cache(maxsize=None)

13.3 OOP Cheat Sheet

ConceptPython
Classclass C(Base):
Constructordef __init__(self, ...):
Call parentsuper().__init__(...)
Private attributeself.__x = value
Class variablecount = 0 (in class body)
Class method@classmethod with cls
Static method@staticmethod
Abstract classfrom abc import ABC, abstractmethod
Operator overloaddef __add__(self, o):
String representationdef __str__(self):

13.4 File Handling Cheat Sheet

OperationCode
Open for readwith open("f.txt", "r") as f:
Open for writewith open("f.txt", "w") as f:
Open for appendwith open("f.txt", "a") as f:
Read allf.read()
Read linef.readline()
Read all linesf.readlines()
Write stringf.write("text\n")
Write manyf.writelines(list)
Positionf.tell() / f.seek(n)
Binary pickle writepickle.dump(obj, f)
Binary pickle readpickle.load(f)

13.5 Regex Cheat Sheet

PatternMatches
\dDigit
\wWord character
\sWhitespace
+ / * / ?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|bAlternation
re.findall(p, s)All matches
re.sub(p, r, s)Replace

13.6 Complexity Quick Reference

OperationListDict / 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)\)

13.7 Common Exceptions Reference

ExceptionTypical cause
SyntaxErrorMissing colon, unbalanced parentheses
IndentationErrorInconsistent indentation
NameErrorUsing an undefined variable
TypeErrorUnsupported operation between types
ValueErrorRight type, wrong value
IndexErrorSequence index out of range
KeyErrorMissing dictionary key
ZeroDivisionErrorDivision by zero
AttributeErrorCalling a missing method
FileNotFoundErrorOpening a missing file in read mode
RecursionErrorRecursion depth limit (1000) exceeded
UnboundLocalErrorReading a local before assignment

XIV. Exam Tips & Practice Questions

Top 10 Exam Tips

  1. Indentation is syntax. A missing or extra space in a block changes the program's meaning or breaks it. Use exactly 4 spaces consistently.
  2. Tuples are immutable. Never write t[0] = x; build a new tuple instead.
  3. Use dict.get(key, default) instead of d[key] when the key may be absent — avoids KeyError.
  4. Sets have no order and no duplicates. Use list(set(x)) to deduplicate, but never rely on ordering.
  5. Recursion needs a base case. Missing base case → RecursionError. Always state it first.
  6. In OOP, use super().__init__() in child classes to call the parent constructor.
  7. Close files or use with. Examiners award marks for resource management.
  8. Always use raw strings for regex: r"\d+", not "\\d+".
  9. Catch specific exceptions (ValueError, ZeroDivisionError) rather than a blanket except Exception.
  10. Comment your logic. Even one line per block demonstrates understanding and can earn partial credit when output is wrong.

Practice Questions

Q1.Explain the difference between a tuple and a list with at least four points. Give one scenario where a tuple is preferable.Easy
Q2.Write a Python program that counts the frequency of each word in a text file and prints the top 3 most frequent words using a dictionary.Medium
Q3.Given two sets A and B, write a program to print the union, intersection, difference and symmetric difference without using set methods (use operators).Easy
Q4.Write a recursive function to compute the nth Fibonacci number. Show the recursion tree for n = 5.Medium
Q5.Write a Python class 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.Hard
Q6.Explain the four pillars of OOP with a Python example for each.Medium
Q7.Write a program to read a CSV-like text file, store each row as a tuple in a list, sort the list by the second column, and write the result to another file.Medium
Q8.Write a program using regular expressions to extract all valid email addresses from a file and save them to a new file, one per line.Medium
Q9.Demonstrate method overriding and polymorphism using a base class Shape and subclasses Circle, Rectangle, Triangle.Medium
Q10.Write a program to implement a simple inventory system using JSON file storage with add, remove and low-stock report features.Hard
Q11.Explain exception handling in Python with an example that uses try, except, else, finally and a custom exception.Medium
Q12.Write a program using a dictionary to store student names and marks, then print the topper, the average marks, and the list of students who failed (marks < 40).Easy

XV. Full Solutions to Practice Questions

Solution 1 — Tuple vs List

FeatureTupleList
MutabilityImmutableMutable
Syntax(1, 2, 3)[1, 2, 3]
Methodscount(), index() onlyMany (append, pop, sort, …)
PerformanceFaster, less memorySlower, more memory
Dictionary keyYes (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.

Solution 2 — Word frequency top 3

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}")

Solution 3 — Set operations with operators

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}

Solution 4 — Recursive Fibonacci

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.

Solution 5 — Library class

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)

Solution 6 — Four pillars of OOP

# 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())

Solution 7 — CSV-like file sort by second column

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.")

Solution 8 — Extract emails to file

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).")

Solution 9 — Method overriding and polymorphism

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

Solution 10 — JSON inventory system

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())

Solution 11 — Exception handling with custom exception

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

Solution 12 — Student marks processing

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']

XVI. References, Key Takeaways & CO Mapping

16.1 Textbooks and References

CodeTitleAuthorPublisher
T-1Fundamentals of Python — First ProgramsKenneth A. LambertCengage Learning
R-1Python Programming: Using Problem Solving ApproachReema TharejaOxford University Press

16.2 Relevant Web Resources

CodeResourcePurpose
RW-1datacamp.com — Intro to Python for Data ScienceGuided Python course
RW-2w3schools.com/python/python_tuples.aspTuples and sequence reference
RW-3coursera.org/learn/pythonPython from basics
AV-1nptel.ac.in/courses/106106145Video lectures on Python
SW-1python.org/downloadsPython interpreter
SW-2anaconda.org/anaconda/pythonAnaconda distribution

16.3 Key Takeaways

  1. Tuples are immutable sequences — ideal for fixed records, coordinates and dictionary keys; they support packing/unpacking and named tuples.
  2. Dictionaries map unique keys to values with \(O(1)\) average lookup; get() avoids KeyError, and comprehensions build them concisely.
  3. Sets store unique unordered elements with full set algebra (union, intersection, difference, symmetric difference); frozensets are the hashable variant.
  4. Functions support positional, keyword, default, *args and **kwargs arguments; the LEGB rule governs name resolution.
  5. Lambda, map, filter and reduce enable functional-style programming; lru_cache provides memoization for recursive calls.
  6. Recursion needs a base case and a recursive case; it is elegant for tree/divide-and-conquer problems but uses stack memory.
  7. OOP rests on classes, objects, self, dunder methods, and the four pillars — encapsulation, abstraction, inheritance, polymorphism.
  8. Inheritance supports single, multilevel, hierarchical, multiple and hybrid forms; MRO determines which parent method is used.
  9. File handling uses open() with modes r/w/a/rb/wb/ab, with for auto-close, and pickle for binary object storage; tell()/seek() control position.
  10. Regular expressions in the re module provide powerful pattern matching through metacharacters, character classes, quantifiers and groups.
  11. Modules and packages organise code; __name__ == "__main__" distinguishes script from library use.
  12. Exception handling uses try / except / else / finally; catch specific exceptions and use custom exception classes for domain errors.

16.4 CO Mapping

Course OutcomeCovered in SectionsKey Deliverables
CO3 — Functions and recursionIV, Vdef, parameters, arguments, scope, lambda, recursion, memoization
CO4 — Core data structuresI, II, III, XITuples, dictionaries, sets, sorting, searching
CO5 — Object-oriented programmingVI, VII, XIIClasses, objects, inheritance, polymorphism, encapsulation, abstraction
CO6 — File handling and regexVIII, IX, X, XIIText/binary files, pickle, JSON, re patterns, modules, exceptions

16.5 Practical List Coverage

PracticalProgramCovered in
7Factorial using recursionExample 5.1
8Count vowels/consonants/upper/lower in a fileExample 8.3, Solution 6
9Binary file with name and roll; searchExample 8.1, Solution 7
10Read a file line by line and print itExample 8.2
11Remove lines containing 'a' into another fileExample 8.4
12Random dice simulator (1–6)Unit I · Example 5.8
13Stack using a listUnit I · Example 7.5, Solution 12
14Most common word in a phishing email fileExample 8.6, Example 9.6
15Print each word separated by '#'Example 8.5
Assessment reminder

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.

End of Unit II

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