INT108 · Python Programming

Python Programming
Complete Course Notes

Unit I

Environment Setup · Data Types · Control Flow · Strings · Collections

Functions · Recursion · OOP · File Handling · Regular Expressions

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

Table of Contents

IPython Environment Setup & Language Basics3
IIVariables, Expressions, Statements & Data Types5
IIIOperators, Operands & Precedence8
IVConditional Statements11
VIterative Statements & Random Numbers14
VIStrings — A Compound Data Type18
VIILists — Mutable Sequences22
VIIITuples — Immutable Sequences26
IXDictionaries — Key–Value Mapping28
XSets — Unordered Unique Collections31
XIFunctions, Parameters, Arguments & Recursion33
XIIObject-Oriented Programming in Python38
XIIIFile Handling — Text & Binary Files43
XIVRegular Expressions & Pattern Matching47
XVSorting, Searching & Data Processing50
XVISummary & Quick Reference Sheet52
XVIIExam Tips & Practice Questions54
XVIIIFull Solutions to Practice Questions57
XIXReferences, Key Takeaways & CO Mapping61
How to use these notes

Read each section once for understanding, then re-read only the formula boxes, tables and exam tips before the exam. Every code snippet is exam-ready — type it out at least once in a Python interpreter; muscle memory beats re-reading. The practice questions at the end are graded Easy Medium Hard and every one has a full worked solution.

I. Python Environment Setup & Language Basics

1.1 What is Python?

Python is a high-level, interpreted, general-purpose, dynamically typed programming language created by Guido van Rossum and first released in 1991. It emphasises code readability through significant whitespace (indentation) and a clean, minimal syntax.

Definition

Interpreter: a program that reads source code line by line, translates it into an intermediate form, and executes it immediately — no separate compilation-to-machine-code step is performed by the programmer.

Key Features of Python

FeatureMeaning / Benefit
InterpretedNo explicit compilation; easy debugging and rapid prototyping.
Dynamically typedVariable types are inferred at runtime; no type declaration needed.
High-levelMemory management (garbage collection) is automatic.
Object-orientedSupports classes, objects, inheritance, polymorphism, encapsulation.
Free & open sourceDownloadable from python.org; large community support.
Extensible & portableRuns on Windows, Linux, macOS; can call C/C++ libraries.
Rich standard libraryModules for files, regex, maths, networking, data science, etc.

1.2 Python Versions

Two major families exist: Python 2.x (legacy, end-of-life since January 2020) and Python 3.x (current). All new development uses Python 3. Important differences:

AspectPython 2Python 3
printStatement: print "Hi"Function: print("Hi")
Integer division5/2 = 25/2 = 2.5, 5//2 = 2
StringsASCII by defaultUnicode by default
Iterationrange() returns listrange() returns lazy object

1.3 Installing Python on Windows

  1. Visit https://www.python.org/downloads and download the latest Python 3 installer (.exe).
  2. Run the installer. Critically, tick “Add Python to PATH” before clicking Install Now.
  3. Choose Customize installation if you want to change the install directory or add optional features (pip, IDLE, documentation).
  4. Wait for the setup to complete, then click Close.
  5. Verify by opening Command Prompt and typing:
    python --version
    pip --version
  6. If python is not recognised, add the installation folder (e.g. C:\Python312\) and its Scripts subfolder to the Path environment variable manually.
Common pitfall

Forgetting to add Python to PATH is the single most common installation error. It causes 'python' is not recognized as an internal or external command on Windows.

1.4 Anaconda Distribution

Anaconda is a free distribution that bundles Python together with 250+ scientific packages (NumPy, pandas, matplotlib, scikit-learn) and the conda package manager, plus Jupyter Notebook and Spyder. It is preferred for data-science work because dependency conflicts are resolved automatically.

1.5 Modes of Running Python

ModeHow to startCharacteristics
Interactive (REPL)Type python in the terminalPrompt >>>; each statement executes immediately; state is lost on exit. Ideal for quick testing.
Script modeSave code in file.py, run python file.pyWhole program is executed top to bottom; persistent and reusable. Used for all real programs.
IDE / NotebookIDLE, PyCharm, VS Code, Jupyter, CodeTantraProvides editor, debugger, syntax highlighting, autocompletion.

1.6 Your First Program — “Hello World”

# hello.py
# This is a comment - the interpreter ignores it.
print("Hello, World!")
print("Welcome to INT108 - Python Programming")

Run it with python hello.py. Output:

Hello, World!
Welcome to INT108 - Python Programming
Example 1.1 — A complete first script
# Program: greet the user and show the Python version
import sys

name = input("Enter your name: ")
print("Hello,", name)
print("You are running Python", sys.version.split()[0])

Sample run: Enter your name: AaravHello, Aarav followed by the version string.

1.7 Comments and Indentation

# Correct indentation
if 10 > 5:
    print("Ten is greater")   # 4 spaces - part of the if-block
print("Done")                 # back to column 0 - outside the block
Error alert

Mixing tabs and spaces produces TabError. Always configure your editor to insert 4 spaces per tab.

1.8 The Python Execution Model

When you run python file.py:

  1. The source file is read and tokenised.
  2. It is parsed into an Abstract Syntax Tree (AST).
  3. The AST is compiled to bytecode (.pyc files inside __pycache__).
  4. The Python Virtual Machine (PVM) executes the bytecode line by line.
Industry relevance

Python dominates in Data Science, Machine Learning, Automation/DevOps, Web Development (Django, Flask), Cybersecurity scripting, and Scientific Computing — a key reason INT108 is tagged under Employability & Skill Development.

II. Variables, Expressions, Statements & Data Types

2.1 Variables and Naming

A variable is a name that refers to a value stored in memory. In Python, assignment creates the variable automatically — no declaration keyword is required.

Definition

Variable: a symbolic name bound to an object in memory. Assignment statement: name = expression — the expression on the right is evaluated first, then bound to the name on the left.

x = 10              # x refers to the integer object 10
message = "Hello"   # message refers to a string object
pi = 3.14159        # pi refers to a float object
is_valid = True     # is_valid refers to a bool object

Rules for Naming Variables (Identifiers)

RuleValidInvalid
Must begin with a letter or underscore_count, total2total
May contain letters, digits, underscoresstudent_1student-1
No spaces or special symbolsroll_noroll no, rate%
Cannot be a Python keywordmarksclass, if, for
Case-sensitiveAge and age are different

Python Keywords (reserved words — cannot be used as identifiers)

False   None    True    and     as      assert  async   await
break   class   continue def    del     elif    else    except
finally for     from    global  if      import  in      is
lambda  nonlocal not    or      pass    raise   return  try
while   with    yield
Avoiding NameError

NameError: name 'x' is not defined occurs when you use a variable before assigning it, or when you misspell a name. Python is case-sensitive: Total and total are two different variables. Always initialise a variable before its first use.

Example 2.1 — Reproducing and fixing a NameError
# Broken version
print(score)        # NameError: name 'score' is not defined

# Corrected version
score = 0
print(score)        # 0

2.2 Values and Types

A value is a fundamental unit of data such as a number or a string. Every value belongs to a type (class). Use the built-in type() function to inspect it.

