0% found this document useful (0 votes)
8 views103 pages

OOPS Python

The document provides a comprehensive overview of Object-Oriented Programming (OOP) in Python, detailing its principles, advantages, and practical applications. It covers key concepts such as classes, objects, encapsulation, inheritance, polymorphism, and abstraction, along with comparisons to procedural programming. Additionally, it includes practical examples and mini-projects to illustrate OOP concepts in action.

Uploaded by

harshithasana027
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)
8 views103 pages

OOPS Python

The document provides a comprehensive overview of Object-Oriented Programming (OOP) in Python, detailing its principles, advantages, and practical applications. It covers key concepts such as classes, objects, encapsulation, inheritance, polymorphism, and abstraction, along with comparisons to procedural programming. Additionally, it includes practical examples and mini-projects to illustrate OOP concepts in action.

Uploaded by

harshithasana027
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

Index

Contents

1.1 What is Object-Oriented Programming?

Chapter 1: Introduction to Object-Oriented Programming


1.1 What is Object-Oriented Programming?
1.2 Why Python Uses OOP
1.2 Why Python Uses Object-Oriented Programming
Practical Examples of Object-Oriented Programming in Python
1.3 OOP vs Procedural Programming
1.3 Object-Oriented Programming vs Procedural Programming
Procedural Programming Example
Object-Oriented Programming Example
2.1 What is a Class?
2.2 What is an Object?
2.2 What is an Object?
2.3 Creating Classes and Objects
The init Method in Python
Memory Allocation: new vs init
The self Keyword in Python
2.4 Attributes and Methods
Class Variables vs Instance Variables
2.5 Common Mistakes
2.5 Common Mistakes in Python OOP
Chapter 3: Constructors and Destructors
3.1 init Method
3.2 Default vs Parameterized Constructor
3.3 del Method

1
2 CONTENTS

Chapter 4: Encapsulation
4.1 What is Encapsulation?
Chapter 4: Encapsulation
4.1 What is Encapsulation?
4.2 Access Modifiers in Python
4.2 Access Modifiers in Python
4.3 Getter and Setter Methods
4.3 Getter and Setter Methods
4.4 Real-World Example
4.4 Real-World Example: Bank Account System

Chapter 5: Inheritance
5.1 What is Inheritance?
5.2 Types of Inheritance
5.2 Types of Inheritance
5.3 Method Overriding
5.3 Method Overriding
5.4 super() Function
5.4 super() Function
5.5 Diamond Problem and MRO
5.5 Diamond Problem (Important!)
5.6 Real-World Example of Inheritance

Chapter 6: Polymorphism
6.1 What is Polymorphism?
Chapter 6: Polymorphism
6.1 What is Polymorphism?
6.2 Method Overloading
6.2 Method Overloading (Python Reality)
6.3 Method Overriding
6.3 Method Overriding
6.4 Operator Overloading
6.4 Operator Overloading
6.5 Real-World Example of Polymorphism

Chapter 7: Abstraction
7.1 What is Abstraction?
Chapter 7: Abstraction
CONTENTS 3

7.1 What is Abstraction?


7.2 Abstract Base Classes
7.2 Abstract Base Classes (ABC)
7.3 Interfaces in Python
7.3 Interfaces in Python
7.4 Real-World Example of Abstraction
7.5 Real-World Example: Payment Gateway System

Chapter 8: Advanced OOP Concepts


8.1 Static Methods
8.1 Static Methods
8.2 Class Methods
8.2 Class Methods
8.3 Instance vs Class vs Static Methods
8.3 Difference: Instance vs Class vs Static Methods
8.4 Composition vs Inheritance
8.4 Composition vs Inheritance
8.5 Object Cloning
8.5 Object Cloning (copy module)

Chapter 9: Special (Magic) Methods


9.1 str vs repr
9.1 s trv sr epr
9.2 Comparison Methods
9.2 e q
9.3 Callable Objects
9.3 l t
9.5 Real-World Example: Employee Class with Magic Meth-
ods

Chapter 10: OOP Design Principles (SOLID)


10.1 Single Responsibility Principle
10.1 Single Responsibility Principle (SRP)
10.2 Open/Closed Principle
10.2 Open/Closed Principle (OCP)
10.3 Liskov Substitution Principle
10.3 Liskov Substitution Principle (LSP)
10.4 Interface Segregation Principle
4 CONTENTS

10.4 Interface Segregation Principle (ISP)


10.5 Dependency Inversion Principle
10.5 Dependency Inversion Principle (DIP)

Chapter 13: Mini Projects


13.1 Student Management System
13.2 Bank Account System
13.3 Library Management System
13.3 Library Management System
1. Encapsulation
2. Inheritance
3. Polymorphism
4. Abstraction

Appendix
Chapter 1: Introduction to
Object-Oriented Programming

1.1 What is Object-Oriented Programming?


-Ready Definition:
Object-Oriented Programming, or OOP, is a programming paradigm where
we design software by modeling real-world entities as objects. Each object rep-
resents a real-world thing and contains both data (attributes) and behavior
(methods). The main goal of OOP is to make code more modular, reusable,
and easier to maintain.

Explanation (How to say it in an ):


Instead of writing a long sequence of instructions, OOP encourages us to
think in terms of objects that interact with each other. Each object is created
from a class, which acts as a blueprint. This approach closely matches how
we think about real-world problems, making complex systems easier to design
and manage.

Real-World Analogy:
For example, consider a Car. A car has properties like color, model, and
speed, and it has behaviors like start(), stop(), and accelerate(). In OOP, we
create a Car class as a blueprint, and each individual car we create from
it is an object. This allows us to manage multiple cars efficiently without
rewriting the same code again and again.

Why OOP is Important:


• It improves code reusability using classes and inheritance.
• It enhances maintainability by organizing code into logical units.

5
6 CONTENTS

• It supports scalability, which is important for large applications.

• It closely represents real-world systems, making development intu-


itive.

One-Line Answer (Very Important):


Object-Oriented Programming is a way of writing programs by modeling
real-world entities as objects that combine data and behavior, making the code
modular, reusable, and easy to maintain.

1.2 Why Python Uses Object-Oriented Pro-


gramming
-Ready Definition:
Python uses Object-Oriented Programming because it helps developers
write clean, reusable, and scalable code while keeping the language simple
and readable. OOP allows Python programs to handle complex real-world
problems in a structured and organized way.

Explanation :
Python is designed to be a high-level and easy-to-read language, and OOP
fits naturally into this philosophy. By using classes and objects, Python al-
lows developers to break large problems into smaller, manageable components.
This makes development faster, debugging easier, and long-term maintenance
more efficient.

Key Reasons Why Python Uses OOP:

• Reusability: Classes allow code to be reused across multiple pro-


grams.

• Readability: Python’s syntax makes OOP concepts easy to under-


stand and apply.

• Scalability: OOP helps Python applications grow from small scripts


to large systems.
CONTENTS 7

• Maintainability: Changes in one part of the code do not break the


entire system.

• Real-World Modeling: Python can easily represent real-world enti-


ties like users, files, and services.

Practical Insight:
Most popular Python frameworks such as Django, Flask, and even ma-
chine learning libraries heavily rely on OOP concepts. Understanding OOP
is essential to write professional Python code.

Practical Examples of Object-Oriented Pro-


gramming in Python
Perspective:
Object-Oriented Programming is not just a concept in Python; it is deeply
integrated into the language itself. Most things we use daily in Python are
objects created from classes and expose behavior through methods.

1. File Handling
When we open a file in Python, it returns a file object. This object contains
both data and behavior such as reading and writing.

f = open("[Link]", "r")
[Link]()
[Link]()

explanation: File handling in Python follows OOP because each file is


treated as an object with its own methods.

2. Built-in Data Structures


Python collections like lists, tuples, sets, and dictionaries are objects created
from built-in classes.
8 CONTENTS

numbers = [1, 2, 3]
[Link](4)

explanation: This shows that even basic data structures in Python are
object-oriented.

3. String Operations
Strings in Python are immutable objects and provide methods to manipulate
text.

name = "python"
[Link]()

explanation: String manipulation in Python is implemented using OOP


concepts.

4. Exception Handling
Exceptions in Python are classes, and raised errors are objects of those
classes.

try:
int("abc")
except ValueError as e:
print(e)

explanation: Python uses inheritance in its exception hierarchy, making


error handling object-oriented.

5. User-Defined Classes
Developers use OOP to model real-world entities using custom classes.

class BankAccount:
def __init__(self, balance):
[Link] = balance
CONTENTS 9

def deposit(self, amount):


[Link] += amount

explanation: This is a clear example of encapsulation where data and


behavior are bundled together.

6. Inheritance in Python
Python supports inheritance to reuse and extend existing functionality.

class MyError(Exception):
pass

explanation: Custom exceptions demonstrate real-world inheritance in


Python.

7. Iterators and Generators


Iteration in Python follows a strict object-oriented protocol.

numbers = iter([1, 2, 3])


next(numbers)

explanation: Iterators use special methods like iter and next .

8. Context Managers
The with statement in Python is implemented using special OOP methods.

with open("[Link]") as f:
data = [Link]()

explanation: Context managers rely on magic methods to manage re-


sources safely.
10 CONTENTS

9. Frameworks and Libraries


Most Python frameworks are designed using object-oriented principles.

• Django: Models, Views, Forms

• Flask: Request and Response objects

• Machine Learning: Model classes

10. Machine Learning Models


Machine learning libraries use OOP to maintain consistency and reusability.

[Link](X, y)
[Link](X)

explanation: Each model is an object with a fixed interface, which


simplifies experimentation and deployment.

