Object-Oriented Programming (OOP)
Object-Oriented Programming (OOP) is a programming paradigm that organises data, or objects, properly rather than functions and logic. In an object-oriented approach, software is organised using classes and objects. A class explains the structure and behaviour (in terms of attributes and methods) that the objects created from it will have. An object is an instance of a class.
This method of programming represents the real-world entities more naturally. For example, a Car class may represent properties like colour, make, and model, while behaviours may include driving, stopping, and honking.
![]() |
Object-Oriented Programming (OOP) in Python |
Why Use Object-Oriented Programming?
Before the OOP concept, procedural programming was the dominant style, where logic and data were kept separate, often leading to code that was difficult to maintain and scale. OOP helps address concerns in modern software development
Core Principles of OOP
Four principles make up the object-oriented paradigm:
Encapsulation
Inheritance
Inheritance is a mechanism by which one class can derive its properties and behaviours from another class. This helps code reusability and establishes a natural hierarchy between classes.
Example:
A Vehicle class can be a base class for Car, Truck, and Motorbike classes, which inherit common functions like starting the engine or stopping.
Polymorphism
Polymorphism allows different classes to be treated as instances of the same class by a common interface. It enables a single function or method to run on objects of different classes.
Example:
A function make_sound(animal) could accept objects of class Dog, Cat, or Bird, and each would respond differently based on its implementation of the make_sound method.
Abstraction
Real-World Analogies
To better understand OOP :
Class and Object: A class is like a blueprint for a house, while an object is an actual house built from that blueprint. You can build many houses (objects) according to a blueprint (class).
Encapsulation: Like a remote control. You interact with its buttons (interface), but you don’t know the complex wiring inside. That complexity is hidden from you.
Inheritance: Imagine a family tree. A child inherits traits from parents, just like a subclass inherits properties and methods from its superclass.
Polymorphism: Suppose a universal remote. One button (method) can control many devices, such as a TV, a DVD player, etc., each responding according to its own design.
Importance of OOP in Python
Python supports multiple programming paradigms, such as procedural, functional, and object-oriented. However, it is especially well-suited for OOP due to:
- Simple and clean syntax that doesn’t add unnecessary complexity.
- Built-in module support for classes, inheritance, and special (magic) methods.
- Extensive standard libraries and third-party modules that are designed with OOP principles.
- Readability and community conventions (like PEP 8) that favour OOP organisation.
Python uses classes and objects as first-class citizens, and its dynamic typing system makes OOP more flexible while still being powerful.
A Simple Example
Python Code
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
def bark(self):
print(f"{self.name} says Woof!")
# Creating objects
dog1 = Dog("Buddy", "Labrador")
dog2 = Dog("Rex", "German Shepherd")
# Accessing methods
dog1.bark() # Output: Buddy says Woof!
dog2.bark() # Output: Rex says Woof!
Common Use of OOP in Python
- Web Development: Frameworks like Django and Flask are built on object-oriented programming (OOP) principles.
- Game Development: Classes like Player, Enemy, and GameObject represent entities in the game world.
- GUI Applications: Tools like Tkinter or PyQt are class-based and benefit from OOP structures.
- Data Science and Machine Learning: Libraries such as scikit-learn and TensorFlow use classes extensively to manage models and datasets.
- APIs and Microservices: Clear, modular class designs make APIs easier to build and scale.
Python and the OOP Paradigm
A Brief History of Python and Object Orientation
Python Code
print(type(5)) # class type 'int'
print(type("Hello")) # class type 'str'
print(type([1, 2, 3])) # class type list
print(type(print)) # class type 'builtin_function_or_method'
Python’s Class-Based System
Python Code
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
return f"Hello, my name is {self.name} and I am {self.age} years old."
p1 = Person("Alice", 30)
print(p1.greet()) # Outputs: Hello, my name is Alice and I am 30 years old.
- Person is the class.
- __init__() is the constructor method, automatically called when a new object is created.
- self is the instance reference.
- name and age are instance variables (attributes).
- greet() is an instance method.
Common Magic Methods:
- __init__(self, ...): Constructor, called when object is created.
- __str__(self): Defines the string representation of the object.
- __repr__(self): Developer-focused representation.
- __len__(self): Used by len().
- __eq__(self, other): Defines behaviour for equality (==).
- __add__(self, other): Defines behaviour for +.
Python Code
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __str__(self):
return f"{self.title} by {self.author}"
book = Book("1984", "George Orwell")
print(book) # Output: 1984 by George Orwell
First-Class Functions and OOP
Python Code
class Greeter:
def __call__(self, name):
return f"Hello, {name}!"
greet = Greeter()
print(greet("Charlie")) # Output: Hello, Charlie!
Python Code
class Animal:
def speak(self):
return "Some sound"
class Dog(Animal):
def speak(self):
return "Woof!"
Python Code
class Engine:
def start(self):
return "Engine started."
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
return self.engine.start()
Duck Typing in Python
Python Code
class Duck:
def quack(self):
return "Quack!"
class Person:
def quack(self):
return "I'm pretending to be a duck."
def make_quack(thing):
print(thing.quack())
make_quack(Duck()) # Quack!
make_quack(Person()) # I'm pretending to be a duck.
The Zen of Python and OOP
Classes and Objects in Python
1. What is a Class?
Python Code
class Person:
pass
Python Code
p1 = Person()
p2 = Person()
print(type(p1)) #
print(p1 == p2) # False – different objects in memory
2. Adding Attributes and Methods
Python Code
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
print(f"Hello, my name is {self.name} and I am {self.age} years old.")
- __init__: This is the constructor method, which is called when a new object is instantiated.
- self: A reference to the current object (similar to this in other languages).
- name and age: Instance attributes.
- greet: A method that uses the object’s data.
Python Code
p1 = Person("Alice", 28)
p2 = Person("Bob", 34)
p1.greet() # Hello, my name is Alice and I am 28 years old.
p2.greet() # Hello, my name is Bob and I am 34 years old.
3. Class Variables vs Instance Variables
Python Code
class Dog:
species = "Canis familiaris" # Class variable
def __init__(self, name):
self.name = name # Instance variable
dog1 = Dog("Rex")
dog2 = Dog("Luna")
print(dog1.species) # Canis familiaris
print(dog2.species) # Canis familiaris
print(dog1.name) # Rex
print(dog2.name) # Luna
4. Instance Methods, Class Methods, and Static Methods
1 Instance Methods
Python Code
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
2 Class Methods
Python Code
class Book:
count = 0
def __init__(self, title):
self.title = title
Book.count += 1
@classmethod
def get_book_count(cls):
return cls.count
3 Static Methods
Python Code
class Math:
@staticmethod
def add(x, y):
return x + y
5 The __str__ and __repr__ Methods
- __str__: User-friendly string representation.
- __repr__: Debugging/developer-friendly representation.
Python Code
class Car:
def __init__(self, make, model):
self.make = make
self.model = model
def __str__(self):
return f"{self.make} {self.model}"
def __repr__(self):
return f"Car('{self.make}', '{self.model}')"
car = Car("Toyota", "Corolla")
print(car) # Toyota Corolla
print(repr(car)) # Car('Toyota', 'Corolla')
6 Object Identity and Comparison
Python Code
x = [1, 2, 3]
y = [1, 2, 3]
print(id(x)) # e.g. 140396465120384
print(id(y)) # different from x
7 Dynamic Attributes and hasattr, getattr, setattr
Python Code
class Animal:
pass
a = Animal()
a.species = "Cat" # Dynamic attribute
print(a.species) # Cat
Python Code
hasattr(a, "species") # True
getattr(a, "species") # 'Cat'
setattr(a, "name", "Whiskers")
print(a.name) # Whiskers
8 Private and Protected Attributes
- _protected: Suggests it’s for internal use.
- __private: Name-mangled to discourage access.
Python Code
class Example:
def __init__(self):
self.public = "I'm public"
self._protected = "I'm protected"
self.__private = "I'm private"
e = Example()
print(e.public) # Accessible
print(e._protected) # Accessible but discouraged
# print(e.__private) # AttributeError
print(e._Example__private) # Access via name mangling
9 Deleting Attributes and Objects
Python Code
class Test:
def __init__(self):
self.value = 100
obj = Test()
del obj.value # Deletes attribute
# del obj # Deletes the object itself
10 Best Practices
- Use descriptive names for classes and attributes.
- Keep methods focused on a single task.
- Prefer composition over inheritance for flexibility.
- Follow the PEP 8 style guide.
- Document your class with docstrings.
Python Code
class Rectangle:
"""
A class to represent a rectangle.
Attributes:
----------
width : float
Width of the rectangle
height : float
Height of the rectangle
Methods:
-------
area():
Returns the area of the rectangle
"""
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
Inheritance and Code Reusability in Python
1. Understanding Inheritance
Python Code
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a noise."
class Dog(Animal):
def speak(self):
return f"{self.name} barks."
d = Dog("Buddy")
print(d.speak()) # Output: Buddy barks.
2. The super() Function
Python Code
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Calls Animal’s __init__
self.breed = breed
d = Dog("Rex", "Labrador")
print(d.name) # Rex
print(d.breed) # Labrador
3 Method Overriding
Python Code
class Vehicle:
def start(self):
print("Vehicle started.")
class Car(Vehicle):
def start(self):
print("Car engine started.")
c = Car()
c.start() # Car engine started.
4 Inheritance Hierarchies
1 Single Inheritance
Python Code
class Parent:
pass
class Child(Parent):
pass
2 Multi-level Inheritance
Python Code
class Grandparent:
pass
class Parent(Grandparent):
pass
class Child(Parent):
pass
3 Multiple Inheritance
Python Code
class Father:
def skills(self):
return "Gardening"
class Mother:
def skills(self):
return "Cooking"
class Child(Father, Mother):
pass
c = Child()
print(c.skills()) # Gardening – resolved by MRO
Python Code
print(Child.__mro__)
5 Composition vs Inheritance
Python Code
class Engine:
def start(self):
return "Engine started"
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
return self.engine.start()
c = Car()
print(c.start()) # Engine started
6 Polymorphism and Inheritance
Python Code
class Cat:
def speak(self):
return "Meow"
class Dog:
def speak(self):
return "Woof"
def animal_sound(animal):
print(animal.speak())
animal_sound(Cat()) # Meow
animal_sound(Dog()) # Woof
7 Abstract Base Classes
Python Code
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof"
Python Code
a = Animal() # TypeError: Can't instantiate abstract class
8 isinstance() and issubclass()
Python Code
print(isinstance(d, Dog)) # True
print(isinstance(d, Animal)) # True
print(issubclass(Dog, Animal)) # True
print(issubclass(Animal, Dog)) # False
9 Inheritance Pitfalls
- Overusing inheritance: Deep hierarchies are hard to manage.
- Tight coupling: Changes in base classes can break subclasses.
- Multiple inheritance conflicts: Can lead to unpredictable results if not carefully managed.
10 Real-World Example: A Role-Playing Game
Python Code
class Character:
def __init__(self, name, level):
self.name = name
self.level = level
def attack(self):
return f"{self.name} attacks!"
class Warrior(Character):
def attack(self):
return f"{self.name} swings a sword!"
class Mage(Character):
def attack(self):
return f"{self.name} casts a fireball!"
def battle(fighter):
print(fighter.attack())
w = Warrior("Thorin", 5)
m = Mage("Gandalf", 10)
battle(w) # Thorin swings a sword!
battle(m) # Gandalf casts a fireball!
Encapsulation and Data Hiding in Python
1. What is Encapsulation?
- It protects an object’s internal state from unwanted changes.
- It enforces a controlled way to interact with the object (via methods).
- It improves code maintainability and flexibility.
2 Public, Protected, and Private Members
1 Public Members
Python Code
class Car:
def __init__(self, make):
self.make = make # public attribute
def start(self): # public method
print(f"{self.make} is starting.")
Python Code
c = Car("Toyota")
print(c.make) # Toyota
c.start() # Toyota is starting.
2 Protected Members
Python Code
class Car:
def __init__(self, make):
self._engine_status = False # protected attribute
def _check_engine(self): # protected method
return self._engine_status
3 Private Members
Python Code
class Car:
def __init__(self, make):
self.__make = make # private attribute
def __start_engine(self): # private method
print(f"{self.__make}'s engine started.")
Python Code
c = Car("Honda")
# print(c.__make) # AttributeError
# c.__start_engine() # AttributeError
Python Code
print(c._Car__make) # Honda
c._Car__start_engine() # Honda's engine started.
3 Getter and Setter Methods
Python Code
class BankAccount:
def __init__(self, balance):
self.__balance = balance
def get_balance(self):
return self.__balance
def set_balance(self, amount):
if amount >= 0:
self.__balance = amount
else:
print("Invalid amount")
Python Code
acc = BankAccount(1000)
print(acc.get_balance()) # 1000
acc.set_balance(1500)
print(acc.get_balance()) # 1500
__balance
, allowing access only through the approved interface.
4 Pythonic Way: Using Properties
Python Code
class Product:
def __init__(self, price):
self.__price = price
@property
def price(self):
return self.__price
@price.setter
def price(self, value):
if value > 0:
self.__price = value
else:
raise ValueError("Price must be positive")
Python Code
p = Product(50)
print(p.price) # 50
p.price = 75
print(p.price) # 75
# p.price = -20 # Raises ValueError
5 Benefits of Encapsulation
- Data protection prevents external objects from corrupting the internal state.
- Modular code: The internal implementation can be changed without affecting the external code.
- Improved maintenance: Bugs are easier to isolate and fix.
- Readability and clarity: Indicates how attributes and methods should be used.
6 Encapsulation in Practice
Python Code
class EmailSender:
def __init__(self, smtp_server):
self.__smtp_server = smtp_server
self.__is_connected = False
def connect(self):
self.__is_connected = True
print(f"Connected to {self.__smtp_server}")
def send_email(self, to, subject, message):
if not self.__is_connected:
print("Please connect to the server first.")
return
print(f"Sending email to {to}: {subject}")
Python Code
e = EmailSender("smtp.mail.com")
e.send_email("john@example.com", "Hello", "This is a test") # Requires connection
e.connect()
e.send_email("john@example.com", "Hello", "This is a test") # Works
Polymorphism and Dynamic Behaviour in Python
1. What is Polymorphism?
2 Types of Polymorphism
- Duck Typing (Dynamic Typing)
- Method Overriding
- Operator Overloading
- Polymorphism with Inheritance
3 Duck Typing in Python
“If it looks like a duck, swims like a duck, and quacks like a duck, it probably is a duck.”
Python Code
class Duck:
def sound(self):
return "Quack"
class Dog:
def sound(self):
return "Bark"
def animal_sound(animal):
print(animal.sound())
d1 = Duck()
d2 = Dog()
animal_sound(d1) # Quack
animal_sound(d2) # Bark
4 Polymorphism via Inheritance
Python Code
class Animal:
def speak(self):
raise NotImplementedError("Subclass must implement abstract method")
class Cat(Animal):
def speak(self):
return "Meow"
class Cow(Animal):
def speak(self):
return "Moo"
def talk(animal: Animal):
print(animal.speak())
talk(Cat()) # Meow
talk(Cow()) # Moo
5 Operator Overloading
Python Code
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"({self.x}, {self.y})"
Python Code
v1 = Vector(2, 3)
v2 = Vector(1, 4)
print(v1 + v2) # (3, 7)
6 Function and Method Overloading
Python Code
def greet(name=None):
if name:
print(f"Hello, {name}!")
else:
print("Hello!")
greet("Alice") # Hello, Alice!
greet() # Hello!
7 Real-World Example: Payment Systems
Python Code
class Payment:
def pay(self, amount):
raise NotImplementedError
class CreditCardPayment(Payment):
def pay(self, amount):
print(f"Paid £{amount} using credit card.")
class PayPalPayment(Payment):
def pay(self, amount):
print(f"Paid £{amount} using PayPal.")
def process_payment(payment: Payment, amount):
payment.pay(amount)
Python Code
class Payment:
def pay(self, amount):
raise NotImplementedError
class CreditCardPayment(Payment):
def pay(self, amount):
print(f"Paid £{amount} using credit card.")
class PayPalPayment(Payment):
def pay(self, amount):
print(f"Paid £{amount} using PayPal.")
def process_payment(payment: Payment, amount):
payment.pay(amount)
Python Code
p1 = CreditCardPayment()
p2 = PayPalPayment()
process_payment(p1, 100)
process_payment(p2, 200)
Python Code
Paid £100 using credit card.
Paid £200 using PayPal.
8 The Role of Abstract Base Classes
Python Code
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
9 Polymorphism in Built-in Functions
Python Code
print(len("hello")) # 5
print(len([1, 2, 3])) # 3
print(str(100)) # "100"
print(str([1, 2])) # "[1, 2]"
10 Benefits of Polymorphism
- Flexibility: Code becomes more general and adaptable.
- Reusability: Polymorphic code can work with any class that meets the expected interface.
- Clean Architecture: Promotes the use of abstract interfaces rather than concrete implementations.
- Extensibility: New types can be introduced without modifying existing code.
11 Limitations and Considerations
- Overuse of inheritance for polymorphism can create tightly coupled designs.
- Duck typing relies on runtime correctness—no compile-time guarantees.
- Ensure clear documentation of expected interfaces, especially in large projects.