0% found this document useful (0 votes)
3 views27 pages

OOP Python Complete Notes

The document provides comprehensive study notes on Object-Oriented Programming with Python, covering basics such as Python's syntax, data types, and control flow, as well as advanced topics like functions and modules. Key differences between Python and C, error handling, and various data structures like lists, tuples, sets, and dictionaries are also discussed. Additionally, it includes practical examples and explanations of concepts like type casting, list comprehensions, and function definitions.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views27 pages

OOP Python Complete Notes

The document provides comprehensive study notes on Object-Oriented Programming with Python, covering basics such as Python's syntax, data types, and control flow, as well as advanced topics like functions and modules. Key differences between Python and C, error handling, and various data structures like lists, tuples, sets, and dictionaries are also discussed. Additionally, it includes practical examples and explanations of concepts like type casting, list comprehensions, and function definitions.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

OOP with Python

Complete Study Notes — All Modules


Object-Oriented Programming | BTech CSE
Module I: Python Basics

1.1 Introduction to Python Programming

Definition: Python is a high-level, interpreted, general-purpose programming language


known for its simple and readable syntax. It was created by Guido van Rossum and first
released in 1991. Python supports multiple programming paradigms including
procedural, object-oriented, and functional programming.

Python is widely used in web development, data science, artificial intelligence, automation, and
scientific computing. It uses indentation (whitespace) to define blocks of code instead of curly braces
like C or Java.

1.2 Python vs C — Key Differences

Definition: Python and C differ fundamentally in how they manage memory, handle
types, and execute code. Python is interpreted and dynamically typed, while C is
compiled and statically typed. Python automates memory management through garbage
collection, whereas C requires manual allocation and deallocation.

Feature Python C
Memory Management Automatic (Garbage Manual (malloc / free)
Collector)
Typing Dynamic — types Static — types declared at
determined at runtime compile time
Syntax Indentation-based blocks Brace-based blocks {}
Execution Interpreted (line by line) Compiled to machine code
Type Declaration Not required Required before use
Speed Slower (interpreted) Faster (compiled)
Code Length Shorter, more readable More verbose
Portability Highly portable Platform dependent

1.3 Python Syntax and Indentation

Definition: Python uses indentation (spaces or tabs) to define the structure and
grouping of code blocks such as loops, conditionals, and functions. Unlike other
languages that use braces {}, Python enforces consistent indentation as part of its
syntax.

if True:
print('This is inside the if block') # 4 spaces indentation
print('This is outside')
Note: Mixing tabs and spaces causes IndentationError. Always use 4 spaces per level
as per PEP 8.

1.4 Data Types in Python

Definition: A data type defines the type of value a variable can hold and the operations
that can be performed on it. Python has several built-in data types that are automatically
assigned based on the value provided (dynamic typing).

Data Type Description Example


int Integer numbers (no decimal) x = 10
float Decimal/floating-point numbers x = 3.14
complex Complex numbers with real & x = 2 + 3j
imaginary parts
str Sequence of characters x = 'Hello'
(immutable)
bool Boolean — True or False x = True
(subclass of int)
NoneType Represents absence of value x = None
list Ordered, mutable collection x = [1, 2, 3]
tuple Ordered, immutable collection x = (1, 2, 3)
set Unordered, unique elements x = {1, 2, 3}
dict Key-value pairs x = {'a': 1}

1.5 Variables and Input/Output

Definition: A variable is a named location in memory used to store data. In Python,


variables do not need explicit type declarations — the type is inferred from the value
assigned. The input() function reads user input as a string, and print() displays output to
the screen.

name = input('Enter your name: ') # reads string from user


age = int(input('Enter age: ')) # convert string to int
print('Hello', name, '! Age:', age) # output
print(f'Hello {name}, you are {age} years old.') # f-string

1.6 Operators and Expressions

Definition: Operators are special symbols used to perform operations on variables and
values. An expression is a combination of variables, constants, and operators that
evaluates to a value.
Operator Type Operators Example
Arithmetic +, -, *, /, //, %, ** 10 // 3 = 3 | 2**3 = 8
Comparison ==, !=, >, <, >=, <= 5 > 3 → True
Logical and, or, not True and False → False
Bitwise &, |, ^, ~, <<, >> 5&3=1
Assignment =, +=, -=, *=, /= x += 5 means x = x + 5
Identity is, is not x is None
Membership in, not in 'a' in 'apple' → True

1.7 Type Casting

Definition: Type casting (or type conversion) is the process of converting a value from
one data type to another. In Python, this is done using built-in functions like int(), float(),
str(), bool(), etc.

Two types of type conversion:


➤ Implicit — Python automatically converts (e.g., int + float → float)
➤ Explicit — Programmer manually converts using casting functions

int('42') → 42 # string to int