Strong Conclusion:
In Python, everything is an object. From simple data types to complex
frameworks and machine learning models, OOP is the foundation that makes
Python powerful and flexible.

One-Line Answer:
Python uses OOP to help developers write readable, reusable, and scalable
code by modeling real-world problems in an organized way.

1.3 Object-Oriented Programming vs Proce-


dural Programming
Definition:
Procedural programming is a programming approach where a program is
written as a sequence of functions or steps, whereas Object-Oriented Program-
ming organizes the program around objects that combine data and behavior.

Explanation:
CONTENTS 11

In procedural programming, the main focus is on functions and the flow of


execution. Data is usually shared globally and passed between functions. In
contrast, OOP focuses on objects, where data and the functions that operate
on that data are bundled together. This makes OOP more suitable for large
and complex applications.

Procedural Programming Example


In procedural programming, data and functions are separate. The program is
written as a sequence of steps that operate on shared data.

# Procedural approach

balance = 0

def deposit(amount):
global balance
balance += amount

def withdraw(amount):
global balance
if amount <= balance:
balance -= amount
else:
print("Insufficient balance")

deposit(1000)
withdraw(300)
print(balance)

Key Observations:

• Data is stored in global variables.

• Functions operate on external data.

• Scaling this code becomes difficult as complexity increases.


12 CONTENTS

Object-Oriented Programming Example


In Object-Oriented Programming, data and behavior are bundled together in-
side objects. Each object manages its own state.

# Object-Oriented approach

class BankAccount:
def __init__(self, balance=0):
[Link] = balance

def deposit(self, amount):


[Link] += amount

def withdraw(self, amount):


if amount <= [Link]:
[Link] -= amount
else:
print("Insufficient balance")

account = BankAccount(0)
[Link](1000)
[Link](300)
print([Link])

Key Observations:
• Data and methods are encapsulated inside the class.

• No global variables are required.

• The code is easier to extend and maintain.

Comparison Summary:

• Procedural programming focuses on functions and execution flow.

• OOP focuses on objects that manage both data and behavior.

• For small scripts, procedural style is sufficient.


CONTENTS 13

• For real-world applications, OOP provides better structure and scala-


bility.

Key Differences:

Procedural Program- Object-Oriented Pro-


ming gramming
Focuses on functions and Focuses on objects and
procedures classes
Data is often shared glob- Data is encapsulated inside
ally objects
Difficult to maintain large Easier to maintain and scale
codebases
Less reusable Highly reusable using
classes and inheritance
Examples: C, early Python Examples: Python, Java,
scripts C++

Python Perspective (Important):


Python supports both procedural and object-oriented programming. Small
scripts can be written procedurally, but for large applications, Python strongly
encourages the use of OOP to improve structure, readability, and maintain-
ability.

One-Line Answer:
Procedural programming focuses on functions and execution flow, while
Object-Oriented Programming focuses on objects that combine data and be-
havior, making OOP more scalable and maintainable.
14 CONTENTS
Chapter 2: Classes and Objects

2.1 What is a Class?


Definition:
A class in Python is a blueprint or template used to create objects. It
defines the structure of an object by specifying the attributes it will have and
the methods it can perform, but it does not hold actual data by itself.

Explanation:
When we define a class, we are not creating real data; we are defining
a design. Actual data is created only when we create objects from the class.
This separation between definition and usage helps in writing organized and
reusable code.

Key Characteristics of a Class:

• A class is a user-defined data type.

• It groups related data and behavior together.

• Memory is not allocated for instance variables until an object is created.

• Multiple objects can be created from the same class.

Simple Python Example:

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

15
16 CONTENTS

def display(self):
print([Link], [Link])

Explanation of the Example:

• Student is a class that defines what a student object looks like.


• name and marks are attributes.
• display() is a method that defines behavior.
• No actual student data exists until an object is created.

2.2 What is an Object?


Definition:
An object in Python is an instance of a class. It represents a real entity
that holds actual data in memory and can access the methods defined in its
class.

Explanation:
A class only defines the structure, but an object is the actual implemen-
tation of that structure. When an object is created, memory is allocated for
its instance variables, and the object can interact with other objects using
methods.

Key Characteristics of an Object:

• An object is created from a class.


• It occupies memory.
• It contains actual values for attributes.
• It can access and execute the methods of its class.

Simple Python Example:


CONTENTS 17

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

def display(self):
print([Link], [Link])

student1 = Student("Alice", 90)


student2 = Student("Bob", 85)

Explanation of the Example:

• student1 and student2 are objects of the Student class.

• Each object has its own copy of instance variables.

• Both objects use the same class methods.

The init Method in Python


Definition:
The init method in Python is a special method that is automatically
called when an object of a class is created. Its main purpose is to initialize
the object’s instance variables with initial values.

Explaination :
When an object is created, Python first allocates memory for the object
and then calls the init method to initialize its state. This allows each
object to start with its own data while sharing the same class definition.

Key Points About init :

• It is a constructor-like method in Python.

• It is automatically invoked during object creation.

• It initializes instance variables using the self keyword.


18 CONTENTS

• It does not return anything explicitly.

Basic Syntax:

class ClassName:
def __init__(self, parameters):
[Link] = parameters

Example with Explanation:

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

• [Link] and [Link] are instance variables.

• Each object of Employee gets its own copy of these variables.

• The values are passed at the time of object creation.

Object Creation Flow:

1. Memory is allocated for the object.

2. The object reference is assigned to self.

3. The init method is called automatically.

4. Instance variables are initialized.

Default vs Parameterized init :


If init takes no parameters (except self), it is called a default con-
structor. If it takes parameters, it is called a parameterized constructor.
CONTENTS 19

class A:
def __init__(self):
print("Default constructor")

class B:
def __init__(self, x):
self.x = x

Common Traps:

• Forgetting to include self as the first parameter.

• Assuming init creates the object (it does not).

• Returning a value from init .

Important Clarification (Very Common Question):


init does not create the object. The object is created by the new
method, and init only initializes it.

One-Line Answer:
The init method is automatically called after object creation to initial-
ize instance variables and define the initial state of an object.

Memory Allocation: new vs init


Explanation:
In Python, object creation and object initialization are two separate steps.
The new method is responsible for creating the object and allocating mem-
ory, while the init method initializes the object with data.

Step-by-Step Object Creation Flow (Very Important):

1. Python calls the new method.

2. new allocates memory for the object and returns the object reference.

3. The returned object is passed as self to the init method.


20 CONTENTS

4. init initializes the object’s instance variables.

Key Difference Between new and init :

new init
Responsible for object creation
Responsible for object initial-
ization
Allocates memory for the ob- Does not allocate memory
ject
Static method by nature Instance method
Must return the object Must not return anything
Called before init Called after new

Simple Code Demonstration:

class Demo:
def __new__(cls):
print("__new__ called")
return super().__new__(cls)

def __init__(self):
print("__init__ called")

obj = Demo()

Output:

__new__ called
__init__ called

Explanation of the Code:

• new is called first and creates the object.

• The object reference returned by new is passed to init as self.

• init initializes the object state.


CONTENTS 21

Important Insight:
If new does not return an object, init will not be called.

When Do We Override new ?

• When creating immutable objects (e.g., int, str, tuple)

• When implementing singleton patterns

• When controlling object creation logic

Common Traps:

• Saying init creates the object (incorrect)

• Returning a value from init

• Forgetting to return the object from new

One-Line Answer (Gold):


In Python, new creates the object and allocates memory, while init
initializes the object after it is created.

The self Keyword in Python


Definition:
The self keyword represents the current object instance. It is used inside
a class to access the instance variables and methods of that specific object.

Explanation:
When a method is called using an object, Python automatically passes
the object reference as the first argument to the method. By convention, this
reference is named self. It allows each object to maintain and access its
own data independently.

Why self is Required:

• It differentiates instance variables from local variables.


22 CONTENTS

• It allows methods to modify the object’s own data.

• It ensures that each object operates on its own memory.

Memory Perspective (Very Important):


Each object created from a class occupies a separate memory location in
the heap. The self variable holds the reference to that memory location.
Using [Link] ensures that we are accessing the correct object’s data
in memory.

Code Example Demonstrating self:

class Demo:
def __init__(self, value):
[Link] = value

def show(self):
print([Link])

obj1 = Demo(10)
obj2 = Demo(20)

[Link]()
[Link]()

Explanation of the Example:

• obj1 and obj2 are two different objects.

• Python internally converts [Link]() to [Link](obj1).

• self refers to obj1 in the first call and obj2 in the second.

• Each object accesses its own value stored in memory.

What Happens If self is Not Used:


CONTENTS 23

class Test:
def set_value(value):
value = 10

This code will not work as expected because the method does not receive
the object reference.

Important Clarification:
self is not a keyword in Python. It is a naming convention, but omitting
it will cause runtime errors.

Common Traps:

• Saying self is optional (incorrect)

• Confusing self with class variables

• Forgetting self in method definitions

One-Line Answer (Gold):


The self keyword refers to the current object and allows instance methods
to access and modify the object’s data stored in memory.

Class Variables vs Instance Variables


Definition (Class Variable):
A class variable is a variable that is shared among all objects of a class.
It is defined inside the class but outside any method and is stored only once
in memory.

Definition (Instance Variable):


An instance variable is a variable that is unique to each object. It is de-
fined using the self keyword and stored separately for every object in mem-
ory.

Memory Perspective (Very Important):

• Class variables are stored in the class namespace.


24 CONTENTS

• Instance variables are stored in the object’s memory space.

• All objects reference the same class variable unless overridden.

Code Example:

class Employee:
company = "Google" # Class variable