TypePython nameExamplesMutable?
Integerint0, 42, -7, 10**20No
Floating pointfloat3.14, -0.5, 2.0e-3No
Complexcomplex2+3j, 1jNo
BooleanboolTrue, FalseNo
Stringstr"hello", 'a', "123"No
Listlist[1, 2, 3]Yes
Tupletuple(1, 2, 3)No
Dictionarydict{"a": 1}Yes
Setset{1, 2, 3}Yes
NoneNoneTypeNoneNo
print(type(10))        # <class 'int'>
print(type(3.14))      # <class 'float'>
print(type("hi"))      # <class 'str'>
print(type(True))      # <class 'bool'>
print(type([1,2]))     # <class 'list'>
Note

bool is a subclass of int: True == 1 and False == 0. Therefore True + True evaluates to 2.

2.3 Expressions and Statements

Example 2.2 — Expression evaluation
length = 12
breadth = 8
area = length * breadth          # expression evaluated, result stored
print("Area =", area)            # Area = 96

2.4 Type Conversion

Python performs implicit conversion (automatic type promotion) and supports explicit conversion (type casting) through constructor functions.

FunctionPurposeExampleResult
int(x)Convert to integer (truncates floats)int(3.9)3
float(x)Convert to floatfloat("2.5")2.5
str(x)Convert to stringstr(45)"45"
bool(x)Convert to booleanbool(0), bool("a")False, True
list(x)Convert iterable to listlist("abc")['a','b','c']
tuple(x)Convert iterable to tupletuple([1,2])(1, 2)
set(x)Convert iterable to setset([1,1,2]){1, 2}
Implicit conversion rule

\(\text{int} \rightarrow \text{float} \rightarrow \text{complex}\) — Python promotes the narrower type to the wider one automatically.

print(3 + 4.5)      # 7.5   (int promoted to float)
print(2 + 3j)       # (2+3j) (int promoted to complex)
print(int("10") + 5)  # 15  (explicit conversion of string)
ValueError

int("hello") raises ValueError: invalid literal for int() with base 10: 'hello'. Always validate user input before converting.

2.5 Input and Output

input() always returns a string. Convert it explicitly when numeric input is needed.

name = input("Enter your name: ")           # string
age  = int(input("Enter your age: "))       # convert to int
cgpa = float(input("Enter your CGPA: "))    # convert to float

print("Name:", name, "Age:", age, "CGPA:", cgpa)

Formatted Output — f-strings (recommended)

x = 12.34567
print(f"Value rounded to 2 decimals: {x:.2f}")   # 12.35
print(f"{'Python':>10}")                          #     Python
print(f"{255:b} {255:o} {255:x}")                 # 11111111 377 ff

2.6 Multiple and Simultaneous Assignment

a, b, c = 1, 2, 3          # multiple assignment
x = y = z = 0              # chained assignment
p, q = q, p                # swap without a temporary variable
Example 2.3 — Swap two numbers without a third variable
a = int(input("a = "))
b = int(input("b = "))

a, b = b, a                # tuple packing / unpacking

print("After swap: a =", a, "b =", b)

Input: a = 5, b = 9 → Output: a = 9, b = 5.

Example 2.4 — Area and circumference of a circle
import math

r = float(input("Radius: "))
area = math.pi * r ** 2
circumference = 2 * math.pi * r

print(f"Area = {area:.2f}")
print(f"Circumference = {circumference:.2f}")

For r = 7: Area = 153.94, Circumference = 43.98.

Exam tip

Questions that say “write a program to enter two numbers” almost always require int(input(...)) — writing only input() loses marks because the values remain strings and + would concatenate instead of add.

III. Operators, Operands & Precedence

An operator is a symbol that performs a computation on one or more operands. Python classifies operators into seven families.

3.1 Arithmetic Operators

OperatorNameExampleResult
+Addition7 + 310
-Subtraction7 - 34
*Multiplication7 * 321
/True division (float)7 / 23.5
//Floor division7 // 23
%Modulus (remainder)7 % 21
**Exponentiation2 ** 532
Negative operand behaviour

Floor division rounds toward negative infinity: -7 // 2 = -4, and the remainder takes the sign of the divisor: -7 % 2 = 1. This differs from C/C++ truncation.

3.2 Relational (Comparison) Operators

They return a Boolean value True or False.

OperatorMeaningExampleResult
==Equal to5 == 5True
!=Not equal to5 != 3True
>Greater than5 > 8False
<Less than5 < 8True
>=Greater than or equal5 >= 5True
<=Less than or equal5 <= 4False

Python also supports chained comparisons: 0 < x < 100 is equivalent to 0 < x and x < 100.

3.3 Logical Operators

OperatorDescriptionExampleResult
andTrue if both operands are TrueTrue and FalseFalse
orTrue if at least one is TrueTrue or FalseTrue
notNegationnot TrueFalse

Truth Table

ABA and BA or Bnot A
TrueTrueTrueTrueFalse
TrueFalseFalseTrueFalse
FalseTrueFalseTrueTrue
FalseFalseFalseFalseTrue
Short-circuit evaluation

and stops as soon as it finds a falsy operand; or stops as soon as it finds a truthy operand. Hence 0 and 1/0 returns 0 without raising ZeroDivisionError.

3.4 Assignment Operators

OperatorEquivalent toExample (x = 10)New x
=Simple assignmentx = 55
+=x = x + 2x += 212
-=x = x - 2x -= 28
*=x = x * 2x *= 220
/=x = x / 2x /= 25.0
//=x = x // 3x //= 33
%=x = x % 3x %= 31
**=x = x ** 2x **= 2100

3.5 Bitwise Operators

Operate on the binary representation of integers.

OperatorNameExample (a=12=1100, b=10=1010)Result
&ANDa & b8 (1000)
|ORa | b14 (1110)
^XORa ^ b6 (0110)
~NOT (complement)~a-13
<<Left shifta << 124
>>Right shifta >> 16
Shift equivalences

\(a \ll n = a \times 2^{n}\)   •   \(a \gg n = \lfloor a / 2^{n} \rfloor\)   •   \(\sim a = -(a+1)\)

3.6 Membership and Identity Operators

OperatorPurposeExampleResult
inTests membership in a sequence3 in [1,2,3]True
not inTests absence"z" not in "python"True
isTests identity (same object)a is bTrue if same object
is notTests non-identitya is not bTrue if different objects
== versus is

== compares values; is compares memory identity. [1,2] == [1,2] is True, but [1,2] is [1,2] is False because they are two distinct list objects.

3.7 Operator Precedence and Associativity

Precedence decides which operator binds tighter. Higher rows bind first.

LevelOperatorsAssociativity
1 (highest)(), [], {}, function callLeft → Right
2**Right → Left
3+x, -x, ~x (unary)Right → Left
4*, /, //, %Left → Right
5+, -Left → Right
6<<, >>Left → Right
7&Left → Right
8^Left → Right
9|Left → Right
10==, !=, <, >, <=, >=, is, inLeft → Right
11notRight → Left
12andLeft → Right
13orLeft → Right
14 (lowest)=, +=, -=, …Right → Left
Memory aid

PEMDAS — Parentheses, Exponentiation, Multiplication/Division, Addition/Subtraction — then comparisons, then not, and, or. When in doubt, use parentheses; they cost nothing and remove ambiguity.

Example 3.1 — Precedence trace
result = 2 + 3 * 4 ** 2 // 8 - 1
print(result)

