Object-Oriented Programming (OOP) in PythonPixelpy-Python programming with source code

Object-Oriented Programming (OOP) in Python

 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
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

Encapsulation means bundling the data (attributes) and methods that operate on the data into a single unit or class, and restricting direct access to some of the object’s components. This protects against accidental interference and enforces a clear structure.

Example:
A class representing a bank account may hide its balance field and only allow changes through deposit or withdrawal methods, protecting the data from unwanted manipulation.

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

The Abstraction hides unnecessary implementation details from the user and shows only the essential features. It allows programmers to focus on interactions at a high level.

Example:
When you use a car, you don’t need to know how the engine works; you just need the steering wheel, pedals, and gear stick.

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:

  1. Simple and clean syntax that doesn’t add unnecessary complexity.
  2. Built-in module support for classes, inheritance, and special (magic) methods.
  3. Extensive standard libraries and third-party modules that are designed with OOP principles.
  4. 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!

In this example:

Dog is a class.

dog1 and dog2 are objects (instances of the class).

__init__ is a constructor that initialises each object.

bark is a method that defines behaviour.

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 was developed by Guido van Rossum in the late 1980s and released in 1991. From its inception, Python was designed to be highly readable, intuitive, and flexible. Van Rossum was inspired by the ABC language and wanted to build something as easy to use but far more extensible and powerful.

Python was not originally designed as a purely object-oriented language, but also a multi-paradigm language that supports procedural, functional, and object-oriented styles. Over time, object-oriented programming became one of the most prominent and widely used as powerful features of Python, with its class system and object model being refined and enhanced with each new version of the language.

One of Python's core design philosophies is that everything is an object. This means that integers, strings, functions, lists, classes, and even modules are implemented as objects of specific classes.

Let’s demonstrate this with a few quick examples:

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'

This object-centric model means that each data type has built-in methods and behaviours, which are accessible via dot notation. For instance:

The string "hello world" is an object of class str, and it has methods like .upper() and .lower() built in.


Python’s Class-Based System

Python's Object-Oriented Programming (OOP) system is a class-based programming language, meaning it allows you to define custom classes, from which you can instantiate multiple objects (instances). It is similar to many other object-oriented languages, such as Java or C++, but Python does it with much less boilerplate and more dynamic behaviour.

Let’s revisit a class definition in Python:

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.

Python’s class syntax is concise, and the dynamic typing system means there is no need to declare variable types explicitly.

Python’s Object Model and __init__, __str__, and More

Python’s object model is powered by magic methods. It is also called Dunder methods, due to the double underscores. These are special methods that you can define in your classes to give them custom behaviour.

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 +.
Example:

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

Here, __str__ allows the object to print a more user-friendly string.

First-Class Functions and OOP

Python treats functions as first-class objects, which means they can be assigned to variables, passed as arguments, and returned from other functions. This integrates elegantly with OOP, as you can have objects that behave like functions using the __call__ method.

Example:

Python Code


class Greeter:
    def __call__(self, name):
        return f"Hello, {name}!"

greet = Greeter()
print(greet("Charlie"))  # Output: Hello, Charlie!

This design flexibility is rarely seen in many traditional object-oriented languages.

Composition vs Inheritance in Python

Python supports both inheritance and composition, and while inheritance gets more attention, composition is often more powerful and flexible.

Inheritance Example:

Python Code


class Animal:
    def speak(self):
        return "Some sound"

class Dog(Animal):
    def speak(self):
        return "Woof!"

Composition Example:

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()

Composition supports building complex functionality by combining smaller objects. It’s often preferred over inheritance in large systems due to its modularity and loose coupling.

Duck Typing in Python

Python doesn’t enforce type-checking in the traditional sense. It uses duck typing, meaning: “If it walks like a duck and quacks like a duck, it’s a duck.”

This means that if two objects have the same methods, Python will treat them the same, regardless of their actual class.

Example:

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.

This style is extremely powerful but requires developers to write clear, well-documented code.

The Zen of Python and OOP

