What is Object-Oriented Programming?
OOP is a programming paradigm based on the concept of objects, which can contain data and code: data in the form of attributes, and code in the form of methods.
According to the official Python documentation, "Classes provide a means of bundling data and functionality together. Creating a new class creates a new type of object, allowing new instances of that type to be made."
Key Insight: In Python, almost everything is an object with properties and methods. Even basic data types like integers and strings are objects.
Core OOP Concepts in Python
Class
A blueprint or template for creating objects. Defines attributes and methods.
Learn more on W3SchoolsObject
An instance of a class. Created from the class blueprint with actual data.
Learn more on W3SchoolsInheritance
Allows a class to inherit attributes and methods from another class.
Official Python DocsPolymorphism
Ability to use a common interface for different data types or classes.
W3Schools TutorialAdditional OOP Principles
Encapsulation
Bundling of data with the methods that operate on that data, often restricting direct access to some components.
Abstraction
Hiding complex implementation details and showing only essential features of an object.
Detailed Code Examples with Explanations
Example 1: Basic Class and Object
Creating a simple class and instantiating objects
class Student:
"""A class to represent a student."""
def __init__(self, name, course):
# Constructor method: Initializes object attributes
self.name = name # Instance attribute
self.course = course # Instance attribute
def introduce(self):
# Instance method: Defines behavior for Student objects
return f"Hi, I'm {self.name} and I'm studying {self.course}."
# Creating objects (instances) of the Student class
s1 = Student("Ravi", "Python Programming")
s2 = Student("Anjali", "Data Science")
# Accessing object methods
print(s1.introduce())
print(s2.introduce())
Explanation:
__init__()is the constructor method that initializes new objectsselfrefers to the instance being created (automatically passed)s1ands2are independent objects with their own attribute values- Methods defined in a class can access instance attributes using
self
Example 2: Inheritance
Creating a base class and derived class with method overriding
class Teacher:
"""Base class representing a teacher."""
def __init__(self, name, subject):
self.name = name
self.subject = subject
def teach(self):
return f"{self.name} is teaching {self.subject}."
class MathTeacher(Teacher): # Inherits from Teacher class
"""Derived class representing a math teacher."""
def __init__(self, name):
# Call parent class constructor using super()
super().__init__(name, "Mathematics")
def teach(self):
# Method overriding: Provides specific implementation
return f"{self.name} is teaching advanced Mathematics."
# Creating instances
generic_teacher = Teacher("Dr. Sharma", "General Science")
math_teacher = MathTeacher("Prof. Gupta")
# Demonstrating inheritance and polymorphism
print(generic_teacher.teach()) # Calls Teacher.teach()
print(math_teacher.teach()) # Calls MathTeacher.teach() (overridden)
print(f"Is MathTeacher a subclass of Teacher? {issubclass(MathTeacher, Teacher)}")
print(f"Is math_teacher an instance of Teacher? {isinstance(math_teacher, Teacher)}")
Key Points:
MathTeacher(Teacher)syntax creates inheritance relationshipsuper()calls the parent class constructor- Method overriding allows child classes to provide specific implementations
issubclass()andisinstance()check class relationships- This demonstrates polymorphism - same method name, different behaviors
Example 3: Encapsulation and Properties
Using private attributes and property decorators for controlled access
class BankAccount:
"""Demonstrates encapsulation with private attributes."""
def __init__(self, account_holder, initial_balance):
self.account_holder = account_holder
self._balance = initial_balance # Protected attribute (convention)
self.__transaction_count = 0 # Private attribute (name mangling)
# Property decorator for controlled access
@property
def balance(self):
# Getter method - allows read access to _balance
return self._balance
@balance.setter
def balance(self, amount):
# Setter method - validates before setting balance
if amount < 0:
raise ValueError("Balance cannot be negative")
self._balance = amount
self.__transaction_count += 1
def get_transaction_count(self):
# Public method to access private attribute
return self.__transaction_count
# Using the encapsulated class
account = BankAccount("Priya Sharma", 5000)
print(f"Account holder: {account.account_holder}")
print(f"Initial balance: ₹{account.balance}") # Uses getter
account.balance = 7500 # Uses setter - valid
print(f"Updated balance: ₹{account.balance}")
print(f"Transaction count: {account.get_transaction_count()}")
# This would raise an error:
# account.balance = -1000 # ValueError: Balance cannot be negative
Encapsulation Details:
- Single underscore (
_balance): Protected attribute (convention, not enforced) - Double underscore (
__transaction_count): Private attribute (name mangling) @propertydecorator creates getter method for controlled access- Setter method (
@balance.setter) adds validation logic - Encapsulation protects object integrity by controlling attribute access
Polymorphism in Python OOP
Polymorphism allows methods to do different things based on the object they are acting upon.
Method Overriding
Child class provides specific implementation of parent class method
Operator Overloading
Same operator behaves differently with different data types
Duck Typing
"If it walks like a duck and quacks like a duck, it's a duck"
Polymorphism in Action: Shape Calculator
class Shape:
"""Base class for different shapes."""
def area(self):
raise NotImplementedError("Subclasses must implement this method")
def perimeter(self):
raise NotImplementedError("Subclasses must implement this method")
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
# Rectangle-specific implementation
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
def __str__(self):
# String representation for printing
return f"Rectangle({self.width}x{self.height})"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
# Circle-specific implementation
import math
return math.pi * self.radius ** 2
def perimeter(self):
# Same method name, different calculation
import math
return 2 * math.pi * self.radius
def __str__(self):
return f"Circle(radius={self.radius})"
# Duck Typing Example: Function works with ANY object that has area() method
def print_shape_info(shape_obj):
"""This function demonstrates polymorphism - works with ANY shape."""
print(f"Shape: {shape_obj}")
print(f"Area: {shape_obj.area():.2f}")
print(f"Perimeter: {shape_obj.perimeter():.2f}")
print("-" * 30)
# Using polymorphism with different objects
shapes = [
Rectangle(5, 10),
Circle(7),
Rectangle(3, 6),
Circle(4.5)
]
# Same interface, different behaviors
for shape in shapes:
print_shape_info(shape) # Polymorphic call
# Operator Overloading Example
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
# Overloading + operator for Vector addition
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2 # Polymorphic operator usage
print(f"{v1} + {v2} = {v3}")
Polymorphism in Practice
- Method Overriding: Both
RectangleandCircleimplementarea()differently - Duck Typing:
print_shape_info()works with any object havingarea()andperimeter()methods - Common Interface: All shapes can be processed uniformly despite different implementations
- Operator Overloading:
+operator behaves differently forVectorobjects vs numbers
Real-World Applications
Payment Processing: Same process_payment() method works for credit cards, PayPal, or cryptocurrencies
Database Operations: save() method works uniformly for different database backends
File Export: Same export() interface for PDF, Excel, or CSV formats
Abstraction in Python OOP
Abstraction simplifies complex reality by modeling classes appropriate to the problem domain.
What is Abstraction?
Abstraction focuses on what an object does rather than how it does it. It's like driving a car - you use the steering wheel, pedals, and gearshift without knowing the internal combustion engine details.
Key Benefits:
- Reduces complexity by hiding implementation details
- Provides clear, simple interfaces
- Improves code maintainability
- Enhances security by restricting access
Abstract Class Example
ABC Modulefrom abc import ABC, abstractmethod
class Vehicle(ABC): # Abstract base class
"""Abstract class defining vehicle interface."""
@abstractmethod
def start_engine(self):
# Abstract method - must be implemented by subclasses
pass
@abstractmethod
def stop_engine(self):
# Abstract method - defines interface only
pass
def get_vehicle_type(self):
# Concrete method - available to all subclasses
return "Generic Vehicle"
class Car(Vehicle): # Concrete implementation
def __init__(self, brand):
self.brand = brand
self.engine_status = "off"
def start_engine(self):
self.engine_status = "on"
return f"{self.brand} engine started. Vroom vroom!"
def stop_engine(self):
self.engine_status = "off"
return f"{self.brand} engine stopped."
def get_vehicle_type(self):
# Overriding concrete method
return "Four-Wheeled Car"
# Using the abstraction
try:
v = Vehicle() # This will fail - cannot instantiate abstract class
except TypeError as e:
print(f"Error: {e}")
# Creating concrete vehicle
my_car = Car("Toyota")
print(my_car.start_engine()) # Uses Car's implementation
print(my_car.get_vehicle_type()) # Overridden method
print(my_car.stop_engine())
How Abstraction Works in This Example:
- ABC Module: Python's
abcmodule provides tools for creating abstract base classes - @abstractmethod: Decorator marks methods that must be implemented by child classes
- Interface Definition:
Vehicleclass defines what operations are available without specifying how - Implementation Hiding: Users interact with
Carobjects without knowing internal details - Forced Implementation: Python prevents instantiation of incomplete abstract classes
Advanced OOP Topics
Multiple Inheritance
Python supports inheriting from multiple parent classes:
class Person:
def __init__(self, name):
self.name = name
class Employee:
def __init__(self, emp_id):
self.emp_id = emp_id
class Manager(Person, Employee):
def __init__(self, name, emp_id, department):
Person.__init__(self, name)
Employee.__init__(self, emp_id)
self.department = department
mgr = Manager("Rajesh Kumar", "MGR101", "IT")
print(f"{mgr.name} (ID: {mgr.emp_id}) manages {mgr.department}")
Method Resolution Order (MRO) determines which parent class method is called.
Class & Static Methods
Methods that belong to the class rather than instances:
class Calculator:
operation_count = 0 # Class attribute
@classmethod
def increment_operation(cls):
cls.operation_count += 1
@staticmethod
def add(x, y):
Calculator.increment_operation()
return x + y
@staticmethod
def multiply(x, y):
Calculator.increment_operation()
return x * y
# Using class and static methods
result1 = Calculator.add(5, 3)
result2 = Calculator.multiply(5, 3)
print(f"Add result: {result1}, Multiply result: {result2}")
print(f"Total operations: {Calculator.operation_count}")
Class methods receive cls parameter, static methods
don't receive implicit parameters.
Why Use Object-Oriented Programming?
Modularity & Organization
Code is organized into logical units (classes), making it easier to understand, maintain, and debug.
Code Reusability
Inheritance allows creating new classes based on existing ones, reducing code duplication.
Scalability & Maintainability
OOP makes it easier to scale applications and add new features without breaking existing code.