πŸ›οΈ DevAscent Academy β€’ Core Python Textbook Series

Unit 12: Object-Oriented Programming (OOP)

The masterclass on Object-Oriented software engineering in Python. Learn to model complex real-world systems, master classes, objects, and the __init__() constructor, distinguish instance from class variables, safeguard state via encapsulation, create hierarchical architectures through inheritance and method overriding, achieve polymorphic flexibility, enforce abstract contracts with ABCs, and customize Python behavior using dunder methods like __str__().

Curriculum Level 1st Year BCA / B.Tech / Diploma
Unit Code PY-112 (Object-Oriented Paradigm)
Content Balance 50% Theory β€’ 30% Code β€’ 20% Flowcharts
Target Standard University Exam & Enterprise Architecture
πŸ“ Formula Verified: 50% Comprehensive Theory 30% Verified Code 20% Visual Flowcharts
1

Procedural Programming vs. OOP & The 4 Foundational Pillars

πŸ“Œ What is Object-Oriented Programming (OOP)?

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "Objects", which contain both Data (in the form of fields or attributes) and Code (in the form of procedures or methods). Unlike procedural programming, where functions operate on separate detached global variables, OOP bundles data and the functions that manipulate that data into self-contained entities.

πŸ“ FLOWCHART 1: PROCEDURAL (POP) VS. OBJECT-ORIENTED (OOP) PARADIGM Paradigm Shift
πŸ”΄ Procedural (C / Early Python)
Functions and Global Data are separate. Any function can accidentally overwrite global variables. High coupling, hard to maintain in large teams.
βž” Paradigm Shift βž”
🟒 Object-Oriented (OOP)
Data + Behavior are bundled together inside isolated Objects. State is protected, reusable, and extensible!
πŸ“ FLOWCHART 2: THE 4 PILLARS OF OBJECT-ORIENTED PROGRAMMING Core Pillars
1. Encapsulation
Bundling data and methods into a single unit; hiding internal state from unauthorized tampering.
2. Abstraction
Hiding complex internal implementation details and exposing only a clean, simple public interface.
3. Inheritance
Reusing existing code by deriving specialized child classes from common parent base classes.
4. Polymorphism
"Many forms": Allowing different classes to respond to the same method call in their own unique way.
Feature Procedural Programming (POP) Object-Oriented Programming (OOP)
Focus Focuses on verbs / actions (step-by-step algorithms). Focuses on nouns / entities (data objects and their behavior).
Data Security Low: Data moves freely and can be modified by any function. High: Data is encapsulated and hidden from unauthorized access.
Code Reuse Limited to function calls. Massive reuse through class inheritance and composition.
Suitability Small mathematical scripts and CLI utilities. Enterprise software, GUI apps, games, large cloud systems.
2

Classes and Objects: Blueprints, Instances, and Memory Allocation

πŸ“Œ Class vs. Object: The Formal Distinction

β€’ A Class is a user-defined prototype or blueprint that defines the attributes (variables) and methods (functions) that characterize any object of its type. It occupies no memory for data until instantiated.
β€’ An Object (or instance) is a concrete realization of a class allocated in computer RAM. Each object has its own unique copy of instance variables.

🏠 The Architectural Blueprint Analogy: A Class is like an architect's blueprint for a 3-bedroom house: you cannot live inside a blueprint! It's just a diagram.
An Object is the actual physical house constructed on a plot of land with bricks and cement. You can build 50 independent physical houses (Objects) from the exact same single blueprint (Class)!
πŸ“ FLOWCHART 3: CLASS BLUEPRINT TO MULTIPLE INSTANCES IN RAM Memory Allocation
Class Student (Blueprint)
Defines structure: name, roll_no, display()
βž” Instantiation βž”
RAM Address 0x7FA1 (Instance 1)
s1 = Student("Aman", 101)
βž” Instantiation βž”
RAM Address 0x7FB8 (Instance 2)
s2 = Student("Priya", 102)
3

Attributes, Methods, Constructors (`__init__`), and The `self` Parameter

πŸ“Œ What is `__init__()` and What is `self`?

β€’ The `__init__()` Method: Known as the constructor in Python. It is automatically called by CPython the instant a new object is instantiated from a class. Its primary role is to initialize the object's initial state (assigning instance variables).
β€’ The `self` Parameter: Represents the current instance of the class. When you write student1.display(), Python automatically translates it to Student.display(student1) behind the scenes. `self` allows methods to know which specific object's data they should access or modify.

Instance Variables vs. Class Variables