Step-by-step: 4**2 = 163*16 = 4848//8 = 62+6 = 88-1 = 7. Output: 7.

Example 3.2 — Even/odd, positive/negative using operators
n = int(input("Enter an integer: "))
print("Even" if n % 2 == 0 else "Odd")
print("Positive" if n > 0 else ("Negative" if n < 0 else "Zero"))
Example 3.3 — Bit-level check for a power of two
n = int(input("n = "))
is_power_of_two = (n > 0) and (n & (n - 1)) == 0
print(is_power_of_two)

For n = 16: 16 & 15 = 0True. For n = 12: 12 & 11 = 8False.

Example 3.4 — Compound condition without a calculator
a, b, c = 4, 9, 2
print(a + b * c ** 2 % 5)
# 2**2 = 4 ; 9*4 = 36 ; 36%5 = 1 ; 4+1 = 5
print((a < b) and (b > c) or (a == c))
# (True) and (True) or (False) => True

IV. Conditional Statements

Conditional (selection) statements let a program choose between alternative paths of execution based on a Boolean condition. Python provides if, if-else, if-elif-else, nested if, and the conditional (ternary) expression.

4.1 The if Statement

if condition:
    statement_block

The block executes only when condition evaluates to True. A colon : and indentation are mandatory.

Example 4.1 — Simple if
marks = int(input("Enter marks: "))
if marks >= 40:
    print("Pass")
print("Result declared")   # always executes

4.2 The if-else Statement

if condition:
    block_A       # executed when condition is True
else:
    block_B       # executed when condition is False
Example 4.2 — Even or odd
n = int(input("Enter a number: "))
if n % 2 == 0:
    print(n, "is even")
else:
    print(n, "is odd")

4.3 The if-elif-else Ladder

Used when there are more than two mutually exclusive alternatives. Conditions are tested top to bottom; the first true branch executes and the rest are skipped.

if condition1:
    block1
elif condition2:
    block2
elif condition3:
    block3
else:
    default_block
Example 4.3 — Grade calculation
marks = float(input("Enter marks (0-100): "))

if marks >= 90:
    grade = "A+"
elif marks >= 80:
    grade = "A"
elif marks >= 70:
    grade = "B"
elif marks >= 60:
    grade = "C"
elif marks >= 40:
    grade = "D"
else:
    grade = "F (Fail)"

print("Grade:", grade)

For marks = 76 the first true condition is marks >= 70 → grade B.

Order matters

In an elif ladder, always place the most restrictive condition first. If you wrote marks >= 40 before marks >= 90, every mark above 40 would be graded D.

4.4 Nested if Statements

An if inside another if — used when a second decision depends on the outcome of the first.

Example 4.4 — Largest of three numbers
a = int(input("a = "))
b = int(input("b = "))
c = int(input("c = "))

if a > b:
    if a > c:
        print("Largest is", a)
    else:
        print("Largest is", c)
else:
    if b > c:
        print("Largest is", b)
    else:
        print("Largest is", c)

Equivalent single-level version:

if a >= b and a >= c:
    print("Largest is", a)
elif b >= a and b >= c:
    print("Largest is", b)
else:
    print("Largest is", c)

4.5 The Conditional (Ternary) Expression

Syntax

value_if_true if condition else value_if_false

n = 7
result = "Even" if n % 2 == 0 else "Odd"
print(result)          # Odd

# Nested ternary
x = 0
label = "Positive" if x > 0 else ("Negative" if x < 0 else "Zero")
print(label)           # Zero

4.6 Truthiness of Values

Any object can be used as a condition. The following are falsy; everything else is truthy.

Falsy valuesTruthy values
False, NoneTrue, non-zero numbers
0, 0.0, 0j"0", "False", any non-empty string
"" (empty string)[1], (0,), {1}, {"a":1}
[], (), {}, set()Any object whose __bool__ returns True
items = []
if items:
    print("Non-empty")
else:
    print("Empty list")     # prints this

4.7 Worked Program — Leap Year

Example 4.5 — Leap-year test

Rule: a year is a leap year if it is divisible by 4 but not by 100, or it is divisible by 400.

year = int(input("Enter a year: "))

if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0):
    print(year, "is a leap year")
else:
    print(year, "is not a leap year")

Test cases: 2024 → leap; 1900 → not leap; 2000 → leap.

4.8 Worked Program — Simple Calculator

Example 4.6 — Menu-driven calculator
a = float(input("First number: "))
op = input("Operator (+,-,*,/): ")
b = float(input("Second number: "))

if op == "+":
    print("Result =", a + b)
elif op == "-":
    print("Result =", a - b)
elif op == "*":
    print("Result =", a * b)
elif op == "/":
    if b == 0:
        print("Error: division by zero")
    else:
        print("Result =", a / b)
else:
    print("Invalid operator")
Exam tip

When a question says “using if-elif-else”, do not use a ternary expression or a dictionary lookup — examiners award marks for the exact construct requested. Also remember the trailing colon and consistent indentation; missing them is the most common loss of marks.

V. Iterative Statements & Random Numbers

An iterative statement (loop) repeats a block of statements. Python offers two loops — while (condition-controlled) and for (collection-controlled) — plus the break, continue and else control statements.

5.1 The while Loop

while condition:
    body          # repeats while condition is True
    update        # must eventually make condition False
Infinite loop

If the condition never becomes False, the loop never terminates. Always update the loop variable inside the body. Press Ctrl + C to interrupt a runaway loop.

Example 5.1 — Sum of the first n natural numbers (while)
n = int(input("n = "))
i, total = 1, 0
while i <= n:
    total += i
    i += 1
print("Sum =", total)

For n = 5 the loop adds 1+2+3+4+5 = 15.

5.2 The for Loop

Iterates over the items of any iterable — string, list, tuple, set, dictionary, range, file, etc.

for variable in iterable:
    body

The range() Function

Three forms

range(stop) → 0 … stop−1  •  range(start, stop) → start … stop−1  •  range(start, stop, step)

list(range(5))          # [0, 1, 2, 3, 4]
list(range(2, 8))       # [2, 3, 4, 5, 6, 7]
list(range(1, 10, 2))   # [1, 3, 5, 7, 9]
list(range(10, 0, -2))  # [10, 8, 6, 4, 2]
Important

range() in Python 3 is a lazy sequence object, not a list. Wrap it with list() to see its elements. The stop value is always excluded.

Example 5.2 — Multiplication table
n = int(input("Table of: "))
for i in range(1, 11):
    print(f"{n} x {i} = {n * i}")

5.3 Choosing Between for and while

Aspectforwhile
Use whenNumber of iterations is known or you iterate a collectionNumber of iterations is unknown; depends on a runtime condition
TerminationAutomatic when iterable is exhaustedProgrammer must update the condition
Typical useTraversing a list, string, rangeMenu loops, sentinel-controlled input, guessing games
RiskLowInfinite loop if update is forgotten

5.4 Nested Loops

A loop inside another loop. For an outer loop of \(m\) iterations and an inner loop of \(n\) iterations, the inner body executes \(m \times n\) times.

Example 5.3 — Star (right-triangle) pattern
rows = 5
for i in range(1, rows + 1):
    for j in range(i):
        print("*", end="")
    print()

Output:

*
**
***
****
*****
Example 5.4 — Multiplication tables from 2 to 5 (nested for)
for n in range(2, 6):
    print(f"--- Table of {n} ---")
    for i in range(1, 11):
        print(f"{n} x {i} = {n * i}")
    print()