float(3) → 3.0 # int to float
str(100) → '100' # int to string
bool(0) → False # 0 is False, any non-zero is True
list((1, 2, 3)) → [1, 2, 3] # tuple to list

1.8 Basic Error Handling

Definition: Error handling is the process of responding to exceptions (runtime errors) in


a controlled way so that the program does not crash unexpectedly. Python uses try-
except blocks to catch and handle exceptions gracefully.

Types of errors in Python:


➤ SyntaxError — incorrect Python syntax (e.g., missing colon)
➤ TypeError — wrong data type used in operation
➤ ValueError — correct type but invalid value (e.g., int('abc'))
➤ ZeroDivisionError — dividing a number by zero
➤ IndexError — accessing list index that doesn't exist
➤ KeyError — accessing a dictionary key that doesn't exist
➤ FileNotFoundError — trying to open a file that doesn't exist

try:
x = int(input('Enter number: '))
result = 10 / x
except ValueError:
print('Please enter a valid number')
except ZeroDivisionError:
print('Cannot divide by zero')
else:
print('Result:', result) # runs only if no exception
finally:
print('Execution complete') # always runs

Note: The finally block always executes regardless of whether an exception occurred or
not. It is typically used for cleanup operations like closing files.

Module II: Control Flow & Data Structures

2.1 Conditional Statements

Definition: Conditional statements allow a program to make decisions and execute


different blocks of code based on whether a condition is True or False. Python uses if,
elif (else if), and else for decision-making.

marks = int(input('Enter marks: '))


if marks >= 90:
print('Grade: A')
elif marks >= 75:
print('Grade: B')
elif marks >= 60:
print('Grade: C')
else:
print('Grade: F')

2.2 Loops

Definition: A loop is a programming construct that repeats a block of code multiple


times until a specified condition is met. Python provides two types of loops: for loops
(used to iterate over a sequence) and while loops (used when the number of iterations is
not known in advance).

for Loop
Used to iterate over a sequence (list, tuple, string, range, etc.)
for i in range(1, 6): # prints 1 to 5
print(i)

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


for fruit in fruits:
print(fruit)
while Loop
Repeats as long as a condition is True. Must have a way to become False, or it runs forever (infinite
loop).
count = 0
while count < 5:
print(count)
count += 1

Loop Control Statements


Statement Description Example
break Exits the loop immediately if x == 5: break
continue Skips current iteration, goes to next if x % 2 == 0: continue
pass Does nothing — used as a for x in range(5): pass
placeholder

2.3 Lists

Definition: A list is an ordered, mutable (changeable) collection that can hold elements
of different data types. Lists are defined using square brackets [] and support indexing,
slicing, and various built-in methods.

nums = [10, 20, 30, 40, 50]


nums[0] → 10 # indexing
nums[-1] → 50 # negative index (from end)
nums[1:4] → [20,30,40] # slicing
[Link](60) # adds 60 at end
[Link](2, 25) # inserts 25 at index 2
[Link](30) # removes first occurrence of 30
[Link]() # sorts in ascending order
len(nums) # number of elements

2.4 Tuples

Definition: A tuple is an ordered, immutable (unchangeable) collection. Once created,


the elements of a tuple cannot be modified. Tuples are defined using parentheses () and
are generally faster than lists. They are used for fixed data that should not change.

coords = (10.5, 20.3)


coords[0] → 10.5
# coords[0] = 5 → TypeError: tuple object does not support item assignment
x, y = coords # tuple unpacking

Feature List Tuple


Mutable Yes — can be changed No — fixed after creation
Syntax [1, 2, 3] (1, 2, 3)
Speed Slower Faster
Use case Dynamic data Fixed/constant data
Methods Many (append, sort, etc.) Few (count, index only)

2.5 Sets

Definition: A set is an unordered collection of unique elements. Sets do not allow


duplicate values and do not maintain insertion order. They are defined using curly
braces {} and are useful for membership testing and set operations like union,
intersection, and difference.

s = {1, 2, 3, 3, 4} # duplicates removed automatically


print(s) # {1, 2, 3, 4}
[Link](5) # add element
[Link](2) # remove element

a = {1, 2, 3}; b = {2, 3, 4}


a | b → {1,2,3,4} # union
a & b → {2,3} # intersection
a - b → {1} # difference
a ^ b → {1,4} # symmetric difference

2.6 Dictionaries

Definition: A dictionary is an unordered (ordered from Python 3.7+) collection of key-


value pairs. Each key must be unique and immutable (like a string or number).
Dictionaries are used when you want to associate related data together, like a real-world
dictionary where each word has a meaning.

student = {'name': 'Alice', 'age': 20, 'marks': 95}


student['name'] → 'Alice' # access by key
student['age'] = 21 # update value
student['grade'] = 'A' # add new key-value
del student['marks'] # delete key
[Link]() → dict_keys(['name', 'age', 'grade'])
[Link]() → dict_values(['Alice', 21, 'A'])
[Link]() → list of (key, value) tuples