Characteristic Instance Variables (self.name) Class / Static Variables (college_name)
Where Defined? Inside constructor or methods using self. Directly inside class body, outside any method.
Scope & Memory Unique to each individual object in RAM. Single copy shared equally across all objects.
Modifying Value Modifying for obj1 affects ONLY obj1. Modifying via Class.var updates all objects!
🐍 01_class_constructor_demo.py
class Student:
    # Class Variable: Shared by ALL student objects
    college_name = "DevAscent Academy"

    # Constructor: Initializes unique attributes per instance
    def __init__(self, name, roll_no, gpa):
        self.name = name          # Instance variable
        self.roll_no = roll_no    # Instance variable
        self.gpa = gpa            # Instance variable

    # Instance Method: Uses self to access calling object's data
    def get_details(self):
        return f"Roll #{self.roll_no}: {self.name} | GPA: {self.gpa} | College: {Student.college_name}"

# Creating two separate instances in memory
s1 = Student("Aman Gupta", 101, 8.9)
s2 = Student("Neha Sharma", 102, 9.4)

print(s1.get_details())
print(s2.get_details())

# Proof of Class Variable sharing: Update college name once
Student.college_name = "DevAscent University"
print("\nAfter updating Class Variable:")
print(f"{s1.name}'s College -> {s1.college_name}")
print(f"{s2.name}'s College -> {s2.college_name}")
Terminal Output
Roll #101: Aman Gupta | GPA: 8.9 | College: DevAscent Academy Roll #102: Neha Sharma | GPA: 9.4 | College: DevAscent Academy After updating Class Variable: Aman Gupta's College -> DevAscent University Neha Sharma's College -> DevAscent University
4

Encapsulation: Access Modifiers, Name Mangling, and Getters/Setters

πŸ“Œ What is Encapsulation?

Encapsulation is the practice of keeping fields within a class private, then providing access to them via public methods (Getters and Setters). It prevents accidental direct modification of critical internal state and ensures data validation rules are strictly respected before changes occur.

Access Level Python Syntax Accessibility How Python Enforces It
Public self.balance Accessible from anywhere (inside & outside class). Standard direct attribute access.
Protected self._balance Accessible within class and subclasses. By Convention: Single underscore signals "do not touch outside".
Private self.__balance Accessible ONLY within declaring class. Name Mangling: Renamed internally to _ClassName__balance.
🐍 02_encapsulation_and_properties.py
class BankAccount:
    def __init__(self, account_holder, initial_balance):
        self.holder = account_holder     # Public
        self.__balance = initial_balance # PRIVATE: double underscore prevents direct tampering!

    # Getter: Securely read balance
    def get_balance(self):
        return self.__balance

    # Setter: Enforce business validation rules
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive!")
        self.__balance += amount
        return self.__balance

account = BankAccount("Rahul Verma", 5000)
print("Account Holder:", account.holder)
print("Current Balance:", account.get_balance())

account.deposit(2500)
print("Balance after β‚Ή2500 deposit:", account.get_balance())

# Attempting direct access to private variable throws an AttributeError!
try:
    print(account.__balance)
except AttributeError:
    print("πŸ›‘ BLOCKED: 'BankAccount' object has no attribute '__balance' (Private Protection!)")
Terminal Output
Account Holder: Rahul Verma Current Balance: 5000 Balance after β‚Ή2500 deposit: 7500 πŸ›‘ BLOCKED: 'BankAccount' object has no attribute '__balance' (Private Protection!)
5

Inheritance: Single, Multiple, Multilevel, and the `super()` Method

πŸ“Œ What is Inheritance?

Inheritance is a mechanism in which a new class (known as the child, derived, or subclass) is created from an existing class (known as the parent, base, or superclass). The child class automatically inherits all public and protected attributes and methods of the parent, enabling rapid code reuse and hierarchical categorization.

πŸ“ FLOWCHART 4: THE 4 MAJOR TYPES OF INHERITANCE IN PYTHON Class Hierarchies
1. Single Inheritance
One Child derives from One Parent:
Employee βž” Manager
2. Multilevel Inheritance
Grandparent βž” Parent βž” Child:
Vehicle βž” Car βž” ElectricCar
3. Multiple Inheritance
One Child derives from Multiple Parents:
(Camera, Phone) βž” Smartphone
4. Hierarchical Inheritance
One Parent has multiple Children:
Shape βž” Circle, Square, Triangle

The `super()` Function

The built-in super() function gives access to methods and constructor of the parent class. In child class constructors, calling super().__init__(...) passes arguments up to the parent so that foundational attributes are initialized cleanly without code duplication.

🐍 03_inheritance_and_super.py
# Parent Base Class
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def introduce(self):
        return f"Hello, my name is {self.name} and I am {self.age} years old."

# Child Derived Class (Single Inheritance)
class Professor(Person):
    def __init__(self, name, age, department, salary):
        # Delegate core attributes to Person's constructor via super()
        super().__init__(name, age)
        self.department = department
        self.salary = salary

    # Extended method specific to Professor
    def conduct_lecture(self, topic):
        return f"Prof. {self.name} ({self.department}) is delivering lecture on: {topic}"