Example 5.5 — Floyd's triangle (nested while)
rows = 4
num = 1
i = 1
while i <= rows:
    j = 1
    while j <= i:
        print(num, end=" ")
        num += 1
        j += 1
    print()
    i += 1
1
2 3
4 5 6
7 8 9 10

5.5 Loop Control — break, continue, else

StatementEffect
breakExits the innermost loop immediately.
continueSkips the remaining body and jumps to the next iteration.
passDoes nothing — a syntactic placeholder.
else on a loopExecutes only if the loop finished without hitting break.
Example 5.6 — Prime check using break and for-else
n = int(input("Enter a number: "))

if n < 2:
    print("Not prime")
else:
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            print(n, "is not prime")
            break
    else:
        print(n, "is prime")

The else clause runs only when no divisor was found, i.e. when break never fired.

Example 5.7 — continue: print only odd numbers
for i in range(1, 11):
    if i % 2 == 0:
        continue
    print(i, end=" ")
# Output: 1 3 5 7 9

5.6 Random Numbers in Loops

The random module generates pseudo-random numbers, essential for simulations, games and testing.

FunctionReturnsExample
random.random()Float in [0.0, 1.0)0.7231...
random.randint(a, b)Integer in [a, b] inclusiverandint(1, 6) → 1…6
random.randrange(a, b, s)Random element from range(a,b,s)randrange(0, 10, 2) → 0,2,4,6,8
random.choice(seq)Random element of a sequencechoice(['a','b','c'])
random.shuffle(lst)Shuffles a list in placeshuffle(cards)
random.sample(pop, k)List of k unique itemssample(range(1,50), 6)
random.seed(x)Fixes the sequence for reproducibilityseed(42)
Example 5.8 — Simulating a dice (Practical 12)
import random

random.seed(1)              # reproducible results
counts = [0] * 7

for _ in range(1000):
    roll = random.randint(1, 6)     # simulate one dice throw
    counts[roll] += 1

for face in range(1, 7):
    print(f"Face {face}: {counts[face]} times")
Example 5.9 — Number-guessing game (while + break)
import random

secret = random.randint(1, 50)
attempts = 0

while True:
    guess = int(input("Guess a number (1-50): "))
    attempts += 1
    if guess < secret:
        print("Too low!")
    elif guess > secret:
        print("Too high!")
    else:
        print(f"Correct! You took {attempts} attempts.")
        break

5.7 Encapsulation and Generalization

These are two fundamental program-design techniques introduced with loops and reused throughout the course.

Definitions

Encapsulation: wrapping a piece of repeated logic inside a function so that it can be called by name instead of copied. It hides the implementation details.

Generalization: replacing hard-coded literal values with parameters so the same function works for many inputs — e.g. turning a “print the 5× table” program into “print the table of n”.

Example 5.10 — From specific to general
# Specific: prints only the table of 5, only 10 rows
for i in range(1, 11):
    print(5 * i)

# Generalized: any number, any number of rows, reusable
def print_table(n, upto=10):
    """Print the multiplication table of n up to 'upto'."""
    for i in range(1, upto + 1):
        print(f"{n} x {i} = {n * i}")

print_table(5)
print_table(7, 5)

Encapsulation happened when the loop was wrapped in print_table; generalization happened when 5 and 10 became parameters.

Example 5.11 — Fibonacci series (Practical 5)
n = int(input("How many terms? "))
a, b = 0, 1
for _ in range(n):
    print(a, end=" ")
    a, b = b, a + b
print()

For n = 8: 0 1 1 2 3 5 8 13

Example 5.12 — Perfect number check (Practical 2)
n = int(input("Enter a number: "))
total = 0
for i in range(1, n):
    if n % i == 0:
        total += i

if total == n:
    print(n, "is a perfect number")
else:
    print(n, "is not a perfect number")

28 → divisors 1+2+4+7+14 = 28 → perfect.

Example 5.13 — Armstrong number check (Practical 3)
n = int(input("Enter a number: "))
digits = len(str(n))
total = 0
temp = n

while temp > 0:
    digit = temp % 10
    total += digit ** digits
    temp //= 10

print(n, "is Armstrong" if total == n else "is not Armstrong")

153 → \(1^3+5^3+3^3 = 1+125+27 = 153\) → Armstrong.

Exam tip

For nested-loop pattern questions, always trace the first three rows by hand. Examiners frequently award marks for the printed pattern even if the loop bounds are slightly wrong — but only if the logic is visible.

VI. Strings — A Compound Data Type

A string is a sequence of characters enclosed in single, double or triple quotes. It is a compound data type — built from smaller pieces (characters) — and it is immutable: once created, its characters cannot be changed in place.

s1 = 'hello'
s2 = "Python"
s3 = """A multi-line
string literal"""
s4 = str(1234)      # '1234'

6.1 Length, Indexing and Negative Indexing

The built-in len() function returns the number of characters. Indexing accesses a single character; indices start at 0 and negative indices count from the end.

Index range

For a string of length \(n\): valid indices are \(0 \ldots n-1\) and \(-1 \ldots -n\).

s = "PYTHON"
# index :  0   1   2   3   4   5
# char  :  P   Y   T   H   O   N
# neg   : -6  -5  -4  -3  -2  -1

print(len(s))     # 6
print(s[0])       # P
print(s[5])       # N
print(s[-1])      # N
print(s[-6])      # P
# print(s[6])     # IndexError: string index out of range

6.2 String Traversal

Traversal means visiting each character in turn, using either a while loop with an index or a for loop directly.

s = "Python"

# Using while + index
i = 0
while i < len(s):
    print(s[i], end="-")
    i += 1
print()

# Using for (Pythonic)
for ch in s:
    print(ch, end="-")
print()

# Using enumerate to get index and character
for idx, ch in enumerate(s):
    print(idx, ch)

6.3 Slicing

Slice syntax

s[start : stop : step] — returns characters from start up to but not including stop, taking every step-th character.

SliceResult for s = "PYTHON"Explanation
s[0:3]'PYT'Indices 0,1,2
s[:3]'PYT'Start defaults to 0
s[3:]'HON'Stop defaults to len
s[:]'PYTHON'Full copy
s[-3:]'HON'Last three
s[::2]'PTO'Every second character
s[::-1]'NOHTYP'Reversed string
s[1:5:3]'YH'Index 1 then 4
Slicing never raises IndexError

Out-of-range slice bounds are silently clamped. "abc"[1:100] returns 'bc', whereas "abc"[100] raises IndexError.

6.4 Immutability

s = "hello"
# s[0] = 'H'        # TypeError: 'str' object does not support item assignment

s = 'H' + s[1:]     # build a NEW string instead
print(s)            # Hello

6.5 String Operators

OperatorMeaningExampleResult
+Concatenation"Py" + "thon"'Python'
*Repetition"ab" * 3'ababab'
inMembership"th" in "python"True
not inNon-membership"z" not in "python"True
==, !=Equality"abc" == "abc"True
<, >Lexicographic comparison"apple" < "banana"True

6.6 Comparing Strings

Comparison uses lexicographic (dictionary) order based on the Unicode code point of each character. Uppercase letters have smaller code points than lowercase letters.