def __init__(self, name):


[Link] = name # Instance variable

emp1 = Employee("Alice")
emp2 = Employee("Bob")

Accessing Variables:

print([Link])
print([Link])

print([Link])
print([Link])
print([Link])

Explanation:

• name is different for each object.

• company is shared across all objects.

• Class variables can be accessed using the class name.

Modifying Class Variable vs Instance Variable:

[Link] = "Microsoft"

Important Observation:
CONTENTS 25

• This does not modify the class variable.

• It creates a new instance variable named company inside emp1.

Correct Way to Modify Class Variable:

[Link] = "Microsoft"

Key Differences ( Table):

Feature Class Variable Instance Variable


Defined using Class name self keyword
Memory Single shared copy Separate copy per object
Accessed by Class / Object Object only
Purpose Common data Object-specific data

Common Traps:

• Confusing instance variable override with class variable modification

• Accessing class variables using self and assuming they are instance
variables

2.5 Common Mistakes in Python OOP


Explanation:
While working with Python OOP, many candidates make subtle mistakes
that can cause runtime errors or logical bugs. Understanding these common
pitfalls helps in writing robust, maintainable code and impressing ers.

1. Forgetting to Include self in Method Definitions:

class Test:
def set_value(value): # Incorrect
[Link] = value
26 CONTENTS

Error: Python automatically passes the object reference, so self must be


the first parameter.
2. Confusing Class Variables with Instance Variables:

class Employee:
company = "Google"

emp1 = Employee()
[Link] = "Microsoft" # Does not change class variable

Tip: Assign to [Link] to modify class variable. Otherwise,


a new instance variable is created.
3. Returning a Value from init :

class Demo:
def __init__(self):
return 5 # Incorrect

init should never return a value. Object creation is handled by new .


4. Using Mutable Default Arguments:

class Demo:
def __init__(self, items=[]): # Problematic
[Link] = items

All objects share the same list. Use None and assign inside init instead.
5. Overwriting Built-in Names:

class List:
pass

Avoid naming classes or variables after Python built-ins (e.g., list, str,
int) — can break code.
6. Accessing Private Variables Directly:
CONTENTS 27

class Demo:
def __init__(self):
self.__value = 10

obj = Demo()
print(obj.__value) # Error
Use getter/setter methods or name mangling ( ClassName variable) in-
stead.
7. Misunderstanding Inheritance and Method Resolution Or-
der (MRO):
class A:
def show(self):
print("A")

class B(A):
pass

obj = B()
[Link]() # Calls [Link]()
Important: Python follows MRO — ers may ask about multiple inheri-
tance.
8. Forgetting Object Creation Before Calling Methods:
class Demo:
def show(self):
print("Hello")

[Link]() # Error without object


Either create an object or make the method a @staticmethod.

One-Line Answer (Gold):


Common mistakes in Python OOP include forgetting self, confusing class
and instance variables, returning from init , using mutable defaults, over-
writing built-ins, directly accessing private variables, and misusing inheri-
tance.
28 CONTENTS

Chapter 3: Constructors and Destructors


3.1 init Method
Definition:
The init method in Python is a special method that is automatically
called when an object of a class is created. Its main purpose is to initialize
the object’s instance variables with initial values.
How it Works (Memory Perspective):
1. Python allocates memory for the new object ( new method is called
internally).
2. The object reference is passed to self.
3. init initializes instance variables.

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

emp1 = Employee("Alice", 1000)


emp2 = Employee("Bob", 1200)
Explanation:
• Each object has its own memory for name and salary.
• init allows objects to start with meaningful initial data.

Common Traps:
• Returning a value from init (incorrect)
• Forgetting self as the first parameter
• Assuming init creates the object (it only initializes)
CONTENTS 29

3.2 Default vs Parameterized Constructor


Definitions:

• Default Constructor: A constructor that takes no parameters (ex-


cept self) and initializes objects with default values.

• Parameterized Constructor: A constructor that takes parameters


to initialize objects with user-defined values.

Examples:
Default Constructor:

class Demo:
def __init__(self):
[Link] = 0

obj = Demo()
print([Link]) # Output: 0

Parameterized Constructor:

class Demo:
def __init__(self, x):
[Link] = x

obj = Demo(10)
print([Link]) # Output: 10

Important Points:

• Python does not support multiple constructors directly (no method over-
loading).

• Use default parameters or *args, **kwargs to simulate multiple con-


structors.
30 CONTENTS

3.3 del Method (Destructor)


Definition:
The del method in Python is a special method called when an object
is about to be destroyed. It allows cleanup of resources before the object is
removed from memory.
Key Points:

• del is automatically called during garbage collection.

• Explicitly using del is rarely needed in Python.

• Python uses reference counting; objects with zero references are garbage
collected.

Python Example:

class Demo:
def __init__(self, name):
[Link] = name
print(f"{[Link]} created")

def __del__(self):
print(f"{[Link]} destroyed")

obj = Demo("Test")
del obj # Explicitly calling destructor

Output:

Test created
Test destroyed

Tips:

• Don’t rely on del for important program logic.

• Understand garbage collection and reference counting.


CONTENTS 31

• Destructors can be called explicitly using del, but Python usually han-
dles it automatically.

One-Line Summary for s:

• init : Initializes an object with initial data.

• Default vs Parameterized Constructor: Default has no parameters, Pa-


rameterized accepts arguments.

• del : Called before an object is destroyed; mainly for cleanup, rarely


used explicitly.
32 CONTENTS
Chapter 4: Encapsulation

Chapter 4: Encapsulation
4.1 What is Encapsulation?
Definition:
Encapsulation is the OOP concept of restricting direct access to an ob-
ject’s internal data and methods, while providing controlled access through
public interfaces. It helps in protecting object integrity and hiding internal
implementation details.
Key Points for s:

• Encapsulation hides the internal state of objects (data hiding).

• It exposes only what is necessary through public methods or properties.

• Helps maintain code robustness and prevents accidental modifications.

• Python uses naming conventions (p rotected, private)toindicateaccesslevels.

Python Example:

class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private variable

def deposit(self, amount):


if amount > 0:
self.__balance += amount

33
34 CONTENTS

def withdraw(self, amount):


if 0 < amount <= self.__balance:
self.__balance -= amount

def get_balance(self):
return self.__balance

# Using the class


acc = BankAccount(1000)
[Link](500)
[Link](200)
print(acc.get_balance()) # Output: 1300

# Direct access will fail


# print(acc.__balance) # AttributeError

Explanation:

• balance is a private variable; direct access outside the class is re-


stricted.
• Public methods deposit, withdraw, and get balance provide con-
trolled access.
• This ensures the account balance cannot be set arbitrarily.

Common Traps:

• Thinking Python enforces true private variables (Python uses name


mangling).
• Accessing private variables directly instead of using getters/setters.
• Overusing public variables, leading to loss of control over object state.

One-Line Answer (Gold):


Encapsulation in Python hides the internal state of an object and exposes
controlled access via public methods, ensuring data integrity and robust code.
CONTENTS 35

4.2 Access Modifiers in Python


Definition:
Access modifiers in Python define the visibility of class attributes and
methods. They determine whether members can be accessed from outside the
class, from subclasses, or only within the class.
Types of Access Modifiers:

1. Public:

• Accessible from anywhere.


• No special prefix is needed.

2. Protected:

• Intended for internal use or subclasses.


• Prefix with a single underscore ( variable).
• Still accessible from outside, but by convention should not be.

3. Private:

• Intended to be inaccessible from outside the class.


• Prefix with double underscores ( variable).
• Python performs name mangling to prevent accidental access.

Python Code Example:

class Demo:
public_var = "I am public"
_protected_var = "I am protected"
__private_var = "I am private"

obj = Demo()

# Accessing public variable


print(obj.public_var) # Output: I am public
36 CONTENTS

# Accessing protected variable (possible but discouraged)


print(obj._protected_var) # Output: I am protected

# Accessing private variable (will fail)


# print(obj.__private_var) # AttributeError

# Access private variable using name mangling


print(obj._Demo__private_var) # Output: I am private

Explanation:
• public var is freely accessible.
• protected var can be accessed, but it signals that it’s intended for
internal/subclass use.
• private var is name-mangled to Demo private var to prevent ac-
cidental external access.

Important Points:
• Python does not enforce strict access control; it’s based on **conven-
tions**.
• Private members are primarily to avoid accidental modifications.
• Protected members are meant for inheritance and internal use.

Common Traps:
• Assuming double underscore makes a variable completely inaccessible
(it can still be accessed via name mangling).
• Forgetting that single underscore is only a **convention**, not enforce-
ment.
• Trying to access private variables directly and getting AttributeError.

One-Line Answer (Gold):


Python access modifiers use public (anywhere), protected ( internal/subclass),
and private ( internal) to control visibility, but enforcement is by convention
rather than strict rules.
CONTENTS 37

4.3 Getter and Setter Methods


Definition:
Getter and Setter methods are used to access and modify private attributes
of a class. They allow controlled access to the internal state of objects while
maintaining encapsulation.
Why Needed:

• Direct access to private variables is restricted.

• Getters provide a safe way to read private data.

• Setters allow validation or controlled modification of private data.

• Helps maintain object integrity and prevent invalid states.

Pythonic Way using @property Decorator:

class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private variable

# Getter method
@property
def balance(self):
return self.__balance

# Setter method
@[Link]
def balance(self, amount):
if amount >= 0:
self.__balance = amount
else:
print("Invalid balance!")

# Using the class


acc = BankAccount(1000)

# Access using getter


38 CONTENTS

print([Link]) # Output: 1000