You can access the Zen of Python by running import this in a Python interpreter. Some principles from Zen that align well with OOP include:
“Simple is better than complex.”
“Readability counts.”
“There should be one—and preferably only one—obvious way to do it.”

Python’s OOP promotes simplicity, clear structure, and elegant solutions over complicated abstractions.

Classes and Objects in Python

1. What is a Class?

In Python, a class is a user-defined blueprint or prototype from which objects are created. Classes encapsulate data for the object and methods to operate on that data. Defining a class in Python is straightforward and follows a very readable syntax.

Here is a simple example:

Python Code


class Person:
    pass

The class keyword is used to define a class. This Person class currently does nothing—it's simply a shell. However, we can now create instances (objects) from it:

Python Code


p1 = Person()
p2 = Person()

print(type(p1))  # 
print(p1 == p2)  # False – different objects in memory

2. Adding Attributes and Methods

A class becomes more useful when it has attributes (data) and methods (functions defined inside a class).

Let’s define a Person class with 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.

To create and use objects from the class:

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.

Each object holds its own copy of the attributes.

3. Class Variables vs Instance Variables

Instance variables are unique to each object. Class variables, however, are shared among all instances of a class.

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

Class variables are often used for constants or default values that apply to all instances.


4. Instance Methods, Class Methods, and Static Methods

Python supports three kinds of methods within classes:

1 Instance Methods

The most common method type, operating on an instance of the class.

Python Code


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

    def area(self):
        return 3.14 * self.radius ** 2

2 Class Methods

Class methods operate on the class itself, rather than on instances. They are marked with the @classmethod decorator.

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

Static methods do not access instance or class data. They’re utility functions grouped with the class logically.

Python Code


class Math:
    @staticmethod
    def add(x, y):
        return x + y

5 The __str__ and __repr__ Methods

Python provides special (magic/dunder) methods to control how objects are represented as strings.
  • __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

Every object in Python has a unique identity (memory address). Use the id() function to see this:

Python Code


x = [1, 2, 3]
y = [1, 2, 3]
print(id(x))  # e.g. 140396465120384
print(id(y))  # different from x

To compare:
== checks value equality using __eq__. 
is checks identity (same memory location).

7 Dynamic Attributes and hasattr, getattr, setattr

Python allows attributes to be added to objects dynamically:

Python Code


class Animal:
    pass

a = Animal()
a.species = "Cat"  # Dynamic attribute

print(a.species)   # Cat

You can use built-in functions to interact with attributes:

Python Code


hasattr(a, "species")         # True
getattr(a, "species")         # 'Cat'
setattr(a, "name", "Whiskers")
print(a.name)                 # Whiskers

8 Private and Protected Attributes

Python doesn’t enforce access restrictions, but naming conventions help signal intent:
  • _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

Attributes and objects can be deleted using del:

Python Code


class Test:
    def __init__(self):
        self.value = 100

obj = Test()
del obj.value  # Deletes attribute
# del obj       # Deletes the object itself

Deleting an object doesn’t guarantee immediate memory release, as Python uses garbage collection.

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.

Example of a well-documented class:

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

Inheritance is a fundamental principle of object-oriented programming that enables a class (child or subclass) to inherit attributes and methods from another class (parent or superclass). Inheritance provides scope, code reusability, modularity, and hierarchical relationships.

Python supports single inheritance, multiple inheritance, and multi-level inheritance.
Let’s begin with a basic example of single 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.

Here, Dog inherits from Animal, and we override the speak() method.

2. The super() Function

Python provides the built-in super() function, which allows access to methods from a superclass. This is particularly useful when extending functionality rather than replacing it.

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

Using super() function ensures proper initialisation of the parent class and supports cooperative multiple inheritance.

3 Method Overriding

Method overriding occurs when a subclass defines a method with the same name as one in the parent class. This allows the subclass to modify or enhance behaviour. 

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.

Method Overriding allows for customised behaviour in specialised subclasses.

4 Inheritance Hierarchies

Let’s examine different types of inheritance supported in Python.

1 Single Inheritance

A class inherits from one parent:

Python Code


class Parent:
    pass

class Child(Parent):
    pass

2 Multi-level Inheritance

A class inherits from a subclass, forming a chain:

Python Code


class Grandparent:
    pass

class Parent(Grandparent):
    pass

class Child(Parent):
    pass

3 Multiple Inheritance

A class inherits from multiple parents:

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 resolves method conflicts using the Method Resolution Order (MRO), which follows a depth-first, left-to-right approach.

You can inspect the MRO using:

Python Code


print(Child.__mro__)

5 Composition vs Inheritance

While inheritance is useful, it can lead to overly tight coupling and fragile hierarchies. An alternative is composition, where a class is composed of other objects, rather than inheriting from them.

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


Composition is generally more flexible and encourages better separation of concerns.

6 Polymorphism and Inheritance

Polymorphism allows different classes to implement methods with the same name. Python handles this naturally due to dynamic typing.

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

The function animal_sound() works with any object that implements a speak() method—this is duck typing in action.

7 Abstract Base Classes

Python allows the abc module to create abstract base classes (ABCs). These classes define interfaces but cannot be instantiated directly.

Python Code


from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof"

Attempting to instantiate Animal directly would raise an error:

Python Code


a = Animal()  # TypeError: Can't instantiate abstract class

ABCs enforce a structure and ensure certain methods are implemented.

8 isinstance() and issubclass()

These functions help check class relationships:

Python Code


print(isinstance(d, Dog))        # True
print(isinstance(d, Animal))     # True
print(issubclass(Dog, Animal))   # True
print(issubclass(Animal, Dog))   # False

Useful for type checking and enforcing structure in dynamic programs.

9 Inheritance Pitfalls

While inheritance is powerful, it can be misused:
  • 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.
Prefer composition when the relationship is not strictly hierarchical (i.e. “has-a” rather than “is-a”).

10 Real-World Example: A Role-Playing Game

Let’s model a simple RPG character system:

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?

Encapsulation is a fundamental principle of object-oriented programming in Python that involves bundling data and methods that operate on that data within a single unit, typically, a class. It also includes restricting direct access to some of the object’s components, which is a means of data hiding.

The main goals of encapsulation are:
  • 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.
Python supports encapsulation through conventions and special syntax, although it does not enforce access restrictions strictly as in languages like Java or C++.

2 Public, Protected, and Private Members

Python uses naming conventions to indicate the intended level of access for class attributes and methods.

1 Public Members

These are accessible from anywhere (inside or outside the class):

Python Code


class Car:
    def __init__(self, make):
        self.make = make  # public attribute

    def start(self):  # public method
        print(f"{self.make} is starting.")

You can access them directly:

Python Code


c = Car("Toyota")
print(c.make)  # Toyota
c.start()      # Toyota is starting.

2 Protected Members

These are indicated by a single underscore prefix and are intended to be internal, although not enforced:

Python Code


class Car:
    def __init__(self, make):
        self._engine_status = False  # protected attribute

    def _check_engine(self):  # protected method
        return self._engine_status

Conventionally, attributes and methods starting with _ are for internal use and not to be accessed directly from outside the class.

3 Private Members

Private members use a double underscore prefix and are name-mangled to make accidental access more difficult:

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.")


You cannot access them directly:

Python Code


c = Car("Honda")
# print(c.__make)          # AttributeError
# c.__start_engine()       # AttributeError

But they can still be accessed with name mangling:

Python Code


print(c._Car__make)        # Honda
c._Car__start_engine()     # Honda's engine started.

This demonstrates that private members in Python are not truly private, but they are discouraged from being accessed outside the class.

3 Getter and Setter Methods

To provide controlled access to private data, we use 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")

Usage:

Python Code


acc = BankAccount(1000)
print(acc.get_balance())  # 1000
acc.set_balance(1500)
print(acc.get_balance())  # 1500

This encapsulates the internal variable __balance, allowing access only through the approved interface.


4 Pythonic Way: Using Properties

Python provides a more elegant solution to getters and setters using the @property decorator.

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")