print("apple" < "banana")      # True  ('a' < 'b')
print("Apple" < "apple")       # True  ('A'=65 < 'a'=97)
print("abc" == "ABC")          # False
print("abc" < "abd")           # True  (first difference at index 2)
print("ab" < "abc")            # True  (shorter prefix is smaller)
Case-insensitive comparison

Use s1.lower() == s2.lower() or s1.casefold() == s2.casefold().

6.7 The find() Function and Searching

MethodReturnsExampleResult
find(sub)Lowest index of sub, or −1"banana".find("na")2
rfind(sub)Highest index, or −1"banana".rfind("na")4
index(sub)Like find but raises ValueError"banana".index("na")2
count(sub)Number of non-overlapping occurrences"banana".count("na")2
startswith(p)True if string begins with p"python".startswith("py")True
endswith(p)True if string ends with p"file.py".endswith(".py")True
Example 6.1 — Looping and counting with find()
def count_occurrences(text, sub):
    """Count how many times 'sub' appears in 'text' (non-overlapping)."""
    count = 0
    start = 0
    while True:
        pos = text.find(sub, start)
        if pos == -1:
            break
        count += 1
        start = pos + len(sub)
    return count

print(count_occurrences("banana", "na"))   # 2
print(count_occurrences("aaaa", "aa"))     # 2

Encapsulation: the search loop is wrapped in a function. Generalization: text and sub are parameters, so the same function works for any string.

6.8 Common String Methods

MethodPurposeExample → Result
upper() / lower()Change case"Py".upper()'PY'
capitalize()First char upper, rest lower"hello world".capitalize()'Hello world'
title()First letter of each word upper"hello world".title()'Hello World'
strip()Remove leading/trailing whitespace" hi ".strip()'hi'
replace(a, b)Substitute substring"a-b".replace("-","+")'a+b'
split(sep)Break into a list"a,b,c".split(",")['a','b','c']
join(iterable)Concatenate with separator"-".join(['a','b'])'a-b'
isdigit()All characters are digits"123".isdigit()True
isalpha()All characters are letters"abc".isalpha()True
isalnum()Letters or digits only"ab12".isalnum()True
isspace()All whitespace" \t".isspace()True
zfill(n)Pad with leading zeros"7".zfill(3)'007'

6.9 String Formatting

StyleSyntaxExample
%-formatting (legacy)"%s is %d" % (name, age)"Aarav is 19"
str.format()"{} is {}".format(name, age)"Aarav is 19"
f-string (Python 3.6+)f"{name} is {age}""Aarav is 19"
price = 1234.5678
print(f"Price: {price:.2f}")        # Price: 1234.57
print(f"{'Item':<10}{'Qty':>5}")    # Item      Qty
print(f"{0.75:.1%}")                # 75.0%

6.10 Worked Programs on Strings

Example 6.2 — Palindrome check (Practical 6)
s = input("Enter a string: ").lower().replace(" ", "")
rev = ""
for ch in s:                 # build the reversed string with a loop
    rev = ch + rev

if s == rev:
    print("Palindrome")
else:
    print("Not a palindrome")

Input Madam → cleaned madam → reversed madam → Palindrome.

Example 6.3 — Count vowels, consonants, upper and lower case (Practical 8)
text = input("Enter a line of text: ")

vowels = consonants = upper = lower = digits = 0
for ch in text:
    if ch.isupper():
        upper += 1
    if ch.islower():
        lower += 1
    if ch.isdigit():
        digits += 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)
print("Digits     :", digits)
Example 6.4 — Capitalize the first letter of every word
text = "python programming is fun"
words = text.split()
result = " ".join(w[0].upper() + w[1:] for w in words)
print(result)      # Python Programming Is Fun
Example 6.5 — Character frequency without a dictionary
s = "programming"
seen = ""
for ch in s:
    if ch not in seen:
        print(ch, ":", s.count(ch))
        seen += ch
Exam tip

Remember that len(s) is a function while s.upper() is a method. Writing s.len() is a guaranteed mark loss. Also, string methods never modify the original string — they return a new one.

VII. Lists — Mutable Sequences

A list is an ordered, mutable collection of items enclosed in square brackets. Items may be of mixed types, and lists may be nested.

numbers   = [10, 20, 30, 40]
mixed     = [1, "two", 3.0, True, None]
nested    = [[1, 2], [3, 4], [5, 6]]
empty     = []
from_range = list(range(5))        # [0, 1, 2, 3, 4]
chars      = list("abc")           # ['a', 'b', 'c']

7.1 Indexing, Slicing and Length

Lists behave exactly like strings for indexing and slicing — but they are mutable, so slice assignment and item assignment are allowed.

L = [10, 20, 30, 40, 50]
print(L[0], L[-1])        # 10 50
print(L[1:4])             # [20, 30, 40]
print(L[::-1])            # [50, 40, 30, 20, 10]
print(len(L))             # 5

L[0] = 99                 # item assignment - allowed!
print(L)                  # [99, 20, 30, 40, 50]

L[1:3] = [200, 300, 400]  # slice assignment - length may change
print(L)                  # [99, 200, 300, 400, 40, 50]

7.2 Nested Lists (Matrices)

matrix = [[1, 2, 3],
          [4, 5, 6],
          [7, 8, 9]]

print(matrix[1][2])       # 6  (row 1, column 2)

for row in matrix:
    for value in row:
        print(value, end=" ")
    print()

7.3 Traversal and Membership

fruits = ["apple", "banana", "cherry"]

for fruit in fruits:               # direct traversal
    print(fruit)

for i in range(len(fruits)):       # index-based traversal
    print(i, fruits[i])

print("banana" in fruits)          # True
print("grape" not in fruits)       # True

7.4 List Operations

OperationExampleResult
Concatenation +[1,2] + [3,4][1,2,3,4]
Repetition *[0] * 3[0,0,0]
Membership in2 in [1,2,3]True
Lengthlen([1,2,3])3
Maximum / Minimummax([3,1,2])3
Sumsum([1,2,3])6
Sorting (new list)sorted([3,1,2])[1,2,3]

7.5 List Methods — Insertion, Deletion, Substitution

MethodPurposeExample (L = [1,2,3])Result
append(x)Add one item at the endL.append(4)[1,2,3,4]
extend(iter)Append all items of an iterableL.extend([5,6])[1,2,3,5,6]
insert(i, x)Insert x at index iL.insert(1, 9)[1,9,2,3]
remove(x)Delete first occurrence of value xL.remove(2)[1,3]
pop(i)Remove and return item at index i (default last)L.pop()returns 3, L = [1,2]
del L[i]Delete item / slicedel L[0][2,3]
clear()Remove all itemsL.clear()[]
index(x)First index of value x[1,2,3].index(2)1
count(x)Number of occurrences[1,1,2].count(1)2
sort()Sort in placeL.sort(reverse=True)descending
reverse()Reverse in placeL.reverse()reversed order
copy()Shallow copyM = L.copy()new list
Example 7.1 — Insertion, deletion and substitution
L = [10, 20, 30, 40, 50]

L.append(60)          # insertion at end
L.insert(0, 5)        # insertion at index 0
print(L)              # [5, 10, 20, 30, 40, 50, 60]

L[2] = 200            # substitution
print(L)              # [5, 10, 200, 30, 40, 50, 60]