# Modify using setter


[Link] = 1500
print([Link]) # Output: 1500

# Invalid modification
[Link] = -500 # Output: Invalid balance!

Explanation:

• @property makes balance() a getter method, allowing access like an


attribute.

• @[Link] defines a setter with validation logic.

• This approach is more Pythonic than separate get balance() and


set balance() methods.

• Encapsulation is maintained; direct access to balance is still restricted.

Common Traps:

• Forgetting to use @property and trying to call getter like a method.

• Allowing direct assignment to private variables, bypassing validation.

• Confusing class variables with instance variables in getters/setters.

One-Line Answer (Gold):


Getter and Setter methods provide controlled access and modification to
private attributes, maintaining encapsulation and allowing validation or com-
putation on access.
CONTENTS 39

4.4 Real-World Example: Bank Account System


Problem Statement:
We want to model a Bank Account system in Python that protects the
account balance and allows controlled deposits and withdrawals. This demon-
strates encapsulation, access modifiers, and getter/setter usage.
Python Code Implementation:

class BankAccount:
def __init__(self, account_number, balance=0):
self.account_number = account_number # Public
self.__balance = balance # Private
self._transactions = [] # Protected

# Getter for balance


@property
def balance(self):
return self.__balance

# Setter for balance with validation


@[Link]
def balance(self, amount):
if amount >= 0:
self.__balance = amount
else:
print("Invalid balance!")

# Deposit method
def deposit(self, amount):
if amount > 0:
self.__balance += amount
self._transactions.append(f"Deposited: {amount}")
else:
print("Deposit must be positive!")

# Withdraw method
def withdraw(self, amount):
if 0 < amount <= self.__balance:
40 CONTENTS

self.__balance -= amount
self._transactions.append(f"Withdrew: {amount}")
else:
print("Insufficient funds or invalid withdrawal!")

# Show transaction history


def show_transactions(self):
for t in self._transactions:
print(t)

# Using the BankAccount class


acc1 = BankAccount("AC123", 1000)
[Link](500)
[Link](200)
print(f"Account Balance: {[Link]}") # Output: 1300
acc1.show_transactions()

Explanation:

• accountn umberispublic; accessibleanywhere.

• balance is private; controlled access via balance getter


and setter.

• transactions is protected; accessible in subclasses but not


intended for public modification.

• Deposit and withdrawal methods ensure that balance updates


are valid and consistent.

• Transaction history is maintained using the protected attribute,


demonstrating encapsulation in practice.

Discussion Points:

• Illustrates encapsulation by hiding the account balance.

• Demonstrates validation in setters to prevent invalid data.


CONTENTS 41

• Shows public, protected, and private usage in a real-world scenario.

• Highlights how Pythonic @property simplifies getter/setter logic.

• Can be extended with inheritance (e.g., SavingsAccount, CurrentAc-


count) without breaking encapsulation.

One-Line Answer (Gold):


This Bank Account system encapsulates sensitive data, exposes controlled
interfaces, validates operations, and demonstrates public, protected, and pri-
vate access in a real-world scenario.
42 CONTENTS
Chapter 5: Inheritance

Definition:
Inheritance is an OOP concept where a class (child/subclass) can acquire
properties and behaviors (attributes and methods) from another class (par-
ent/superclass). It promotes code reuse, scalability, and logical hierarchy in
programs.
Key Points for s:

• The child class inherits variables and methods of the parent class.

• Allows adding or modifying functionality in the child class.

• Supports hierarchical modeling of real-world relationships (IS-A rela-


tionship).

• Helps reduce code duplication.

Python Syntax Example:

# Parent class
class Vehicle:
def __init__(self, brand):
[Link] = brand

def start_engine(self):
print(f"{[Link]} engine started")

# Child class inheriting from Vehicle


class Car(Vehicle):
def play_music(self):

43
44 CONTENTS

print("Playing music in the car")

# Using the classes


my_car = Car("Toyota")
my_car.start_engine() # Inherited from Vehicle
my_car.play_music() # Defined in Car

Explanation:
• Car inherits from Vehicle using parentheses syntax: class Car(Vehicle).
• Child object my car can access both parent (start engine) and child
(play music) methods.
• This demonstrates **code reuse** and **logical IS-A hierarchy** (Car
IS-A Vehicle).

Common Traps:
• Forgetting to pass parameters to parent init in child class (if parent
has a parameterized constructor).
• Confusing “HAS-A” relationship (composition) with inheritance (IS-
A).
• Modifying parent variables in child without understanding scope and
memory.

One-Line Answer (Gold):


Inheritance allows a child class to acquire properties and behaviors from
a parent class, enabling code reuse and hierarchical modeling.

5.2 Types of Inheritance


Definition:
Inheritance in Python can take multiple forms depending on how classes
are related. Understanding the types of inheritance helps in designing proper
class hierarchies.
Types of Inheritance:
CONTENTS 45

1. Single Inheritance:

• A child class inherits from only one parent class.

class Parent:
def greet(self):
print("Hello from Parent")

class Child(Parent):
pass

c = Child()
[Link]() # Output: Hello from Parent

2. Multiple Inheritance:

• A child class inherits from more than one parent class.


• Python uses Method Resolution Order (MRO) to determine which
parent method to call.

class Father:
def skills(self):
print("Father skills")

class Mother:
def skills(self):
print("Mother skills")

class Child(Father, Mother):


pass

c = Child()
[Link]() # Output: Father skills (MRO)

3. Multilevel Inheritance:

• A chain of inheritance where a child inherits from a parent, and


another child inherits from this child.
46 CONTENTS

class Grandparent:
def info(self):
print("Grandparent")

class Parent(Grandparent):
pass

class Child(Parent):
pass

c = Child()
[Link]() # Output: Grandparent

4. Hierarchical Inheritance:

• Multiple child classes inherit from a single parent class.

class Parent:
def greet(self):
print("Hello from Parent")

class Child1(Parent):
pass

class Child2(Parent):
pass

c1 = Child1()
c2 = Child2()
[Link]() # Output: Hello from Parent
[Link]() # Output: Hello from Parent

5. Hybrid Inheritance:

• Combination of two or more types of inheritance (single, multiple,


multilevel, hierarchical).
• Can be complex; MRO resolves method conflicts.
CONTENTS 47

Explanation:

• Single inheritance is simplest and most common.


• Multiple inheritance allows combining features from multiple parents.
• Multilevel inheritance creates a chain of classes.
• Hierarchical inheritance models multiple children from one parent.
• Hybrid inheritance is a realistic scenario in complex systems but re-
quires understanding of MRO.

Common Traps:

• Confusing multiple and multilevel inheritance.


• Not understanding MRO in multiple inheritance.
• Assuming parent variables are automatically independent across multi-
ple children (beware of mutable class variables).

One-Line Answer (Gold):


Python inheritance types include Single, Multiple, Multilevel, Hierarchi-
cal, and Hybrid, each modeling different class relationships and enabling code
reuse.

5.3 Method Overriding


Definition:
Method overriding occurs when a child class provides a new implementa-
tion of a method that is already defined in its parent class. It allows a subclass
to modify or extend the behavior of inherited methods.
Key Points for s:

• The method in the child class must have the same name and parameters
as the parent method.
• Python determines which method to call based on the object’s class (run-
time polymorphism).
48 CONTENTS

• Method overriding is different from overloading (Python does not sup-


port true method overloading).

Python Example:
class Vehicle:
def start_engine(self):
print("Starting generic vehicle engine")

class Car(Vehicle):
def start_engine(self): # Overriding parent method
print("Starting car engine with keyless ignition")

v = Vehicle()
v.start_engine() # Output: Starting generic vehicle engine

c = Car()
c.start_engine() # Output: Starting car engine with keyless ignition

Explanation:
• Car overrides the start engine method of Vehicle.
• When calling [Link] engine(), Python uses the child class method
instead of the parent.
• This demonstrates **runtime polymorphism** in Python.

Using super() with Overriding:


class Car(Vehicle):
def start_engine(self):
super().start_engine() # Call parent method
print("Car engine ready to drive")

c = Car()
c.start_engine()
# Output:
# Starting generic vehicle engine
# Car engine ready to drive
CONTENTS 49

Explanation:

• super() allows the child method to extend parent functionality instead


of completely replacing it.

Common Traps:

• Changing method signature in child class (will not override properly).

• Forgetting self in method definition.

• Confusing method overriding with method overloading (Python does not


support true overloading).

One-Line Answer (Gold):


Method overriding allows a subclass to provide a new implementation of
a parent method, enabling runtime polymorphism and customized behavior.

5.4 super() Function


Definition:
The super() function in Python returns a temporary object of the super-
class that allows access to its methods and properties. It is commonly used
in inheritance to call parent class methods inside child class methods.
Key Points for s:

• super() avoids explicitly naming the parent class, making code more
maintainable.

• It is useful for **extending** parent functionality rather than com-


pletely overriding it.

• Plays a crucial role in **multiple inheritance** and **MRO (Method


Resolution Order)**.

• Ensures correct parent method is called in complex hierarchies.

Basic Single Inheritance Example:


50 CONTENTS

class Vehicle:
def start_engine(self):
print("Starting generic vehicle engine")

class Car(Vehicle):
def start_engine(self):
super().start_engine() # Call parent method
print("Car engine ready to drive")

c = Car()
c.start_engine()
# Output:
# Starting generic vehicle engine
# Car engine ready to drive

Explanation:

• super().start engine() calls the Vehicle method from Car.

• Allows Car to add behavior without duplicating parent code.

Parameterized Constructor Example:

class Parent:
def __init__(self, name):
[Link] = name
print(f"Parent initialized: {[Link]}")