2.7 List Comprehensions

Definition: A list comprehension is a concise and readable way to create a new list by
applying an expression to each item in an existing iterable, optionally filtering items with
a condition. It replaces the need for a for loop with append() in a single line.

Syntax: [expression for item in iterable if condition]


# Without list comprehension
squares = []
for x in range(10):
[Link](x**2)

# With list comprehension (same result, one line)


squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
words = [[Link]() for w in ['hello', 'world']]

2.8 Arrays and User-Defined Datatypes

Definition: An array is a collection of elements of the same data type stored in


contiguous memory. In Python, the array module provides arrays that are more
memory-efficient than lists for numeric data. User-defined datatypes are custom data
structures created using classes that bundle data and behaviour together.

import array
arr = [Link]('i', [1, 2, 3, 4, 5]) # 'i' = integer type
[Link](6)
print(arr[0]) # 1

Note: For most practical purposes, Python lists are used instead of arrays. NumPy
arrays are used in scientific computing for high-performance numeric operations.

Module III: Functions & Modules

3.1 Defining and Calling Functions

Definition: A function is a reusable, named block of code that performs a specific task.
Functions help avoid code repetition (DRY principle — Don't Repeat Yourself), make
programs modular, and easier to test and debug. Functions are defined using the def
keyword.

def greet(name, msg='Hello'): # msg has a default value


'''Greets a person with a message.''' # docstring
return f'{msg}, {name}!'

print(greet('Alice')) # Hello, Alice!


print(greet('Bob', 'Good day')) # Good day, Bob!

Argument Type Description Example


Positional Matched by order of parameters greet('Alice', 'Hi')
Keyword Matched by parameter name greet(msg='Hi',
name='Alice')
Default Has a fallback value if not def greet(name,
provided msg='Hello')
*args Variable number of positional def f(*args): for a in args
arguments
**kwargs Variable number of keyword def f(**kwargs):
arguments [Link]()

3.2 Parameters and Return Values

Definition: Parameters are variables listed in the function definition that receive values
when the function is called. Return values are the output that a function sends back to
the caller using the return statement. A function can return multiple values as a tuple.

def min_max(numbers):
return min(numbers), max(numbers) # returns tuple

low, high = min_max([3, 1, 7, 2, 9])


print(low, high) # 1 9

Note: If a function has no return statement, it returns None by default.

3.3 Namespace, Scope and Lifetime of Variables

Definition: A namespace is a container that holds a mapping of names to objects.


Scope defines where in the code a variable is accessible. The lifetime of a variable is
the period during which the variable exists in memory. Python follows the LEGB rule to
resolve variable names.

Scope Description Lifetime


L — Local Variables defined inside the current Until function returns
function
E — Enclosing Variables in enclosing/outer function Until outer function
(closures) returns
G — Global Variables defined at the module/file Until program ends
level
B — Built-in Python's pre-defined names like len, Always available
print, range

x = 'global'
def outer():
x = 'enclosing'
def inner():
x = 'local'
print(x) # prints 'local' (L takes priority)
inner()
outer()
3.4 Built-in Functions

Definition: Python provides a large set of built-in functions that are always available
without importing any module. These functions perform common operations like
input/output, type conversion, mathematical operations, and sequence manipulation.

Function Purpose Example


print() Displays output to screen print('Hello')
input() Reads user input as string name = input()
len() Returns length of sequence len([1,2,3]) → 3
range() Generates a sequence of range(0, 10, 2)
numbers
type() Returns data type of object type(3.14) → float
int/float/str/bool() Type conversion functions int('42') → 42
max(), min() Largest/smallest value max([3,1,4]) → 4
sum() Sum of all elements sum([1,2,3]) → 6
sorted() Returns sorted list sorted([3,1,2]) → [1,2,3]
enumerate() Adds index to iterable for i,v in enumerate(lst)
zip() Combines two iterables zip([1,2],[3,4])

3.5 Lambda Functions

Definition: A lambda function is an anonymous (nameless), single-expression function


defined using the lambda keyword. It can take any number of arguments but can only
have one expression. Lambda functions are mainly used as short-lived functions passed
as arguments to other functions like map(), filter(), and sorted().

# Regular function
def square(x): return x**2

# Equivalent lambda
square = lambda x: x**2
print(square(5)) # 25

# Lambda with multiple arguments


add = lambda a, b: a + b
print(add(3, 4)) # 7

3.6 map(), filter(), reduce()

Definition: These are higher-order functions — functions that take other functions as
arguments. map() applies a function to every item of an iterable. filter() selects items
from an iterable based on a condition. reduce() (from functools) applies a function
cumulatively to reduce the iterable to a single value.

nums = [1, 2, 3, 4, 5]

# map() — apply function to each element


doubled = list(map(lambda x: x*2, nums)) # [2,4,6,8,10]

# filter() — keep elements that satisfy condition


evens = list(filter(lambda x: x%2==0, nums)) # [2,4]

# reduce() — accumulate to single value


from functools import reduce
total = reduce(lambda a,b: a+b, nums) # 15

3.7 Recursion

Definition: Recursion is a technique where a function calls itself to solve a smaller


version of the same problem. Every recursive function must have a base case (a
condition that stops the recursion) and a recursive case (where the function calls itself
with a smaller input). Without a base case, recursion leads to infinite loop and stack
overflow.

def factorial(n):
if n == 0 or n == 1: # base case
return 1
return n * factorial(n - 1) # recursive case

print(factorial(5)) # 5*4*3*2*1 = 120

def fibonacci(n):
if n <= 1: return n
return fibonacci(n-1) + fibonacci(n-2)

Note: Each recursive call is stored on the call stack. Python has a default recursion limit
of 1000 ([Link]()). Deep recursion can cause RecursionError.

3.8 Creating and Importing Modules

Definition: A module is a file containing Python definitions and statements (functions,


classes, variables). Modules help organize code into logical, reusable units. You can
import a module using the import statement, or import specific names using
from...import.

# [Link]
def add(a, b): return a + b
PI = 3.14159
# [Link]
import mymodule
print([Link](2, 3)) # 5

from mymodule import add, PI


print(add(2,3), PI) # 5 3.14159

import numpy as np # alias

if __name__ == '__main__': # runs only when executed directly


print('Running as main')

Module IV: Object-Oriented Programming

4.1 Introduction to OOP Paradigms

Definition: Object-Oriented Programming (OOP) is a programming paradigm that


organizes software design around data (objects) rather than functions and logic. An
object is an instance of a class that bundles together related data (attributes) and
behaviour (methods). OOP makes programs modular, reusable, and easier to maintain.

The four main pillars of OOP are:


➤ Encapsulation — bundling data and methods, hiding internal details
➤ Abstraction — exposing only essential features, hiding complexity
➤ Inheritance — a class can acquire properties of another class
➤ Polymorphism — same interface, different implementations

4.2 OOP vs Procedural Programming

Definition: Procedural programming organizes code as a sequence of instructions


(functions/procedures) that operate on data. OOP organizes code around objects that
combine both data and the functions that work on that data. OOP is better for large,
complex programs while procedural is simpler for small scripts.

Feature Procedural Object-Oriented


Focus Functions/procedures Objects and classes
Data Global or passed around Encapsulated inside objects
Reusability Function reuse Class/inheritance reuse
Security Less secure (global data) More secure (encapsulation)
Examples C, Pascal Python, Java, C++

4.3 Classes and Objects


Definition: A class is a blueprint or template for creating objects. It defines what
attributes (data) and methods (behaviour) the objects of that class will have. An object is
a specific instance of a class — created from the blueprint with its own set of values for
the attributes.

Analogy: A class is like a cookie cutter. Objects are the cookies made from it. All cookies have the
same shape (structure) but can have different toppings (values).

class Student:
school = 'ABC College' # class variable (shared by all objects)

def __init__(self, name, roll, marks): # constructor


[Link] = name # instance variable
[Link] = roll
[Link] = marks

def display(self):
print(f'Name: {[Link]}, Roll: {[Link]}, Marks: {[Link]}')

def grade(self):
if [Link] >= 90: return 'A'
elif [Link] >= 75: return 'B'
else: return 'C'

# Creating objects
s1 = Student('Alice', 101, 92)
s2 = Student('Bob', 102, 78)
[Link]() # Name: Alice, Roll: 101, Marks: 92
print([Link]()) # A
print([Link]) # ABC College (class variable)

4.4 The self Keyword

Definition: self is a reference to the current instance (object) of the class. It is used to
access instance variables and methods from within the class. self must be the first
parameter of every instance method, but it is not passed explicitly when calling the
method — Python does it automatically.

class Car:
def __init__(self, brand, speed):
[Link] = brand # '[Link]' belongs to this specific car
[Link] = speed

def describe(self):
print(f'{[Link]} runs at {[Link]} km/h')

c1 = Car('Toyota', 120)
c2 = Car('BMW', 200)
[Link]() # Toyota runs at 120 km/h
[Link]() # BMW runs at 200 km/h
Note: self is not a keyword in Python — it is a convention. You can technically name it
anything, but always use self for readability and best practice.

4.5 Class Variables vs Instance Variables

Definition: A class variable is shared by all objects of the class — it is defined outside
any method but inside the class. An instance variable is unique to each object — it is
defined inside __init__ using self. Changing a class variable affects all objects, while
changing an instance variable affects only that specific object.

Feature Class Variable Instance Variable


Definition Inside class, outside methods Inside __init__ with self
Shared by All objects of the class Each object has its own copy
Access [Link] or [Link] [Link] only
Change affects All objects Only that specific object
Example [Link] = 'ABC' [Link] = 'Alice'

4.6 Constructors — __init__

Definition: A constructor is a special method that is automatically called when a new


object of a class is created. In Python, the constructor is the __init__() method. It is used
to initialize the instance variables of the object with the values provided at the time of
object creation.

Types of constructors:
➤ Default constructor — no parameters (except self)
➤ Parameterized constructor — accepts arguments to initialize attributes
class Rectangle:
def __init__(self, length=1, width=1): # default values
[Link] = length
[Link] = width

def area(self):
return [Link] * [Link]

r1 = Rectangle() # uses defaults: 1x1


r2 = Rectangle(5, 3) # 5x3
print([Link]()) # 15

4.7 Method Types

Definition: Python supports three types of methods inside a class: instance methods
(work on object data, take self), class methods (work on class data, take cls, use
@classmethod), and static methods (don't access class or instance data, use
@staticmethod).

class MathUtils:
pi = 3.14159

def __init__(self, value):


[Link] = value

def double(self): # instance method


return [Link] * 2

@classmethod
def circle_area(cls, r): # class method
return [Link] * r * r

@staticmethod
def add(a, b): # static method
return a + b

m = MathUtils(10)
print([Link]()) # 20
print(MathUtils.circle_area(5)) # 78.53975
print([Link](3, 4)) # 7

4.8 Special (Dunder) Methods

Definition: Dunder (double underscore) methods, also called magic methods, are
special methods in Python that start and end with double underscores (e.g., __init__,
__str__). They allow objects to implement and interact with built-in Python operations
like printing, addition, comparison, and more. Python calls these automatically in
response to certain operations.

Method Triggered By Purpose


__init__(self) obj = Class() Constructor — initializes object
__str__(self) print(obj) or str(obj) Human-readable string
representation
__repr__(self) repr(obj) or in REPL Official string for debugging
__len__(self) len(obj) Return length/size of object
__add__(self, other) obj1 + obj2 Addition operator overloading
__sub__(self, other) obj1 - obj2 Subtraction operator
overloading
__eq__(self, other) obj1 == obj2 Equality comparison
__lt__(self, other) obj1 < obj2 Less-than comparison
__del__(self) del obj or garbage Destructor — cleanup on
collected deletion
__getitem__(self, key) obj[key] Indexing support
__contains__(self, item) item in obj Membership testing

4.9 Operator Overloading

Definition: Operator overloading means giving additional meaning to existing Python


operators for user-defined objects. By defining special dunder methods inside a class,
you can make Python operators like +, -, *, ==, < work with objects of your class in a
meaningful way.

class Vector:
def __init__(self, x, y):
self.x = x
self.y = y

def __add__(self, other): # overload +


return Vector(self.x + other.x, self.y + other.y)

def __sub__(self, other): # overload -


return Vector(self.x - other.x, self.y - other.y)

def __str__(self): # readable output


return f'Vector({self.x}, {self.y})'

v1 = Vector(2, 3)
v2 = Vector(1, 4)
print(v1 + v2) # Vector(3, 7)
print(v1 - v2) # Vector(1, -1)

4.10 Aggregation vs Composition

Definition: Both Aggregation and Composition represent HAS-A relationships between


classes, meaning one class contains an object of another class as part of its definition.
They differ in the degree of ownership: Composition implies a strong ownership (child
cannot exist without parent), while Aggregation implies a weak relationship (child can
exist independently).

Feature Aggregation (weak HAS-A) Composition (strong HAS-A)


Dependency Child can exist without parent Child cannot exist without
parent
Ownership Weak — shared ownership Strong — parent owns child
Example Department HAS-A Employee House HAS-A Room
If parent deleted Child still exists Child also deleted

# Composition — Room cannot exist without House


class Room:
def __init__(self, name): [Link] = name

class House:
def __init__(self):
[Link] = [Room('Living'), Room('Bedroom')] # created inside

# Aggregation — Employee exists independently


class Employee:
def __init__(self, name): [Link] = name

class Department:
def __init__(self, emp): [Link] = emp # passed from outside

Module V: Advanced OOP Concepts

5.1 Inheritance

Definition: Inheritance is the mechanism by which one class (called the child or derived
class) acquires the properties and methods of another class (called the parent or base
class). It promotes code reusability — the child class inherits all attributes and methods
of the parent and can also add new ones or override existing ones. The relationship is
described as IS-A (e.g., a Dog IS-A Animal).

Type Description Example


Single One parent, one child class Dog(Animal)
Multiple Child inherits from multiple parents class C(A, B)
Multilevel Chain of inheritance A→B→C class C(B) where class B(A)
Hierarchical Multiple children from one parent Dog and Cat both inherit
Animal
Hybrid Combination of above types Mix of multiple and
multilevel

class Animal:
def __init__(self, name):
[Link] = name
def speak(self):
return f'{[Link]} makes a sound'

class Dog(Animal): # Single Inheritance


def speak(self): # Method Overriding
return f'{[Link]} says Woof!'

class GuideDog(Dog): # Multilevel Inheritance


def guide(self):
return f'{[Link]} is guiding someone'

d = Dog('Rex')
print([Link]()) # Rex says Woof!
print([Link]) # Rex (inherited from Animal)

5.2 super() — Invoking Parent Class

Definition: super() is a built-in function used to call methods from a parent class inside
a child class. It is most commonly used in the __init__ method to ensure that the parent
class is properly initialized before adding child-specific initialization. It follows the
Method Resolution Order (MRO) to determine which parent to call.

class Person:
def __init__(self, name, age):
[Link] = name
[Link] = age

class Student(Person):
def __init__(self, name, age, roll):
super().__init__(name, age) # call Person's __init__
[Link] = roll

def display(self):
print(f'{[Link]}, Age: {[Link]}, Roll: {[Link]}')

s = Student('Alice', 20, 101)


[Link]() # Alice, Age: 20, Roll: 101

5.3 Method Resolution Order (MRO)

Definition: Method Resolution Order (MRO) defines the order in which Python searches
for a method or attribute in a class hierarchy, especially in multiple inheritance. Python
uses the C3 Linearization algorithm to determine the MRO. The order is: the class itself,
then left-to-right through parent classes, then object (the root class).

class A:
def greet(self): return 'Hello from A'
class B(A):
def greet(self): return 'Hello from B'
class C(A):
def greet(self): return 'Hello from C'
class D(B, C): pass

print(D.__mro__)
# (<class D>, <class B>, <class C>, <class A>, <class object>)
print(D().greet()) # Hello from B (B comes before C in MRO)

5.4 Method Overriding and Polymorphism

Definition: Polymorphism means 'many forms' — the ability of different objects to


respond to the same method call in different ways. Method overriding is when a child
class provides its own implementation of a method that is already defined in its parent
class. This is one of the key ways polymorphism is achieved in OOP.

Types of Polymorphism in Python:


➤ Duck Typing — if an object has the required method, it can be used regardless of its type
➤ Method Overriding — child class redefines parent class method
➤ Operator Overloading — operators behave differently based on object type

class Shape:
def area(self): return 0

class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14 * self.r * self.r

class Rectangle(Shape):
def __init__(self, l, w): self.l, self.w = l, w
def area(self): return self.l * self.w

# Polymorphic behaviour — same method name, different results


shapes = [Circle(5), Rectangle(4, 6)]
for s in shapes:
print([Link]()) # 78.5 | 24

5.5 Encapsulation

Definition: Encapsulation is the OOP principle of bundling the data (attributes) and the
methods (functions) that operate on that data into a single unit (class), and restricting
direct access to some of the object's internal components. This is called data hiding.
Encapsulation ensures that an object's internal state can only be changed in controlled
ways, preventing accidental or unauthorized modification.

Encapsulation achieves two things:


➤ Data Bundling — related data and functions are kept together in one class
➤ Data Hiding — internal data is protected using access modifiers (private/protected)

Key benefits of Encapsulation:


➤ Data Protection — internal data cannot be changed directly from outside the class
➤ Controlled Access — getter and setter methods regulate how data is read or modified
➤ Modularity — each class is self-contained and independent of others
➤ Maintainability — internal implementation can change without affecting outside code
➤ Security — sensitive data (like a bank balance) is hidden from direct access

class BankAccount:
def __init__(self, owner, balance):
[Link] = owner # public
self.__balance = balance # private (name mangled)
def deposit(self, amount):
if amount > 0:
self.__balance += amount
print(f'Deposited {amount}. New balance: {self.__balance}')

def withdraw(self, amount):


if amount <= self.__balance:
self.__balance -= amount
else:
print('Insufficient funds')

def get_balance(self): # getter method


return self.__balance

acc = BankAccount('Alice', 1000)


[Link](500) # Deposited 500. New balance: 1500
print(acc.get_balance()) # 1500
# print(acc.__balance) # AttributeError — private!

Note: Name mangling: Python renames __balance to _BankAccount__balance


internally. Accessing acc._BankAccount__balance still works but is strongly discouraged
— it defeats the purpose of encapsulation.

Access Modifiers in Python:


Modifier Syntax Convention Accessible From
Public name No prefix Anywhere — inside class,
subclass, outside
Protected _name Single underscore Inside class and subclasses
prefix (by convention only)
Private __name Double underscore Only inside the class
prefix (enforced by name
mangling)

5.6 Abstraction

Definition: Abstraction is the OOP principle of hiding the internal implementation details
and exposing only the essential features of an object to the outside world. It allows the
user to interact with an object through a simplified interface without needing to
understand how it works internally. In Python, abstraction is implemented using Abstract
Base Classes (ABC) from the abc module.

Real-world analogy: When you drive a car, you use the steering wheel and pedals (the interface)
without knowing how the engine works internally (the implementation). The complexity is abstracted
away from you.

Key benefits of Abstraction:


➤ Simplicity — users interact with a clean, simple interface
➤ Security — internal logic is hidden from outside access
➤ Maintainability — implementation can change without affecting user code
➤ Forces structure — all subclasses must implement abstract methods

from abc import ABC, abstractmethod

class Shape(ABC): # Abstract Base Class


@abstractmethod
def area(self): pass # must be overridden in subclass

@abstractmethod
def perimeter(self): pass

def describe(self): # concrete method (has implementation)


print(f'Area: {[Link]()}, Perimeter: {[Link]()}')

class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return round(3.14 * self.r**2, 2)
def perimeter(self): return round(2 * 3.14 * self.r, 2)

# s = Shape() # TypeError — cannot instantiate abstract class


c = Circle(5)
[Link]() # Area: 78.5, Perimeter: 31.4

Note: Difference: Encapsulation hides DATA using access modifiers. Abstraction hides
IMPLEMENTATION using abstract classes and interfaces.

5.7 Iterators and Generators

Definition: An iterator is an object that implements two methods: __iter__() (returns the
iterator object itself) and __next__() (returns the next value, raises StopIteration when
exhausted). A generator is a simpler way to create an iterator using a function with the
yield keyword. Each call to yield pauses the function and saves its state until the next
value is requested.

Iterator Example
class CountDown:
def __init__(self, start):
[Link] = start

def __iter__(self): return self

def __next__(self):
if [Link] <= 0:
raise StopIteration
[Link] -= 1
return [Link] + 1

for n in CountDown(3): print(n) # 3 2 1


Generator Example
def count_up(n):
for i in range(1, n+1):
yield i # pauses here and returns i each time

gen = count_up(3)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# print(next(gen)) # StopIteration

Note: Generators are memory-efficient — they generate values one at a time (lazily)
instead of storing the entire sequence in memory like a list.

5.8 Exception Handling

Definition: Exception handling is the mechanism to detect, respond to, and recover
from runtime errors (exceptions) so that the program does not crash unexpectedly.
Python uses try-except-else-finally blocks. An exception is an event that disrupts the
normal flow of the program's execution.

try:
num = int(input('Enter number: '))
result = 100 / num
except ValueError:
print('Not a valid number!')
except ZeroDivisionError:
print('Cannot divide by zero!')
except Exception as e:
print(f'Unexpected error: {e}') # catch-all
else:
print(f'Result: {result}') # runs only if no exception
finally:
print('Done.') # always runs

Raising and Custom Exceptions


raise ValueError('Value must be positive') # manually raise

class AgeError(Exception): # custom exception


def __init__(self, msg): super().__init__(msg)

def set_age(age):
if age < 0: raise AgeError('Age cannot be negative')
return age

5.9 File I/O

Definition: File I/O (Input/Output) allows a Python program to read data from files and
write data to files on the disk. Python provides built-in functions like open(), read(),
write(), and close() for file operations. The with statement is used as a context manager
to ensure files are automatically closed after use, even if an error occurs.

Mode Description
'r' Read only — error if file not found (default)
'w' Write — creates new or overwrites existing file
'a' Append — adds content at end without erasing
'r+' Read and write — file must exist
'rb'/'wb' Read/Write in binary mode (for images, etc.)

# Writing to a file
with open('[Link]', 'w') as f:
[Link]('Hello, World!\n')
[Link]('Python File I/O\n')

# Reading from a file


with open('[Link]', 'r') as f:
content = [Link]() # entire file as string
# lines = [Link]() # list of lines
# line = [Link]() # one line at a time

# Appending to a file
with open('[Link]', 'a') as f:
[Link]('Appended line\n')

Note: Always use the with statement for file handling. It automatically calls [Link]() even
if an exception occurs during file operations.

5.10 Regular Expressions

Definition: A regular expression (regex) is a sequence of characters that defines a


search pattern. It is used for pattern matching, searching, extracting, and replacing text.
Python provides the re module to work with regular expressions. Regex is extremely
useful for validating inputs like email addresses, phone numbers, and parsing text data.

Pattern Meaning Matches


\d Any digit (0–9) '3', '7'
\D Any non-digit 'a', '#'
\w Word character (letter/digit/_) 'a', 'Z', '9', '_'
\W Non-word character '@', ' '
\s Whitespace (space, tab, newline) ' ', '\t'
. Any character except newline 'a', '3', '#'
* 0 or more of previous 'aaa', '' (empty)
+ 1 or more of previous 'aaa', 'a'
? 0 or 1 of previous (optional) 'colour' or 'color'
{n} Exactly n occurrences \d{3} → '123'
^ Start of string ^Hello
$ End of string end$
[abc] Any one of a, b, or c 'a', 'b', 'c'
[^abc] Any character except a, b, c 'd', 'x'
(abc) Capture group groups in match

Function Description
[Link](pat, str) Match at beginning of string only
[Link](pat, str) Search anywhere in the string — returns first match
[Link](pat, str) Return list of all non-overlapping matches
[Link](pat, repl, str) Replace matches with repl string
[Link](pat, str) Split string by pattern
[Link](pat) Compile pattern into regex object for reuse

import re

# Find all digits in a string


[Link](r'\d+', 'I have 3 cats and 10 dogs') # ['3', '10']

# Validate email address


pattern = r'^[\w.-]+@[\w.-]+\.\w{2,4}$'
[Link](pattern, 'alice@[Link]') # Match object (valid)
[Link](pattern, 'notanemail') # None (invalid)

# Replace all digits with 'X'


[Link](r'\d', 'X', 'Call 9876543210') # 'Call XXXXXXXXXX'

5.11 Metaclass

Definition: A metaclass is a class whose instances are classes. In Python, everything is


an object — even classes. The default metaclass of all classes is type. Metaclasses
allow you to customize class creation: you can add or modify attributes, enforce naming
conventions, register classes automatically, or restrict instantiation. They are an
advanced feature rarely needed in everyday programming.

# type is the default metaclass


print(type(int)) # <class 'type'>
print(type(list)) # <class 'type'>
# Custom metaclass
class UpperMeta(type):
def __new__(cls, name, bases, dct):
# Convert all method names to uppercase
upper_dct = {[Link](): v for k, v in [Link]()}
return super().__new__(cls, name, bases, upper_dct)

class MyClass(metaclass=UpperMeta):
def hello(self): return 'hi'

5.12 Unit Testing

Definition: Unit testing is the practice of testing individual units (functions or methods)
of a program in isolation to ensure they work correctly. Python's unittest module
provides a framework for writing and running tests. Each test case inherits from
[Link] and contains test methods that start with test_. Tests are run
automatically, and pass/fail results are reported.

import unittest

def add(a, b): return a + b


def divide(a, b): return a / b

class TestMathFunctions([Link]):

def test_add_positive(self):
[Link](add(2, 3), 5)

def test_add_negative(self):
[Link](add(-1, -1), -2)

def test_divide(self):
[Link](divide(10, 3), 3.333, places=2)

def test_divide_by_zero(self):
with [Link](ZeroDivisionError):
divide(5, 0)

if __name__ == '__main__':
[Link]()

Assert Method Description


assertEqual(a, b) Check a == b
assertNotEqual(a, b) Check a != b
assertTrue(x) Check x is True
assertFalse(x) Check x is False
assertIsNone(x) Check x is None
assertRaises(Error, fn) Check fn raises given exception
assertAlmostEqual(a, b) Check a ≈ b (for floats)

Quick Revision — All Key Concepts

Concept Definition in One Line


Class Blueprint or template for creating objects
Object Instance of a class with its own data
__init__ Constructor — initializes object when created
self Reference to the current object instance
Class Variable Shared by all objects of the class
Instance Variable Unique to each individual object
Encapsulation Bundle data + methods; hide internal details
Abstraction Show interface, hide implementation (ABC)
Inheritance Child class acquires parent class properties
Polymorphism Same method name, different behaviour per class
super() Call parent class constructor/method from child
MRO Order Python searches for methods — C3 linearization
@classmethod Method that receives class (cls), not instance
@staticmethod Method with no self or cls — utility function
Dunder method Special method like __init__, __str__, __add__
Operator overloading Redefine operators (+,-,*) for custom objects
Aggregation HAS-A weak — child exists independently
Composition HAS-A strong — child cannot exist without parent
Iterator Object with __iter__ and __next__ methods
Generator Function using yield to produce values lazily
Lambda Anonymous single-line function using lambda keyword
List comprehension [expr for item in iterable if cond]
map() Apply function to every element of iterable
filter() Keep elements that satisfy a condition
reduce() Accumulate iterable to a single value
try-except Handle runtime exceptions gracefully
finally Block that always runs after try/except
with open() Context manager — auto-closes file
[Link]() Return all regex matches as a list
Metaclass Class of a class — type is the default metaclass
unittest Python module for writing automated test cases
ABC Abstract Base Class — cannot be instantiated directly
@abstractmethod Forces subclass to override this method
LEGB Rule Local → Enclosing → Global → Built-in scope lookup
Recursion Function calling itself with a base case to stop
Module A .py file containing reusable code

You might also like