prof = Professor("Dr. Arvind Mehta", 48, "Computer Science", 120000)
print(prof.introduce())        # Inherited directly from Person!
print(prof.conduct_lecture("Algorithms & Dynamic Programming"))
Terminal Output
Hello, my name is Dr. Arvind Mehta and I am 48 years old. Prof. Dr. Arvind Mehta (Computer Science) is delivering lecture on: Algorithms & Dynamic Programming
6

Polymorphism, Method Overriding, and Duck Typing

πŸ“Œ What is Polymorphism & Method Overriding?

β€’ Polymorphism (Greek for "many forms") is the ability to use the same interface or method call across different underlying types. For example, a single function render() can draw circles, squares, or polygons depending on the object passed to it.
β€’ Method Overriding: Occurs when a child class provides its own specific implementation of a method that is already defined in its parent class. When called on the child instance, the child's version executes, overriding the parent's generic logic.
β€’ Duck Typing: Python's dynamic typing philosophy: "If it walks like a duck and quacks like a duck, it's a duck." Python doesn't check explicit inheritance typesβ€”it simply checks if the required method exists!

🐍 04_polymorphism_and_overriding.py
class PaymentMethod:
    def pay(self, amount):
        raise NotImplementedError("Subclasses must implement pay()!")

# Child 1: Overrides pay() with Credit Card logic
class CreditCardPayment(PaymentMethod):
    def __init__(self, card_no):
        self.card_no = card_no[-4:]

    def pay(self, amount):
        return f"πŸ’³ Paid β‚Ή{amount} using Credit Card ending in **{self.card_no}"

# Child 2: Overrides pay() with UPI logic
class UPIPayment(PaymentMethod):
    def __init__(self, upi_id):
        self.upi_id = upi_id

    def pay(self, amount):
        return f"πŸ“± Paid β‚Ή{amount} via UPI handle: {self.upi_id}"

# Polymorphic Dispatcher Function: Works with ANY payment object!
def checkout(payment_gateway, bill_amount):
    print(payment_gateway.pay(bill_amount))

payments = [
    CreditCardPayment("4532118944528901"),
    UPIPayment("shivan@okaxis")
]

for p in payments:
    checkout(p, 2499)
Terminal Output
πŸ’³ Paid β‚Ή2499 using Credit Card ending in **8901 πŸ“± Paid β‚Ή2499 via UPI handle: shivan@okaxis
7

Abstraction: Abstract Base Classes (ABCs) and `@abstractmethod`

πŸ“Œ What is Abstraction in Software Design?

Abstraction involves hiding background details and displaying only essential features to the user. In Python, abstraction is enforced using the abc module (Abstract Base Classes).
An Abstract Class serves as a strict structural contract: it defines methods that all child classes must implement, and cannot be instantiated directly!

🐍 05_abstract_base_classes.py
from abc import ABC, abstractmethod
import math

# Abstract Contract Class
class Shape(ABC):
    @abstractmethod
    def calculate_area(self):
        """Every concrete child class MUST implement calculate_area."""
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    # Fulfilling the abstract contract
    def calculate_area(self):
        return round(math.pi * (self.radius ** 2), 2)

class Rectangle(Shape):
    def __init__(self, length, width):
        self.length = length
        self.width = width

    # Fulfilling the abstract contract
    def calculate_area(self):
        return self.length * self.width

c = Circle(7)
r = Rectangle(10, 5)

print("Circle Area   :", c.calculate_area())
print("Rectangle Area:", r.calculate_area())

# Attempting to instantiate an Abstract Class fails immediately!
try:
    s = Shape()
except TypeError as err:
    print("πŸ›‘ Abstraction Enforced:", err)
Terminal Output
Circle Area : 153.94 Rectangle Area: 50 πŸ›‘ Abstraction Enforced: Can't instantiate abstract class Shape without an implementation for abstract method 'calculate_area'
8

Special Methods (Dunder Methods): Customizing `__str__()` and `__repr__()`

πŸ“Œ What are Special / Dunder Methods?

Dunder methods (short for "Double Underscore" methods) are predefined special methods in Python that have double underscores at the start and end of their names (e.g., __init__, __str__, __len__, __eq__). They allow your custom objects to integrate seamlessly with built-in Python operators and functions like print(), len(), and +.

Method Triggered By Primary Intended Audience
__str__(self) print(obj) or str(obj) End Users: Returns clean, readable, friendly informal description.
__repr__(self) Typing object in Python interactive console Developers: Unambiguous representation, ideally valid Python code to recreate the object.
__len__(self) len(obj) Returns length/item count of custom container objects.
__add__(self, other) obj1 + obj2 Customizes addition operator behavior (Operator Overloading).
🐍 06_dunder_str_and_repr.py
class Book:
    def __init__(self, title, author, price):
        self.title = title
        self.author = author
        self.price = price

    # User-facing string representation for print()
    def __str__(self):
        return f"πŸ“– '{self.title}' by {self.author} (β‚Ή{self.price})"

    # Developer-facing unambiguous representation for debugging
    def __repr__(self):
        return f"Book(title='{self.title}', author='{self.author}', price={self.price})"