class Child(Parent):
def __init__(self, name, age):
super().__init__(name) # Call parent’s __init__
[Link] = age
print(f"Child initialized: {[Link]}")

c = Child("Alice", 10)
# Output:
# Parent initialized: Alice
# Child initialized: 10
CONTENTS 51

Explanation:
• super() ensures the parent constructor runs, initializing parent at-
tributes.
• Avoids manually repeating parent initialization logic.

Multiple Inheritance Example (MRO Demo):


class A:
def show(self):
print("A show")

class B(A):
def show(self):
print("B show")
super().show()

class C(A):
def show(self):
print("C show")
super().show()

class D(B, C):


def show(self):
print("D show")
super().show()

d = D()
[Link]()
# Output:
# D show
# B show
# C show
# A show
Explanation:
• Python follows **Method Resolution Order (MRO)**: D → B → C →
A.
52 CONTENTS

• super() dynamically finds the next class in MRO, not just the imme-
diate parent.

• This allows complex inheritance hierarchies to work correctly without


explicitly naming parent classes.

Common Traps:

• Using parent class name instead of super() in multiple inheritance


(can break MRO).

• Forgetting to pass parameters to super().i nit( ) whenparentconstructorexpectsthem.

• Assuming super() only calls the immediate parent (MRO matters!).

One-Line Answer (Gold):


super() returns a temporary object of the parent class to call its meth-
ods or constructors, enabling proper method chaining, extension, and correct
behavior in multiple inheritance.

5.5 Diamond Problem (Important!)


Definition:
The Diamond Problem occurs in multiple inheritance when a class inherits
from two classes that both inherit from the same parent class. This can
create ambiguity in method resolution. Python solves this using the Method
Resolution Order (MRO).
Illustration of the Diamond Problem:

A
/ \
B C
\ /
D

Explanation:

• Classes B and C inherit from A.


CONTENTS 53

• Class D inherits from both B and C.

• If D calls a method defined in A, Python must decide whether to use


B’s version, C’s version, or A’s version directly.

Python Example:

class A:
def show(self):
print("A show")

class B(A):
def show(self):
print("B show")
super().show()

class C(A):
def show(self):
print("C show")
super().show()

class D(B, C):


def show(self):
print("D show")
super().show()

d = D()
[Link]()
# Output:
# D show
# B show
# C show
# A show

Explanation:

• Python uses **C3 Linearization** to determine the **MRO**.

• For class D, MRO is: D → B → C → A → object.


54 CONTENTS

• super() automatically follows this order, resolving the diamond prob-


lem without ambiguity.

Check MRO in Python:

print(D.__mro__)
# Output: (<class ’__main__.D’>, <class ’__main__.B’>,
# <class ’__main__.C’>, <class ’__main__.A’>, <class ’object’>)

Common Traps:

• Assuming Python does not solve diamond problem automatically (it


uses MRO).

• Calling parent class methods directly in multiple inheritance instead of


using super().

• Not understanding that MRO order affects method calls.

One-Line Answer (Gold):


The Diamond Problem arises in multiple inheritance when ambiguity ex-
ists in parent method resolution; Python resolves it using the Method Reso-
lution Order (MRO) and super().

5.6 Real-World Example of Inheritance


Problem Statement:
We want to model an employee system where different types of employ-
ees (Developer, Manager) inherit common attributes and behaviors from a
general Employee class.
Python Code Implementation:

# Parent class
class Employee:
def __init__(self, name, salary):
[Link] = name
[Link] = salary
CONTENTS 55

def work(self):
print(f"{[Link]} is working.")

def get_salary(self):
print(f"{[Link]} earns {[Link]} per month.")

# Child class 1
class Developer(Employee):
def __init__(self, name, salary, language):
super().__init__(name, salary)
[Link] = language

def work(self):
print(f"{[Link]} is writing code in {[Link]}.")

# Child class 2
class Manager(Employee):
def __init__(self, name, salary, team_size):
super().__init__(name, salary)
self.team_size = team_size

def work(self):
print(f"{[Link]} is managing a team of {self.team_size} people.")

# Using the classes


dev = Developer("Alice", 70000, "Python")
mgr = Manager("Bob", 90000, 5)

[Link]() # Output: Alice is writing code in Python.


dev.get_salary() # Output: Alice earns 70000 per month.

[Link]() # Output: Bob is managing a team of 5 people.


mgr.get_salary() # Output: Bob earns 90000 per month.

Explanation:

• Employee is the parent class containing common attributes and meth-


56 CONTENTS

ods.

• Developer and Manager inherit from Employee and override the work()
method to define their specific behavior.

• super() ensures parent class attributes are initialized correctly.

• This models the real-world “IS-A” relationship: Developer IS-A Em-


ployee, Manager IS-A Employee.

Answer (If Asked for Real-World Example):


A common real-world example is an Employee management system: a
general Employee class contains common attributes like name and salary,
while specific roles like Developer and Manager inherit from it, overriding
methods like work() to provide role-specific behavior. This demonstrates code
reuse, logical hierarchy, and polymorphism.
Chapter 6: Polymorphism

Chapter 6: Polymorphism
6.1 What is Polymorphism?
Definition:
Polymorphism is an OOP concept where the same method or operator can
behave differently depending on the object or context it is applied to. It allows
a single interface to represent different underlying forms.
Key Points for s:

• Supports code flexibility and reusability.

• Enables the same method name to perform different tasks in different


classes (method overriding).

• Can be achieved through inheritance, duck typing, or operator overload-


ing in Python.

Python Example (Method Overriding):

class Bird:
def speak(self):
print("Some generic bird sound")

class Parrot(Bird):
def speak(self):
print("Parrot says: Squawk!")

57
58 CONTENTS

class Crow(Bird):
def speak(self):
print("Crow says: Caw!")

birds = [Parrot(), Crow()]


for bird in birds:
[Link]()

# Output:
# Parrot says: Squawk!
# Crow says: Caw!

Explanation:

• Both Parrot and Crow have the same method speak(), but output
differs.

• Python determines the correct method at runtime (**runtime polymor-


phism**).

Python Example (Duck Typing):

class Dog:
def speak(self):
print("Woof!")

class Cat:
def speak(self):
print("Meow!")

def make_animal_speak(animal):
[Link]() # Works for any object with speak() method

make_animal_speak(Dog()) # Output: Woof!


make_animal_speak(Cat()) # Output: Meow!

Explanation:

• Python’s duck typing allows polymorphic behavior without inheritance.


CONTENTS 59

• Any object with the required method can be used interchangeably.

Common Traps:

• Confusing polymorphism with overloading (Python does not support


true method overloading).

• Assuming polymorphism is only achieved through inheritance (duck typ-


ing is also polymorphism).

• Forgetting dynamic behavior occurs at runtime.

One-Line Answer (Gold):


Polymorphism allows the same method or operator to behave differently
depending on the object or context, enabling flexible and reusable code.

6.2 Method Overloading (Python Reality)


Definition:
Method overloading is the ability to define multiple methods in a class
with the same name but different parameters.
Note: Python does not support true method overloading as in Java
or C++; the last defined method with the same name overrides the previous
ones.
Python Example Demonstrating Overloading Issue:

class Calculator:
def add(self, a, b):
return a + b

# Attempting to overload
def add(self, a, b, c=0):
return a + b + c

calc = Calculator()
print([Link](5, 10)) # Output: 15
print([Link](5, 10, 20)) # Output: 35
60 CONTENTS

Explanation:

• Python does not truly overload methods by argument number or type.

• The last method defined with the same name overwrites the previous
one.

• Default parameters can simulate method overloading.

Pythonic Way to Simulate Method Overloading:

class Calculator:
def add(self, *args):
return sum(args)

calc = Calculator()
print([Link](5, 10)) # Output: 15
print([Link](5, 10, 20, 2)) # Output: 37

Explanation:

• Using *args allows a single method to accept variable number of argu-


ments.

• This is the Pythonic way to implement “overloading-like” behavior.

Common Traps:

• Thinking Python supports true method overloading (it does not).

• Defining multiple methods with the same name will overwrite previous
definitions.

• Confusing default arguments or *args with actual overloading.

One-Line Answer (Gold):


Python does not support true method overloading; the last defined method
with the same name overwrites previous ones, but default arguments or *args
can simulate overloading.
CONTENTS 61

6.3 Method Overriding


Definition:
Method overriding in polymorphism occurs when a subclass provides its
own implementation of a method already defined in the parent class. It allows
objects of different classes to respond differently to the same method call,
enabling runtime polymorphism.
Python Example:

class Shape:
def area(self):
print("Calculating area of generic shape")

class Square(Shape):
def __init__(self, side):
[Link] = side

def area(self): # Overriding parent method


print(f"Area of square: {[Link] ** 2}")

class Circle(Shape):
def __init__(self, radius):
[Link] = radius

def area(self): # Overriding parent method


import math
print(f"Area of circle: {[Link] * [Link] ** 2}")

shapes = [Square(5), Circle(3)]


for shape in shapes:
[Link]()

# Output:
# Area of square: 25
# Area of circle: 28.274333882308138

Explanation:
62 CONTENTS

• Both Square and Circle override the area() method of Shape.

• The same method call [Link]() produces different behavior de-


pending on the object type.

• Demonstrates **runtime polymorphism**: objects respond differently


to the same interface.

Common Traps:

• Forgetting to define the method in the child class correctly (parameters


must match).

• Confusing method overriding with overloading (Python only supports


overriding).

• Assuming polymorphic behavior without inheritance (duck typing is


polymorphism too).

One-Line Answer (Gold):