L.remove(200)         # deletion by value
del L[0]              # deletion by index
popped = L.pop()      # delete and return last
print(L, "| popped:", popped)

7.6 Aliasing versus Copying

Aliasing trap

B = A does not copy a list — it makes B refer to the same object. Modifying B also modifies A.

A = [1, 2, 3]
B = A                # ALIAS - same object
B.append(4)
print(A)             # [1, 2, 3, 4]  (A changed too!)

C = A.copy()         # SHALLOW COPY - new object
C.append(5)
print(A)             # [1, 2, 3, 4]  (unchanged)
print(C)             # [1, 2, 3, 4, 5]

D = A[:]             # another way to copy
E = list(A)          # another way to copy

For nested lists, a shallow copy still shares the inner lists. Use copy.deepcopy() for a fully independent copy.

import copy
deep = copy.deepcopy(nested)

7.7 List Comprehension

Syntax

[expression for item in iterable if condition]

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, ..., 18]

words = ["hello", "world"]
upper = [w.upper() for w in words]   # ['HELLO', 'WORLD']

matrix = [[1,2],[3,4]]
flat = [n for row in matrix for n in row]   # [1, 2, 3, 4]

7.8 Worked Programs on Lists

Example 7.2 — Largest, smallest and second largest
nums = [45, 12, 89, 33, 67, 89, 5]

largest = max(nums)
smallest = min(nums)

unique = sorted(set(nums), reverse=True)
second = unique[1] if len(unique) > 1 else None

print("Largest :", largest)
print("Smallest:", smallest)
print("Second largest:", second)

Output: Largest 89, Smallest 5, Second largest 67.

Example 7.3 — Remove duplicates preserving order
data = [1, 3, 2, 3, 5, 1, 4]
result = []
for x in data:
    if x not in result:
        result.append(x)
print(result)          # [1, 3, 2, 5, 4]
Example 7.4 — Matrix addition using nested lists
A = [[1, 2], [3, 4]]
B = [[5, 6], [7, 8]]
C = []

for i in range(len(A)):
    row = []
    for j in range(len(A[0])):
        row.append(A[i][j] + B[i][j])
    C.append(row)

print(C)               # [[6, 8], [10, 12]]
Example 7.5 — Stack implementation using a list (Practical 13)
stack = []

def push(item):
    stack.append(item)
    print(f"Pushed {item}")

def pop_item():
    if not stack:
        print("Stack underflow")
        return None
    return stack.pop()

def peek():
    return stack[-1] if stack else None

push(10); push(20); push(30)
print("Top:", peek())
print("Popped:", pop_item())
print("Stack now:", stack)
Example 7.6 — Sum and average of list elements
marks = [78, 92, 65, 88, 54]
total = sum(marks)
average = total / len(marks)
print(f"Total = {total}, Average = {average:.2f}")

VIII. 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 and safer than lists when the data must not change.

point   = (3, 5)
rgb     = (255, 128, 0)
mixed   = (1, "two", 3.0)
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])
Single-element tuple

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

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

8.2 Tuple Assignment, 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

8.3 Tuples as Return Values

A function can return several values at once by returning a tuple.

Example 8.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

8.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]

8.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 8.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 8.3 — Counting occurrences in a tuple
votes = ("A", "B", "A", "C", "A", "B")
for candidate in set(votes):
    print(candidate, "->", votes.count(candidate), "votes")
Exam tip

If a question asks “why are tuples used when lists exist?”, answer: immutability guarantees data integrity, allows use as dictionary keys, and provides better performance and memory efficiency.

IX. 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}

9.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 9.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}

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

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

9.4 Worked Programs on Dictionaries

Example 9.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 9.3 — Word frequency in a sentence, sorted
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 9.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 9.5 — Merging two dictionaries
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}

d1.update(d2)               # in-place merge
print(d1)                   # {'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.

X. Sets — Unordered Unique Collections

A set is an unordered, mutable collection of unique, hashable elements. Duplicate values 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().

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

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

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

10.4 Worked Programs on Sets

Example 10.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]

To preserve the original order while removing duplicates, use the list-and-check technique from Example 7.3.

Example 10.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 10.3 — Fast membership testing
vowels = set("aeiou")
word = "programming"
found = {ch for ch in word if ch in vowels}
print(found)         # {'o', 'a', 'i'}
Example 10.4 — 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]

XI. Functions, Parameters, Arguments & Recursion

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

11.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!

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

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

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

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

11.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]

11.7 Recursion

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:

Example 11.1 — Factorial by recursion (Practical 7)
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 11.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 11.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 11.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

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.

XII. Object-Oriented Programming in Python

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.

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

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

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

    def __init__(self, name):
        self.name = name          # instance variable (per object)

e1 = Employee("Aarav")
e2 = Employee("Diya")

print(e1.company, e2.company)     # LPU Tech LPU Tech
Employee.company = "LPU Innovations"
print(e1.company)                 # LPU Innovations (both changed)

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

12.5 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 12.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.

12.6 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 12.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 12.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 12.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.

12.7 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 (Compile-time style)

Dunder methods let user-defined objects respond to built-in operators.

OperatorDunder method
+__add__(self, other)
-__sub__(self, other)
*__mul__(self, other)
==__eq__(self, other)
<__lt__(self, other)
len()__len__(self)
print()__str__(self)
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)

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

Exam tip

Four pillars — Encapsulation (bundling + hiding), Abstraction (hiding complexity), Inheritance (reuse via “is-a”), Polymorphism (same call, different behaviour). Be ready to define each and give one code example; this is a very common 5-mark question.

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

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

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

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

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

13.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 13.1 — Create a binary file of names and roll numbers (Practical 9)
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")

13.6 Worked File-Handling Programs

Example 13.2 — Read a file line by line and print it (Practical 10)
with open("sample.txt", "r") as f:
    for line_no, line in enumerate(f, start=1):
        print(f"{line_no}: {line.rstrip()}")
Example 13.3 — Count vowels, consonants, upper and lower case in a file (Practical 8)
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 13.4 — Remove lines containing 'a' into another file (Practical 9)
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 13.5 — Display each word separated by '#' (Practical 15)
with open("sample.txt", "r") as f:
    for line in f:
        words = line.split()
        print("#".join(words))
Example 13.6 — Find the most common word in a file (Practical 14)
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.")

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

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

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

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

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

14.5 Worked Examples

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

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

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

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

15.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]

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

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

15.6 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

15.7 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 15.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

XVI. Summary & Quick Reference Sheet

16.1 Built-in Function Reference

FunctionPurposeExample → Result
print()Display outputprint("hi")
input()Read a string from the userinput("Name: ")
len(x)Number of items / characterslen("abc") → 3
type(x)Data type of an objecttype(3.0)float
int/float/strType conversionint("7") → 7
range()Sequence of integerslist(range(3)) → [0,1,2]
sum(), min(), max()Aggregate valuessum([1,2,3]) → 6
sorted(), reversed()Return sorted / reversed iteratorsorted([3,1]) → [1,3]
enumerate()Index + value pairslist(enumerate("ab"))
zip()Pair elements of iterableslist(zip([1,2],"ab"))
abs(), round(), pow()Numeric utilitiesround(3.456,1) → 3.5
isinstance(o, T)Type checkisinstance(5, int) → True

16.2 Data Structure Cheat Sheet

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

16.3 Key Syntax Summary