b = Book("Fluent Python", "Luciano Ramalho", 1850)

print("Calling print(b)   -> Triggers __str__ :", b)
print("Calling repr(b)    -> Triggers __repr__:", repr(b))
Terminal Output
Calling print(b) -> Triggers __str__ : πŸ“– 'Fluent Python' by Luciano Ramalho (β‚Ή1850) Calling repr(b) -> Triggers __repr__: Book(title='Fluent Python', author='Luciano Ramalho', price=1850)
9

Unit 12 Practical Capstone: Enterprise Banking Architecture

This capstone combines all 10 OOP concepts: an abstract base class (BankAccount), concrete subclasses with inheritance (SavingsAccount, CurrentAccount), private balance encapsulation, method overriding for interest calculation, polymorphism, and __str__() dunder formatting.

🐍 enterprise_banking_engine.py
from abc import ABC, abstractmethod

# 1. Abstraction
class BankAccount(ABC):
    def __init__(self, account_no, holder, initial_balance):
        self.account_no = account_no
        self.holder = holder
        self.__balance = initial_balance  # 2. Encapsulation: Private state

    def get_balance(self):
        return self.__balance

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            return self.__balance
        raise ValueError("Deposit must be positive!")

    def _withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            return True
        return False

    @abstractmethod
    def apply_monthly_interest(self):
        pass

    # 3. Special Dunder Method
    def __str__(self):
        return f"[{self.__class__.__name__}] #{self.account_no} ({self.holder}) Balance: β‚Ή{self.__balance:,.2f}"

# 4. Inheritance & Method Overriding
class SavingsAccount(BankAccount):
    INTEREST_RATE = 0.04  # 4% annual interest

    def apply_monthly_interest(self):
        interest = self.get_balance() * (self.INTEREST_RATE / 12)
        self.deposit(round(interest, 2))
        return interest

class CurrentAccount(BankAccount):
    OVERDRAFT_LIMIT = 25000

    def apply_monthly_interest(self):
        return 0.0  # Current accounts do not earn interest

# 5. Polymorphic Execution
accounts = [
    SavingsAccount("SB-1001", "Ananya Roy", 50000),
    CurrentAccount("CA-5002", "Apex Technologies Ltd", 180000)
]

print("--- Monthly Batch Processing ---")
for acc in accounts:
    interest_earned = acc.apply_monthly_interest()
    print(acc)
    print(f"  -> Monthly Interest Credited: β‚Ή{interest_earned:.2f}\n")
Terminal Output
--- Monthly Batch Processing --- [SavingsAccount] #SB-1001 (Ananya Roy) Balance: β‚Ή50,166.67 -> Monthly Interest Credited: β‚Ή166.67 [CurrentAccount] #CA-5002 (Apex Technologies Ltd) Balance: β‚Ή180,000.00 -> Monthly Interest Credited: β‚Ή0.00
10

University Exam & Viva Preparation Guide (Frequently Asked Questions)

Q1. What is the role of the `self` parameter in Python methods?
self represents the current instance of the class. It allows methods to access and modify the specific attributes and methods belonging to the object that invoked them. Python passes the instance reference to self automatically.
Q2. Differentiate between Class Variables and Instance Variables.
Instance Variables: Defined inside constructors using self.name; each object maintains its own distinct copy in memory.
Class Variables: Defined directly inside the class body; a single copy is shared across all instances of that class.
Q3. How does Python implement Private variables (Encapsulation)?
Python implements private attributes using a double underscore prefix (e.g. self.__balance). Python triggers Name Mangling, automatically renaming the variable internally to _ClassName__balance to prevent direct outside access.
Q4. What is the difference between Method Overloading and Method Overriding?
Method Overriding: A child class provides a specialized implementation of a method already defined in its parent class (fully supported in Python).
Method Overloading: Defining multiple methods with the same name but different parameter signatures in the same class (Python does NOT natively support this; default arguments or *args are used instead).
Q5. What is an Abstract Base Class (ABC) and why is it used?
An Abstract Base Class (created using from abc import ABC, abstractmethod) defines a mandatory blueprint/contract for subclasses. Any class with an @abstractmethod cannot be instantiated directly, forcing all child classes to implement that method.
Q6. What is the difference between `__str__()` and `__repr__()`?
β€’ __str__(): Returns a clean, user-friendly string representation intended for end-users when calling print(obj).
β€’ __repr__(): Returns an unambiguous, detailed representation intended for developers and debugging, showing the exact code needed to recreate the object.