Method overriding allows subclasses to provide different implementations
for the same method, enabling polymorphic behavior at runtime.

6.4 Operator Overloading


Definition:
Operator overloading is a feature of Python’s OOP that allows us to define
or change the behavior of standard operators (+, -, , etc.) for user-defined
objects. It provides polymorphic behavior for operators.
Key Points for s:

• Special methods (also called magic or dunder methods) define operator


behavior.

• Common dunder methods: add , sub , mul , str , len , etc.

• Operator overloading improves readability and usability of custom classes.

Python Example: Overloading + Operator


CONTENTS 63

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

# Overload the + operator


def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)

# String representation
def __str__(self):
return f"Point({self.x}, {self.y})"

p1 = Point(2, 3)
p2 = Point(5, 7)
p3 = p1 + p2 # Calls p1.__add__(p2)
print(p3) # Output: Point(7, 10)

Explanation:

• + operator is overloaded using add .

• Allows intuitive addition of custom objects.

• str provides readable output when printing objects.

Python Example: Overloading len()

class Team:
def __init__(self, members):
[Link] = members

def __len__(self):
return len([Link])

team = Team(["Alice", "Bob", "Charlie"])


print(len(team)) # Output: 3

Explanation:
64 CONTENTS

• len allows using the built-in len() function with custom objects.

• Provides consistent behavior with built-in Python types.

Common Traps:

• Forgetting to return a new object in arithmetic operator overloading.

• Overloading operators without understanding the meaning (confuses


code readability).

• Confusing operator overloading with method overloading.

One-Line Answer (Gold):


Operator overloading allows defining or changing the behavior of standard
operators for user-defined objects using special (dunder) methods, enabling
polymorphic behavior.

6.5 Real-World Example of Polymorphism


Problem Statement:
We want to model a payment system where different payment methods
(CreditCard, PayPal, Bitcoin) respond differently to a common action pay().
This demonstrates polymorphism: the same method name behaves differently
depending on the object.
Python Code Implementation:

# Parent class
class PaymentMethod:
def pay(self, amount):
raise NotImplementedError("This method should be overridden in child c

# Child class 1
class CreditCard(PaymentMethod):
def pay(self, amount):
print(f"Paying {amount} using Credit Card.")

# Child class 2
CONTENTS 65

class PayPal(PaymentMethod):
def pay(self, amount):
print(f"Paying {amount} using PayPal.")

# Child class 3
class Bitcoin(PaymentMethod):
def pay(self, amount):
print(f"Paying {amount} using Bitcoin.")

# Using polymorphism
payments = [CreditCard(), PayPal(), Bitcoin()]

for method in payments:


[Link](100)

Output:

Paying 100 using Credit Card.


Paying 100 using PayPal.
Paying 100 using Bitcoin.

Explanation:

• All classes share a common interface: the method pay().

• Each class provides its own implementation of pay().

• The loop demonstrates **runtime polymorphism**: the same method


call produces different behavior depending on the object type.

• This is also an example of **interface-based polymorphism**.

Answer (If Asked for Real-World Example):


A payment system is a real-world example of polymorphism: different
payment methods like CreditCard, PayPal, and Bitcoin implement a common
method pay(), but each responds differently when called. This allows writing
flexible, reusable code while treating all objects uniformly.
66 CONTENTS
Chapter 7: Abstraction

Chapter 7: Abstraction
7.1 What is Abstraction?
Definition:
Abstraction is an OOP concept that hides internal implementation details
and shows only the necessary functionality to the user. It allows focusing on
what an object does rather than how it does it.
Key Points for s:

• Abstraction hides complexity and reduces code dependency.

• Achieved in Python using abstract classes (abc module) and abstract


methods (@abstractmethod).

• Enables defining a common interface for different child classes.

Python Example using Abstract Class:

from abc import ABC, abstractmethod

# Abstract class
class Vehicle(ABC):

@abstractmethod
def start_engine(self):
pass # Implementation hidden

67
68 CONTENTS

# Child class 1
class Car(Vehicle):
def start_engine(self):
print("Starting car engine")

# Child class 2
class Bike(Vehicle):
def start_engine(self):
print("Starting bike engine")

vehicles = [Car(), Bike()]

for v in vehicles:
v.start_engine()
# Output:
# Starting car engine
# Starting bike engine

Explanation:

• Vehicle is abstract; we cannot create objects directly.

• start engine() is abstract — child classes must implement it.

• Shows **what the object does** (start engine) without exposing **how**
it does it internally.

Common Traps:

• Trying to instantiate an abstract class directly.

• Forgetting to implement all abstract methods in the child class.

• Confusing abstraction with encapsulation (abstraction hides implemen-


tation, encapsulation hides data).

One-Line Answer (Gold):


Abstraction hides the internal implementation details of an object and
exposes only the essential functionality to the user.
CONTENTS 69

7.2 Abstract Base Classes (ABC)


Definition:
An Abstract Base Class (ABC) is a class that cannot be instantiated and
is used to define a common interface for its subclasses using abstract methods.
Python Example:

from abc import ABC, abstractmethod

class Shape(ABC):

@abstractmethod
def area(self):
pass

class Circle(Shape):
def __init__(self, radius):
[Link] = radius

def area(self):
import math
return [Link] * [Link] ** 2

class Square(Shape):
def __init__(self, side):
[Link] = side

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

shapes = [Circle(3), Square(5)]


for s in shapes:
print([Link]())

Explanation:

• Shape is an ABC; cannot create Shape() directly.


• area() is abstract — all child classes must implement it.
70 CONTENTS

• Ensures a consistent interface across different shapes.

Common Traps:

• Forgetting to import ABC and abstractmethod.

• Not implementing all abstract methods in subclasses.

• Confusing abstract classes with interfaces (Python does not have formal
interfaces, ABC acts as one).

One-Line Answer (Gold):


An ABC defines a common interface with abstract methods that child
classes must implement, ensuring consistent behavior.

7.3 Interfaces in Python


Definition:
In Python, an interface can be implemented using abstract classes con-
taining only abstract methods. It defines a contract that subclasses must
follow.
Python Example: Payment Interface

from abc import ABC, abstractmethod

class PaymentInterface(ABC):

@abstractmethod
def pay(self, amount):
pass

class CreditCard(PaymentInterface):
def pay(self, amount):
print(f"Paying {amount} via Credit Card")

class PayPal(PaymentInterface):
def pay(self, amount):
CONTENTS 71

print(f"Paying {amount} via PayPal")

payments = [CreditCard(), PayPal()]


for p in payments:
[Link](50)

Explanation:

• PaymentInterface acts like an interface, defining a method pay().

• All subclasses must implement pay().

• This allows polymorphic behavior: same interface, different implemen-


tations.

Common Traps:

• Forgetting to implement all methods of the interface (abstract class).

• Confusing Python ABCs with Java interfaces (Python’s interface is im-


plemented via ABC).

• Trying to instantiate the interface directly.

One-Line Answer (Gold):


In Python, interfaces are implemented using abstract classes with only
abstract methods, defining a contract that subclasses must follow.

7.4 Real-World Example of Abstraction


Problem Statement:
A real-world example of abstraction is an ATM machine. The user inter-
acts with the ATM to withdraw or deposit money without knowing the internal
processes (network calls, database access, security checks).
Python Code Implementation:
72 CONTENTS

from abc import ABC, abstractmethod

# Abstract class (ATM Interface)


class ATM(ABC):

@abstractmethod
def withdraw(self, amount):
pass

@abstractmethod
def deposit(self, amount):
pass

# Concrete class
class BankATM(ATM):
def __init__(self, balance):
[Link] = balance

def withdraw(self, amount):


if amount > [Link]:
print("Insufficient balance!")
else:
[Link] -= amount
print(f"Withdrew {amount}, remaining balance {[Link]}")

def deposit(self, amount):


[Link] += amount
print(f"Deposited {amount}, new balance {[Link]}")

# Using the ATM


atm = BankATM(1000)
[Link](200) # Output: Withdrew 200, remaining balance 800
[Link](500) # Output: Deposited 500, new balance 1300

Explanation:

• ATM defines the abstract interface — methods withdraw() and deposit()


must be implemented.
CONTENTS 73

• BankATM provides concrete implementations of these methods.

• Users only see the functionality (withdraw or deposit) without wor-


rying about internal operations.

• Demonstrates **abstraction in real-world systems**.

Answer (If Asked for Real-World Example):


A typical real-world example of abstraction is an ATM. The user interacts
with simple methods like withdraw() and deposit(), but the internal processes
(security, database access, network calls) are hidden. This shows how ab-
straction allows focusing on “what” the object does rather than “how”.

7.5 Real-World Example: Payment Gateway System


Problem Statement:
In a payment system, users can pay via different methods (CreditCard,
PayPal, Bitcoin). The system should hide the internal payment processing
(abstraction) while allowing multiple payment methods to implement a com-
mon interface (polymorphism).
Python Code Implementation:

from abc import ABC, abstractmethod

# Abstract class (Abstraction)


class PaymentProcessor(ABC):

@abstractmethod
def pay(self, amount):
pass

# Concrete class 1 (Polymorphism)


class CreditCardProcessor(PaymentProcessor):
def pay(self, amount):
print(f"Processing credit card payment of {amount}")

# Concrete class 2 (Polymorphism)


class PayPalProcessor(PaymentProcessor):
74 CONTENTS

def pay(self, amount):


print(f"Processing PayPal payment of {amount}")

# Concrete class 3 (Polymorphism)


class BitcoinProcessor(PaymentProcessor):
def pay(self, amount):
print(f"Processing Bitcoin payment of {amount}")

# Using the payment system


payments = [CreditCardProcessor(), PayPalProcessor(), BitcoinProcessor()]

for p in payments:
[Link](100)
Output:
Processing credit card payment of 100
Processing PayPal payment of 100
Processing Bitcoin payment of 100

Explanation:
• PaymentProcessor is an abstract class — users cannot see internal
processing. This is **abstraction**.
• Each payment class implements pay() differently — this is **polymor-
phism**.
• Looping through the list of payment objects demonstrates **runtime
polymorphism**: same interface, different behavior.
• Users interact with a simple interface (pay(amount)) without worrying
about credit card API calls, PayPal API, or Bitcoin transactions.

Answer (If Asked for Real-World Example):


A payment gateway system demonstrates both abstraction and polymor-
phism. The abstract class PaymentProcessor hides internal logic, while Cred-
itCard, PayPal, and Bitcoin classes implement the same interface differently.
This allows flexible, reusable code and shows a real-world use of OOP prin-
ciples.
Chapter 8: Advanced OOP
Concepts

8.1 Static Methods


Definition:
A static method belongs to a class rather than an instance and does
not access or modify instance-specific attributes. It is defined using the
@staticmethod decorator.
Python Example:

class MathUtils:

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

# Calling static method without creating an object


print([Link](5, 10)) # Output: 15

Explanation:

• No need to create an object of the class.

• Useful for utility functions related to the class but not dependent on
instance attributes.

Tip: Static methods are commonly asked in s to test understanding of


**instance vs class-level behavior**.

75
76 CONTENTS

8.2 Class Methods


Definition:
A class method belongs to the class and can access class attributes. It
is defined using the @classmethod decorator and receives cls as the first
argument.
Python Example:

class Employee:
company = "ABC Corp"

@classmethod
def show_company(cls):
print(f"Company: {[Link]}")

# Calling class method without object


Employee.show_company() # Output: Company: ABC Corp

Explanation:

• Can access or modify class-level attributes.

• Useful for factory methods or operations affecting the class as a whole.

8.3 Difference: Instance vs Class vs Static Methods


Method Type Access Decorator Use Case
Instance Method Instance attributes None Normal behavior, d
Class Method Class attributes @classmethod Factory methods, c
Static Method Neither instance nor class attributes @staticmethod Utility functions, in
Python Example:

class Demo:
class_var = 10

def instance_method(self):
print("Instance method")
CONTENTS 77

@classmethod
def class_method(cls):
print(f"Class method, class_var = {cls.class_var}")

@staticmethod
def static_method():
print("Static method")

d = Demo()
d.instance_method() # Requires object
Demo.class_method() # Can call without object
Demo.static_method() # Can call without object

8.4 Composition vs Inheritance


Definition:

• Inheritance (IS-A): One class derives from another. Example: De-


veloper IS-A Employee.

• Composition (HAS-A): One class contains objects of another class.


Example: Car HAS-A Engine.

Python Example: Composition vs Inheritance

# Inheritance example
class Employee:
def work(self):
print("Employee working")

class Developer(Employee):
def code(self):
print("Developer coding")

# Composition example
class Engine:
78 CONTENTS

def start(self):
print("Engine started")

class Car:
def __init__(self):
[Link] = Engine() # Car HAS-A Engine

def start_car(self):
[Link]()

Explanation:

• Use inheritance for IS-A relationships.

• Use composition for HAS-A relationships.

• Composition is preferred in complex systems to reduce tight coupling.

8.5 Object Cloning (copy module)


Definition:
Cloning creates copies of objects. Python provides shallow and deep copy
using the copy module.
Python Example:

import copy

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

emp1 = Employee("Alice", ["Python", "ML"])

# Shallow copy
emp2 = [Link](emp1)
[Link]("DL")
CONTENTS 79

print([Link]) # Output: [’Python’, ’ML’, ’DL’] -> shared reference

# Deep copy
emp3 = [Link](emp1)
[Link]("NLP")
print([Link]) # Output: [’Python’, ’ML’, ’DL’] -> unchanged

Explanation:

• [Link]() copies object but nested objects are shared (shallow).

• [Link]() copies object and nested objects (deep).

• Important in questions involving **mutable objects** and **object cloning**.


80 CONTENTS
Chapter 9: Special (Magic)
Methods

9.1 str vs repr


Definition:

• str : Returns a readable string representation of the object (user-


friendly).
• repr : Returns an unambiguous string representation, ideally usable
to recreate the object (developer-friendly).

Python Example:

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

def __str__(self):
return f"Employee: {[Link]}, Salary: {[Link]}"

def __repr__(self):
return f"Employee(’{[Link]}’, {[Link]})"

emp = Employee("Alice", 70000)


print(str(emp)) # Output: Employee: Alice, Salary: 70000
print(repr(emp)) # Output: Employee(’Alice’, 70000)

Explanation:

81
82 CONTENTS

• Use str for human-readable output.


• Use repr for debugging and developer purposes.

9.2 eq
Definition:
The eq method allows defining custom equality comparison for objects
using the == operator.
Python Example:

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

def __eq__(self, other):


return [Link] == [Link] and [Link] == [Link]

emp1 = Employee("Alice", 70000)


emp2 = Employee("Alice", 70000)
emp3 = Employee("Bob", 80000)

print(emp1 == emp2) # True


print(emp1 == emp3) # False

Explanation:

• Enables object comparison using ‘==‘.


• Must define explicitly, otherwise Python compares memory addresses.

9.3 lt
Definition:
The lt method allows defining custom behavior for the ‘¡‘ operator.
Python Example:
CONTENTS 83

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

def __lt__(self, other):


return [Link] < [Link]

emp1 = Employee("Alice", 70000)


emp2 = Employee("Bob", 80000)

print(emp1 < emp2) # True

Explanation:

• Enables sorting and comparison of objects using standard operators.

• Useful for lists of objects, e.g., sorting employees by salary.

9.5 Real-World Example: Employee Class with Magic


Methods
Problem Statement:
We want an Employee class that supports human-readable display, debug-
ging, equality checks, comparison by salary, and callable behavior for greet-
ings.
Python Code Implementation:

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

# Human-readable string
def __str__(self):
return f"Employee: {[Link]}, Salary: {[Link]}"

# Developer-friendly representation
84 CONTENTS

def __repr__(self):
return f"Employee(’{[Link]}’, {[Link]})"

# Equality comparison
def __eq__(self, other):
return [Link] == [Link] and [Link] == [Link]

# Less-than comparison for sorting


def __lt__(self, other):
return [Link] < [Link]

# Callable behavior
def __call__(self, greeting):
print(f"{greeting}, {[Link]}!")

# Creating employee objects


emp1 = Employee("Alice", 70000)
emp2 = Employee("Bob", 80000)
emp3 = Employee("Alice", 70000)

# Using magic methods


print(str(emp1)) # Employee: Alice, Salary: 70000
print(repr(emp2)) # Employee(’Bob’, 80000)
print(emp1 == emp3) # True
print(emp1 < emp2) # True

emp1("Hello") # Hello, Alice!


emp2("Good morning") # Good morning, Bob!

# Sorting employees by salary


employees = [emp2, emp1]
[Link]()
print(employees) # Uses __repr__ to display sorted list

Explanation:

• str → User-friendly display for print statements.


CONTENTS 85

• repr → Developer-friendly, used when printing a list of objects.

• eq → Compare employees based on name and salary.

• lt → Enables sorting by salary.

• call → Allows greeting an employee as if the object is a function.

Answer (If Asked for Real-World Example):


In a payroll or HR system, an Employee class can implement multiple
dunder methods: str and repr for readable output and debugging, eq
to compare employees, lt for sorting by salary, and call for greeting
functionality. This demonstrates mastery of Python’s magic methods and
real-world OOP design.
86 CONTENTS
Chapter 10: OOP Design
Principles (SOLID)

10.1 Single Responsibility Principle (SRP)


Definition:
A class should have only one reason to change, i.e., it should have a single
responsibility.
Python Example:

# Bad example: One class does multiple things


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

def calculate_salary(self):
return [Link] * 12

def save_to_db(self):
print(f"Saving {[Link]} to database")

# Good example: Separate responsibilities


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

def calculate_salary(self):

87
88 CONTENTS

return [Link] * 12

class EmployeeDB:
def save(self, employee):
print(f"Saving {[Link]} to database")

Explanation:

• SRP improves **maintainability**.

• Employee handles salary logic; EmployeeDB handles database opera-


tions.

• Each class has a **single reason to change**.

10.2 Open/Closed Principle (OCP)


Definition:
Software entities (classes, modules, functions) should be open for exten-
sion but closed for modification.
Python Example:

# Base class
class Shape:
def area(self):
pass

# Extend without modifying


class Rectangle(Shape):
def __init__(self, width, height):
[Link] = width
[Link] = height

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

class Circle(Shape):
CONTENTS 89

def __init__(self, radius):


[Link] = radius

def area(self):
import math
return [Link] * [Link] ** 2

shapes = [Rectangle(5,6), Circle(3)]


for s in shapes:
print([Link]())

Explanation:

• Add new shapes without modifying existing code.

• Achieves **extensibility** while maintaining **stability**.

10.3 Liskov Substitution Principle (LSP)


Definition:
Objects of a superclass should be replaceable with objects of a subclass
without affecting correctness.
Python Example:

class Bird:
def fly(self):
print("Bird can fly")

class Sparrow(Bird):
pass

class Penguin(Bird):
def fly(self):
raise Exception("Penguin cannot fly") # Violates LSP

# Correct design: separate classes


class Bird:
90 CONTENTS

pass

class FlyingBird(Bird):
def fly(self):
print("Flying")

class Sparrow(FlyingBird):
pass

class Penguin(Bird):
pass

Explanation:

• Subclasses should respect the behavior of the parent class.

• Avoid violating expectations (like Penguin “flying”).

• Ensures **reliability** when using polymorphism.

10.4 Interface Segregation Principle (ISP)


Definition:
Clients should not be forced to depend on interfaces they do not use. Large
interfaces should be split into smaller, specific ones.
Python Example:

from abc import ABC, abstractmethod

# Bad: One large interface


class Worker(ABC):
@abstractmethod
def work(self):
pass

@abstractmethod
def eat(self):
CONTENTS 91

pass

# ISP: Split interfaces


class Workable(ABC):
@abstractmethod
def work(self):
pass

class Eatable(ABC):
@abstractmethod
def eat(self):
pass

class Human(Workable, Eatable):


def work(self):
print("Working")

def eat(self):
print("Eating")

Explanation:

• Each class implements only what it needs.

• Reduces **unnecessary dependencies**.

10.5 Dependency Inversion Principle (DIP)


Definition:
High-level modules should not depend on low-level modules. Both should
depend on abstractions (interfaces).
Python Example:

from abc import ABC, abstractmethod

class Notification(ABC):
@abstractmethod
92 CONTENTS

def notify(self, message):


pass

class EmailNotification(Notification):
def notify(self, message):
print(f"Email: {message}")

class SMSNotification(Notification):
def notify(self, message):
print(f"SMS: {message}")

class User:
def __init__(self, notifier: Notification):
[Link] = notifier

def send_message(self, message):


[Link](message)

# Using DIP
user1 = User(EmailNotification())
user1.send_message("Hello via Email")

user2 = User(SMSNotification())
user2.send_message("Hello via SMS")

Explanation:

• User class depends on the abstract Notification interface, not concrete


implementations.

• Enables **flexibility**, **extensibility**, and easier **testing**.


Chapter 13: Mini Projects

13.1 Student Management System


Problem Statement:
Manage student details including name, roll number, grades, and provide
functionalities to add, display, and compute average grades.
Python Code Implementation:

class Student:
def __init__(self, name, roll, grades):
[Link] = name
[Link] = roll
[Link] = grades # list of numbers

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

def average(self):
return sum([Link]) / len([Link])

# Managing multiple students


students = [
Student("Alice", 101, [90, 85, 92]),
Student("Bob", 102, [78, 82, 80])
]

for s in students:
[Link]()
print("Average:", [Link]())

93
94 CONTENTS

Explanation:

• Each student is represented as an object (Encapsulation).

• Methods display() and average() provide functionality related to the stu-


dent object.

• Easy to extend (add new methods like grade report) without affecting
other classes.

13.2 Bank Account System


Problem Statement:
Implement a bank account system supporting deposits, withdrawals, and
balance check using OOP.
Python Code Implementation:

class BankAccount:
def __init__(self, owner, balance=0):
[Link] = owner
[Link] = balance

def deposit(self, amount):


[Link] += amount
print(f"Deposited {amount}, new balance: {[Link]}")

def withdraw(self, amount):


if amount > [Link]:
print("Insufficient balance!")
else:
[Link] -= amount
print(f"Withdrew {amount}, remaining balance: {[Link]}")

def check_balance(self):
print(f"Balance for {[Link]}: {[Link]}")

# Usage
acc = BankAccount("Alice", 1000)
CONTENTS 95

[Link](500)
[Link](300)
acc.check_balance()

Explanation:

• Encapsulates account details and operations in a class.

• Supports multiple accounts by creating multiple objects.

• Easy to extend: add transfer(), statement() methods without modifying


core logic.

13.3 Library Management System


Problem Statement:
Manage books in a library, including adding books, borrowing, and return-
ing them.
Python Code Implementation:

class Book:
def __init__(self, title, author):
[Link] = title
[Link] = author
self.is_borrowed = False

class Library:
def __init__(self):
[Link] = []

def add_book(self, book):


[Link](book)

def borrow_book(self, title):


for book in [Link]:
if [Link] == title and not book.is_borrowed:
book.is_borrowed = True
print(f"{title} borrowed successfully")
96 CONTENTS

return
print(f"{title} not available")

def return_book(self, title):


for book in [Link]:
if [Link] == title and book.is_borrowed:
book.is_borrowed = False
print(f"{title} returned successfully")
return
print(f"{title} was not borrowed")

# Usage
lib = Library()
b1 = Book("Python OOP", "Alice")
b2 = Book("ML Basics", "Bob")
lib.add_book(b1)
lib.add_book(b2)

lib.borrow_book("Python OOP")
lib.return_book("Python OOP")

Explanation:

• Uses **composition**: Library HAS-A Book.

• Each book object stores its own state (‘isb orrowed‘).

• Easy to extend: add search(), removeb ook(), orf ine()methods.

13.3 Library Management System


Problem Statement:
Manage books in a library, including adding books, borrowing, and return-
ing them.
Python Code Implementation:

class Book:
def __init__(self, title, author):
CONTENTS 97

[Link] = title
[Link] = author
self.is_borrowed = False

class Library:
def __init__(self):
[Link] = []

def add_book(self, book):


[Link](book)

def borrow_book(self, title):


for book in [Link]:
if [Link] == title and not book.is_borrowed:
book.is_borrowed = True
print(f"{title} borrowed successfully")
return
print(f"{title} not available")

def return_book(self, title):


for book in [Link]:
if [Link] == title and book.is_borrowed:
book.is_borrowed = False
print(f"{title} returned successfully")
return
print(f"{title} was not borrowed")

# Usage
lib = Library()
b1 = Book("Python OOP", "Alice")
b2 = Book("ML Basics", "Bob")
lib.add_book(b1)
lib.add_book(b2)

lib.borrow_book("Python OOP")
lib.return_book("Python OOP")

Explanation:
98 CONTENTS

• Uses **composition**: Library HAS-A Book.

• Each book object stores its own state (‘isb orrowed‘).

• Easy to extend: add search(), removeb ook(), orf ine()methods.

1. Encapsulation
Definition: Encapsulation is the practice of restricting direct access to ob-
ject data and providing controlled access via methods. It improves security,
modularity, and maintainability.
Example 1: Bank Account

class BankAccount:
def __init__(self, owner, balance=0):
[Link] = owner
self.__balance = balance # Private variable

def deposit(self, amount):


self.__balance += amount

def withdraw(self, amount):


if amount <= self.__balance:
self.__balance -= amount
else:
print("Insufficient balance!")

def check_balance(self):
return self.__balance

acc = BankAccount("Alice")
[Link](1000)
[Link](500)
print(acc.check_balance()) # 500

Explanation:

• balance is private → cannot be accessed directly from outside.


CONTENTS 99

• Methods control access and logic.

• Shows **data hiding + controlled access**, favorite example.

Example 2: Employee Salary Hiding

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

def get_salary(self):
return self.__salary

def set_salary(self, amount):


if amount > 0:
self.__salary = amount

emp = Employee("Bob", 50000)


emp.set_salary(60000)
print(emp.get_salary()) # 60000

Tip: Use private variables to prevent misuse and expose **getter/setter**


methods for controlled updates.

2. Inheritance
Definition: Inheritance allows a class (child) to inherit attributes and meth-
ods from another class (parent), enabling code reuse and hierarchical relation-
ships.
Example 1: Vehicle and Car

class Vehicle:
def __init__(self, brand):
[Link] = brand

def drive(self):
print(f"{[Link]} is moving")
100 CONTENTS

class Car(Vehicle):
def honk(self):
print(f"{[Link]} says Beep Beep!")

my_car = Car("Toyota")
my_car.drive() # Inherited
my_car.honk() # Own method

Example 2: Employee Hierarchy

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

def work(self):
print(f"{[Link]} is working")

class Manager(Employee):
def manage_team(self):
print(f"{[Link]} is managing team")

m = Manager("Alice", 90000)
[Link]() # Parent method
m.manage_team() # Child method

Tip: Be ready to explain **IS-A relationships**: Manager IS-A Em-


ployee.

3. Polymorphism
Definition: Polymorphism allows objects to be treated the same way despite
having different underlying implementations.
Example 1: Method Overriding

class Bird:
def speak(self):
CONTENTS 101

print("Some sound")

class Sparrow(Bird):
def speak(self):
print("Chirp Chirp")

class Duck(Bird):
def speak(self):
print("Quack Quack")

birds = [Sparrow(), Duck()]


for b in birds:
[Link]()

Example 2: Operator Overloading

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

def __add__(self, other):


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

p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2
print(p3.x, p3.y) # 4 6

Tip: Polymorphism can be **runtime (overriding) or compile-time (op-


erator overloading)** in Python.

4. Abstraction
Definition: Abstraction hides internal implementation details and exposes
only relevant functionality.
Example 1: Payment Gateway
102 CONTENTS

from abc import ABC, abstractmethod

class Payment(ABC):
@abstractmethod
def pay(self, amount):
pass

class CreditCard(Payment):
def pay(self, amount):
print(f"Paid {amount} using Credit Card")

class PayPal(Payment):
def pay(self, amount):
print(f"Paid {amount} using PayPal")

payments = [CreditCard(), PayPal()]


for p in payments:
[Link](100)

Example 2: Shape Drawing

from abc import ABC, abstractmethod

class Shape(ABC):
@abstractmethod
def area(self):
pass

class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
import math
return [Link] * self.r ** 2

Tip: Focus on **what the class does** not **how it does it**; always
relate to abstract classes or interfaces.
Appendix

103

You might also like