Usage:

Python Code


p = Product(50)
print(p.price)  # 50

p.price = 75
print(p.price)  # 75

# p.price = -20  # Raises ValueError

This allows attribute access as if it were a public attribute, but internally controls how it's read or modified.

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

Let’s model a secure email sender class:

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}")

Usage:

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

Here, we ensure that emails can only be sent when the object is in the correct internal state.


Polymorphism and Dynamic Behaviour in Python

1. What is Polymorphism?

Polymorphism is a core concept in object-oriented programming that means "many forms". It allows different classes to be treated as instances of the same parent class, particularly when they share a common interface (methods or behaviours).

In simpler terms, polymorphism enables different object types to respond to the same method call in their own unique way. This promotes code generalisation, scalability, and cleaner architecture.

2 Types of Polymorphism

In Python, polymorphism is implemented in several ways:
  1. Duck Typing (Dynamic Typing)
  2. Method Overriding
  3. Operator Overloading
  4. Polymorphism with Inheritance

3 Duck Typing in Python

Python follows a concept called duck typing:
“If it looks like a duck, swims like a duck, and quacks like a duck, it probably is a duck.”
This means Python doesn’t check for an object’s type to determine whether it can be used for a particular purpose. It checks for the presence of a method or attribute.

Example:

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

Here, both Duck and Dog have a sound() method. Python does not care what class the object belongs to—as long as the required method exists.

4 Polymorphism via Inheritance

This is the more traditional OOP form of polymorphism where subclasses override or extend parent class behaviours.

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

This design ensures that all animals speak in their own way, but they still adhere to a common contract—every subclass of Animal must implement speak().

5 Operator Overloading

In Python, operators like +, -, *, etc., can be overloaded for user-defined classes using special methods.

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})"

Usage:

Python Code


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

This is polymorphism because the + operator behaves differently depending on the object type.

6 Function and Method Overloading

Python does not support traditional function overloading (i.e., multiple functions with the same name but different arguments). However, similar behaviour can be mimicked using default arguments or variable-length arguments:

Python Code


def greet(name=None):
    if name:
        print(f"Hello, {name}!")
    else:
        print("Hello!")

greet("Alice")  # Hello, Alice!
greet()         # Hello!

This technique achieves a form of overloading by altering function behaviour based on input.

7 Real-World Example: Payment Systems

Imagine designing a payment system where different payment types behave differently:

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)

Usage:

Python Code


p1 = CreditCardPayment()
p2 = PayPalPayment()

process_payment(p1, 100)
process_payment(p2, 200)

Output:

Python Code


Paid £100 using credit card.
Paid £200 using PayPal.

This shows polymorphism where multiple classes provide their own implementation of a shared interface.

8 The Role of Abstract Base Classes

Python's abc module lets you define abstract classes, enforcing that subclasses implement specific methods. This adds structure and clarity to polymorphic systems.

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

Both Circle and Square must implement area() or they'll raise an error on instantiation.

9 Polymorphism in Built-in Functions

Python’s built-in functions like len() and str() exhibit polymorphism by working differently based on the type of object:

Python Code


print(len("hello"))     # 5
print(len([1, 2, 3]))    # 3
print(str(100))          # "100"
print(str([1, 2]))       # "[1, 2]"

These functions call internal methods like __len__() and __str__() that can be overridden in custom classes.


10 Benefits of Polymorphism

  1. Flexibility: Code becomes more general and adaptable.
  2. Reusability: Polymorphic code can work with any class that meets the expected interface.
  3. Clean Architecture: Promotes the use of abstract interfaces rather than concrete implementations.
  4. Extensibility: New types can be introduced without modifying existing code.

11 Limitations and Considerations

  1. Overuse of inheritance for polymorphism can create tightly coupled designs.
  2. Duck typing relies on runtime correctness—no compile-time guarantees.
  3. Ensure clear documentation of expected interfaces, especially in large projects.






#buttons=(Ok, Go it!) #days=(20)

Our website uses cookies to enhance your experience. Learn More
Ok, Go it!