ConceptSyntax
Conditionalif c1: ... elif c2: ... else: ...
TernaryA if cond else B
While loopwhile cond: body
For loopfor x in iterable: body
Loop controlbreak / continue / pass / loop else
Functiondef f(a, b=1, *args, **kwargs): return value
Lambdaf = lambda x: x * 2
Classclass C(Base): def __init__(self): ...
Inheritancesuper().__init__(...)
File openwith open("f.txt", "r") as f:
Regexre.findall(r"\d+", text)
List comprehension[f(x) for x in it if cond]
Dictionary comprehension{k: v for k, v in items}
Exception handlingtry: ... except E: ... else: ... finally: ...

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

16.5 Common Exceptions Reference

ExceptionTypical cause
SyntaxErrorMissing colon, unbalanced parentheses, bad indentation structure
IndentationErrorInconsistent indentation
NameErrorUsing an undefined variable
TypeErrorUnsupported operation between types, e.g. "1" + 1
ValueErrorRight type, wrong value, e.g. int("abc")
IndexErrorSequence index out of range
KeyErrorMissing dictionary key
ZeroDivisionErrorDivision by zero
AttributeErrorCalling a method the object does not have
FileNotFoundErrorOpening a non-existent file in read mode
RecursionErrorRecursion depth limit (1000) exceeded
UnboundLocalErrorReading a local variable before assignment

XVII. 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. Colons after every block header. if x > 0:, for i in range(5):, def f():, class C:, else:, try:.
  3. input() returns a string. Wrap it with int() or float() when arithmetic is required.
  4. Remember that ranges exclude the stop value. range(1, n) runs from 1 to n−1; use range(1, n+1) for 1…n.
  5. Strings and tuples are immutable. Never write s[0] = 'X'; build a new object instead.
  6. = is assignment, == is comparison. Confusing them is the most common logic error in if statements.
  7. Close files or use with. Examiners award marks for resource management.
  8. Trace recursive functions by hand. Write the call stack for at least two levels to prove you understand the base case.
  9. State complexity where relevant. Linear search \(O(n)\), binary search \(O(\log n)\), bubble sort \(O(n^2)\), built-in sort \(O(n\log n)\).
  10. Comment your logic. Even one line per block demonstrates understanding and can earn partial credit when output is wrong.

Practice Questions

Q1.Write a Python program that accepts two integers and prints all arithmetic operations (+, −, ×, /, //, %, **) with appropriate labels.Easy
Q2.Write a program to check whether a number entered by the user is a perfect number. (A perfect number equals the sum of its proper divisors.)Easy
Q3.Write a program to check whether a given number is an Armstrong number. (An n-digit number equal to the sum of its digits each raised to the power n.)Medium
Q4.Write a program to print the Fibonacci series up to n terms using a for loop, and then again using recursion.Medium
Q5.Write a program to check whether an entered string is a palindrome, without using slicing or built-in reversal.Medium
Q6.Read a text file and display the number of vowels, consonants, uppercase letters, lowercase letters and digits in the file.Medium
Q7.Create a binary file containing names and roll numbers. Then search for a given roll number and display the corresponding name, or an appropriate message if not found.Hard
Q8.Write a Python class BankAccount with private balance, and methods deposit(), withdraw() and get_balance(). Demonstrate encapsulation.Medium
Q9.Write a program using regular expressions to validate an email address and extract all phone numbers of the form +91-XXXXXXXXXX from a text block.Hard
Q10.Write a program that takes a list of ten integers, removes duplicates, sorts the result in descending order, and prints the second largest element.Medium
Q11.Write a recursive function to compute the sum of the digits of a positive integer. Show the recursion trace for n = 4729.Medium
Q12.Implement a stack using a list with push, pop, peek and is_empty operations, and demonstrate underflow handling.Hard

XVIII. Full Solutions to Practice Questions

Solution 1 — Arithmetic operations

a = int(input("Enter first number : "))
b = int(input("Enter second number: "))

print("Addition       :", a + b)
print("Subtraction    :", a - b)
print("Multiplication :", a * b)
print("Division       :", a / b)
print("Floor Division :", a // b)
print("Modulus        :", a % b)
print("Exponent       :", a ** b)

Sample (a=7, b=2): 9, 5, 14, 3.5, 3, 1, 49.

Solution 2 — Perfect number

n = int(input("Enter a number: "))

if n < 2:
    print(n, "is not a perfect number")
else:
    divisor_sum = 0
    for i in range(1, n):
        if n % i == 0:
            divisor_sum += i

    if divisor_sum == n:
        print(n, "is a perfect number")
    else:
        print(n, "is not a perfect number")

Trace for n = 28: divisors 1, 2, 4, 7, 14 → sum = 28 → perfect. For n = 12: 1+2+3+4+6 = 16 ≠ 12 → not perfect.

Solution 3 — Armstrong number

n = int(input("Enter a number: "))
num_digits = len(str(n))
temp = n
total = 0

while temp > 0:
    digit = temp % 10
    total += digit ** num_digits
    temp //= 10

if total == n:
    print(n, "is an Armstrong number")
else:
    print(n, "is not an Armstrong number")

Trace for n = 153: digits = 3. 3³ = 27, 5³ = 125, 1³ = 1 → 27+125+1 = 153 → Armstrong.

Solution 4 — Fibonacci (iteration and recursion)

# --- Iterative version ---
def fib_iter(n):
    a, b = 0, 1
    for _ in range(n):
        print(a, end=" ")
        a, b = b, a + b
    print()

# --- Recursive version ---
def fib_rec(n):
    if n <= 1:
        return n
    return fib_rec(n - 1) + fib_rec(n - 2)

terms = int(input("How many terms? "))
fib_iter(terms)

print("Recursive:", end=" ")
for i in range(terms):
    print(fib_rec(i), end=" ")
print()

For terms = 8: 0 1 1 2 3 5 8 13 from both versions. The iterative version is \(O(n)\); the naive recursive version is \(O(2^n)\).

Solution 5 — Palindrome without slicing

s = input("Enter a string: ").lower()
reversed_s = ""

for ch in s:
    reversed_s = ch + reversed_s

if s == reversed_s:
    print("Palindrome")
else:
    print("Not a palindrome")

Trace for "madam": reversed_s builds m → am → dam → adam → madam. Equal → Palindrome.

Solution 6 — Character analysis of a file

vowels = consonants = upper = lower = digits = 0
vowel_set = set("aeiouAEIOU")

with open("sample.txt", "r") as f:
    for line in f:
        for ch in line:
            if ch.isdigit():
                digits += 1
            if ch.isalpha():
                if ch in vowel_set:
                    vowels += 1
                else:
                    consonants += 1
                if ch.isupper():
                    upper += 1
                else:
                    lower += 1

print("Vowels     :", vowels)
print("Consonants :", consonants)
print("Uppercase  :", upper)
print("Lowercase  :", lower)
print("Digits     :", digits)

Solution 7 — Binary file with search

import pickle

def write_records(filename):
    n = int(input("How many records? "))
    records = []
    for i in range(n):
        name = input(f"Name {i+1}: ")
        roll = int(input(f"Roll {i+1}: "))
        records.append((roll, name))
    with open(filename, "wb") as f:
        pickle.dump(records, f)
    print("Records saved.")

def search_record(filename, target_roll):
    try:
        with open(filename, "rb") as f:
            records = pickle.load(f)
    except FileNotFoundError:
        print("File not found.")
        return

    for roll, name in records:
        if roll == target_roll:
            print(f"Roll {roll} -> {name}")
            return
    print(f"Roll number {target_roll} not found.")

write_records("students.dat")
search_record("students.dat", int(input("Search roll number: ")))

Key points: binary mode 'wb' / 'rb', pickle.dump / pickle.load, and a clear “not found” message.

Solution 8 — Encapsulated BankAccount

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.__balance = balance          # private

    def deposit(self, amount):
        if amount <= 0:
            print("Deposit amount must be positive.")
            return
        self.__balance += amount
        print(f"Deposited Rs.{amount}. Balance = Rs.{self.__balance}")

    def withdraw(self, amount):
        if amount <= 0:
            print("Withdrawal amount must be positive.")
        elif amount > self.__balance:
            print("Insufficient balance.")
        else:
            self.__balance -= amount
            print(f"Withdrew Rs.{amount}. Balance = Rs.{self.__balance}")

    def get_balance(self):
        return self.__balance

acc = BankAccount("Aarav", 5000)
acc.deposit(2500)
acc.withdraw(1000)
acc.withdraw(100000)
print("Final balance:", acc.get_balance())

Encapsulation demonstrated: __balance is inaccessible as acc.__balance; all modification goes through validated methods.

Solution 9 — Regex validation and extraction

import re

text = """
Contact aarav.sharma@lpu.in or diya@example.org for details.
Call +91-9876543210 or +91-9123456789 between 10am and 5pm.
Invalid: bad@@mail, +91-12345
"""

# --- Email validation ---
email_pattern = r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"
emails = re.findall(email_pattern, text)
print("Emails found:", emails)

# --- Phone number extraction ---
phone_pattern = r"\+91-\d{10}"
phones = re.findall(phone_pattern, text)
print("Phone numbers:", phones)

# --- Validate one email strictly ---
candidate = "aarav.sharma@lpu.in"
strict = r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
print("Valid email?", bool(re.match(strict, candidate)))
Emails found: ['aarav.sharma@lpu.in', 'diya@example.org']
Phone numbers: ['+91-9876543210', '+91-9123456789']
Valid email? True

Solution 10 — Duplicates, sorting, second largest

numbers = []
for i in range(10):
    numbers.append(int(input(f"Enter number {i+1}: ")))

unique = list(set(numbers))          # remove duplicates
unique.sort(reverse=True)            # descending order

print("Sorted unique list:", unique)

if len(unique) >= 2:
    print("Second largest:", unique[1])
else:
    print("Not enough distinct elements.")

Note: using set() loses the original order, which does not matter here because the list is sorted afterwards. If order preservation is required, use the loop-and-check method.

Solution 11 — Recursive digit sum with trace

def digit_sum(n):
    if n == 0:
        return 0
    return n % 10 + digit_sum(n // 10)

print(digit_sum(4729))     # 22

Recursion trace for n = 4729:

digit_sum(4729)
 = 9 + digit_sum(472)
 = 9 + (2 + digit_sum(47))
 = 9 + (2 + (7 + digit_sum(4)))
 = 9 + (2 + (7 + (4 + digit_sum(0))))
 = 9 + 2 + 7 + 4 + 0
 = 22

Solution 12 — Stack implementation

class Stack:
    def __init__(self):
        self.items = []

    def is_empty(self):
        return len(self.items) == 0

    def push(self, item):
        self.items.append(item)
        print(f"Pushed: {item}")

    def pop(self):
        if self.is_empty():
            print("Stack underflow - nothing to pop.")
            return None
        return self.items.pop()

    def peek(self):
        if self.is_empty():
            print("Stack is empty.")
            return None
        return self.items[-1]

    def size(self):
        return len(self.items)

s = Stack()
s.push(10); s.push(20); s.push(30)
print("Top element:", s.peek())      # 30
print("Popped     :", s.pop())       # 30
print("Size       :", s.size())      # 2

s.pop(); s.pop()
print("Popped from empty:", s.pop()) # underflow message

XIX. References, Key Takeaways & CO Mapping

19.1 Textbooks and References

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

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

19.3 Key Takeaways

  1. Python is an interpreted, dynamically typed, high-level language; installation requires adding it to PATH, and Anaconda bundles it with data-science tooling.
  2. Variables need no declaration; names must follow identifier rules and avoid the 35 reserved keywords. Using a name before assignment raises NameError.
  3. Operators follow a strict precedence order; ** is right-associative, and parentheses always take priority.
  4. if-elif-else selects exactly one branch; conditions should be ordered from most to least restrictive.
  5. for iterates over collections, while repeats on a condition; break, continue and loop else control the flow.
  6. Strings are immutable sequences supporting indexing, negative indexing, slicing, traversal, comparison and a rich method set.
  7. Lists are mutable and support insertion, deletion, substitution, slicing, comprehension and in-place sorting; tuples are their immutable counterpart.
  8. Dictionaries map unique keys to values with \(O(1)\) average lookup; sets store unique unordered elements with fast membership testing and full set algebra.
  9. Functions promote reuse through parameters, default arguments, *args/**kwargs; recursion needs a base case and consumes stack space.
  10. OOP in Python rests on classes, objects, encapsulation, inheritance and polymorphism; dunder methods enable operator overloading.
  11. File handling uses open() with modes, and with guarantees closure; pickle handles binary object storage.
  12. Regular expressions in the re module provide powerful pattern matching through metacharacters, character classes, quantifiers and groups.

19.4 CO Mapping

Course OutcomeCovered in SectionsKey Deliverables
CO1 — Installation and basicsI, IIInstallation steps, REPL vs script mode, variables, types, I/O
CO2 — Conditionals and iterationIII, IV, Vif-elif-else, ternary, while, for, nested loops, break/continue, random
CO3 — Functions and recursionXIdef, parameters, arguments, scope, lambda, recursion with base case
CO4 — Core data structuresVI, VII, VIII, IX, X, XVStrings, lists, tuples, dictionaries, sets, sorting and searching
CO5 — Object-oriented programmingXIIClasses, objects, encapsulation, inheritance, polymorphism, abstraction
CO6 — File handling and regexXIII, XIVText/binary files, pickle, re patterns, validation and extraction

19.5 Practical List Coverage

PracticalProgramCovered in
1Arithmetic operations on two numbersSolution 1, §III
2Perfect number checkExample 5.12, Solution 2
3Armstrong number checkExample 5.13, Solution 3
4Factorial of a numberExample 11.1
5Fibonacci seriesExample 5.11, Solution 4
6Palindrome using a loopExample 6.2, Solution 5
7Factorial using recursionExample 11.1
8Count vowels/consonants/upper/lower in a fileExample 13.3, Solution 6
9Binary file with name and roll; searchExample 13.1, Solution 7
10Read a file line by line and print itExample 13.2
11Remove lines containing 'a' into another fileExample 13.4
12Random dice simulator (1–6)Example 5.8
13Stack using a listExample 7.5, Solution 12
14Most common word in a phishing email fileExample 13.6, Example 14.6
15Print each word separated by '#'Example 13.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 I

Python Programming · Complete Course Notes

INT108 · L:T:P 3:0:2 · 4 Credits

“First, solve the problem. Then, write the code.” — John Johnson