Object-oriented programming (OOP) concepts in Python with practical examples.
Introduction
What is OOP?
Object-oriented programming refers to a programming practice that represents real world entities like an employee or a product, and their attributes as objects. This ensures that the data and attributes associated with these entities are easily manipulated, accessed and optimized.
It also involves the breakdown of an app, software or coding process into smaller re-usable parts. Each part is then treated as a separate object. OOP is useful to write cleaner and reusable code.
OOP is useful when building a software with a lot of parts to it. For example, building a brick game with Python turtle library. A brick game has a ball, multiple bricks, a paddle and a scoreboard. The best approach is to divide the project into different parts, whereby the ball, bricks and paddle are treated as separate objects.
The re-usability of code in OOP comes in handy when you need to build another project that requires a paddle or a ball-bouncing logic. You can easily go back to your brick game project, get the ball object and its code with little or no customization for your new project.
Tutorial Objective
This tutorial is for anyone who is new to OOP principles in Python. It is a beginner-friendly guide that will help you understand the concept of OOP in Python and how to use it in your project. In this tutorial, you will learn the following:
Class keyword in Python
Objects and instances
Attributes and methods
Inheritances in object-oriented programming
Encapsulation in object-oriented programming
Polymorphism in object-oriented programming
Requirements
Python basics: You need a basic knowledge of Python programming concepts such as loops, variables, functions and data types.
Code Interpreter: A code interpreter or IDE like Pycharm, VS Code, etc. with Python3+ installed on your computer.
OOP in Python
The class keyword.
The class
keyword is used to imitate the OOP concept in Python. It is used to declare a Python object that represents a real word entity. This object is often referred to as a Python class.
The Python class serves as a blueprint for creating multiple variations of the entity it represents. Each of these variations are known as instances, and sometimes referred to as objects.
A Python class can be used to create custom data types, and organize code into a more structured and reusable form.
Example of a class
class Product:
pass
As seen in the code above, the class
keyword is followed by the class name - Product
. Generally, class names are written in camel case with examples like SoftwareDeveloper
, TechnicalWriter
, etc. However, your code will still run and function correctly if you do otherwise.
Python objects and instances
An object in Python refers to anything that has an attribute and a method or function. A string is an object in Python. For example, the word good is a string and is also an object because it has attributes and methods such as: .capitalize()
, .upper()
, .lower()
, etc. for different operations. Below are other examples
# Converts the string to uppercase
print(variable.upper())
>>> GOOD
# converts the first character to uppercase it
print(variable.capitalize())
>>> Good
Instance, on the other hand, refers to an occurrence of a Python class. An instance uses the attributes and methods declared within a class. The example below shows you how to create an instance from a class
class Product:
pass
bag = Product()
In the example above, the bag
variable is an instance of the Product
class.
Attributes and Methods
What are attributes?
Attributes refer to the features or unique qualities of an instance or a class.
Types of Attributes
There are two types of attributes in Python OOP. They include the following:
Class attributes
This refers to the general attributes that are set on a class. They are global attributes that can be accessed by each instance of the class. They are used in cases where all instances of a class have an attribute in common.
The Product
class can have attributes such as form
(solid or liquid), type
(digital or physical), size
(large or small). These kinds of attributes are called class attributes.
class Product:
product_type = "physical"
bag = Product()
chair = Product()
# Using the bag instance
print(bag.product_type)
>>> "physical"
# Using the chair instance
print(chair.product_type)
>>> "physical"
# Using the Product class itself
print(Product.product_type)
>>> "physical"
In the code snippet above, the product_type
attribute is added to the Product
class. The product_type
remains the same for every instance of the Product
class that is created.
The Product
class can also access the attribute directly without an instance. Attributes are accessed using any of these format — <instance_name>.<attribute>
or <class_name>.<attribute>
Instance attributes
This refers to the unique attributes of each instance of a class. These attributes are specific to each instance and can be modified only by the instance itself. Below is an example of how instance attributes are used:
class Product:
def __init__(self, colour, quantity):
self.colour = colour
self.quantity = quantity
radio = Product("black", 3)
bag = Product("blue", 10)
print(radio.colour)
>>> "black"
print(radio.quantity)
>>> 3
print(bag.colour)
>>> "blue"
print(bag.quantity)
>>> 10
In the snippet above, the two instances of the Product class — radio
and bag
have a colour and quantity attribute. Each instance has a unique value for their attribute.
The __init__()
function is used to create an instance attribute within a class. This function runs immediately after an instance is created. This helps to declare the instance attributes for quick access. It also accepts positional and keyword arguments which act as the unique attribute of each instance.
The self
argument refers to an instance of the class. It is a compulsory argument that must be included. An instance attribute is set by using the self.<attribute>
syntax.
What is a method?
A method is a function or action that can be called by a class or an instance. Methods are defined within a class and operate on the data or attributes of that class. They allow you to perform actions specific to an instance of a class or the class itself.
Types of methods
In Python, there are four types of methods within a class. They include:
Class methods
Instance methods
Static methods
Special methods (Dunder methods).
This guide will only cover the first two types of methods (class and instance) while we the rest are briefly discussed.
Class methods
This type of method operates on the class itself rather than on each instance. Like a class attribute, it is also a global method. They are used for operations that are common to each instance of a class. Below is an example of a class method:
class Trader:
@classmethod
def sell(cls):
print("A Trader sold an item")
trader = Trader()
trader.sell()
>>> "A Trader sold an item"
Trader.sell()
>>> "A Trader sold an item"
In the example above, the Trader
class has a method named sell()
— considering that all traders sell an item.
The @classmethod
decorator is used to create a class method as seen in the code snippet above. A class method takes cls
as a compulsory argument which refers to the class itself. This method can be called by either an instance of the class or the Trader
class itself.
Instance methods
This refers to methods that are specific and unique to each instance of a class. Unlike the class methods, instance methods operate on the unique attributes of each instance. The class method does not have access to the instance attributes and therefore cannot perform unique functions.
While some traders sell an item at the normal price, some can decide to offer a discount on theirs. The value of the new price is subject to each trader's percentage discount.
Below is an example of an instance method:
class Trader:
def __init__(self, name):
self.name = name
self.selling_price = 500
@classmethod
def sell(cls):
print("A Trader sold an item")
def offer_discount(self):
# At 20% discount
discount = 0.2 * self.selling_price
self.selling_price = self.selling_price - discount
print(f"{self.name} offers a discount")
trader = Trader("John")
trader.offer_discount()
>>> "John offers a discount"
# New selling price after discount
print(trader.selling_price)
>>> 400
In the example above, there is an extra method named offer_discount()
. This method performs a unique function on each instance of the Trader
class. It prints out the name of the trader and their new selling price. They also take self
as their first parameter allowing them to access and modify the instance's attributes. If you create another instance of the Trader class, their selling price will remain 500. Only John's price differs because he offered a 20% discount.
Static methods
Static methods are defined using the @staticmethod
decorator. They don't take an initial argument like class and instance methods. As for the Trader class, a static method can be useful for trade calculations, but does not depend on the attributes or state of a trader instance.
Special methods
These methods have double underscores (__) at the beginning and end of their names, such as __init__()
, __str__()
, __add__()
, etc. They define how instances of a class should behave in specific situations. For example, the __init__()
is called when an instance is created, and __str__()
defines how the object should be represented as a string (str).
Inheritance in OOP
Definition
Inheritance in OOP refers to a condition where a class inherits from another class. Inheritance is often used when a class needs a method from another class with which it is closely related or shares similarities. The inherited class is called the parent class, while the new class is called child class.
How it works
Let's assume we have a Trader
class with attributes such as name
, age
, daily_income
, monthly_income
and products
, and a sell()
method. Inheritance becomes useful when we need to create other subdivisions of the Trader class like Wholesaler
or Retailer
. To reduce ambiguity and duplication of code, we let these subdivisions inherit from the attributes and methods of the Trader class, rather than creating them from scratch.
Inheritance examples
Below is a code snippet showing how inheritance is implemented:
class Trader:
def __init__(self, name, daily_income, monthly_income, products):
self.daily_income = daily_income
self.name = name
self.monthly_income = monthly_income
self.products = products
def sell(self):
print(f"{self.name} sold a product")
class Wholesaler(Trader):
pass
class Retailer(Trader):
pass
max_products = ["Wipes", "Food ingredients", "Soap", "Baby kits", "Groceries"]
wholesaler_max = Wholesaler("Max", 100, "3000", max_products)
wholesaler_max.sell()
>>> "Max sold a product"
print(wholesaler_max.name)
>>> "Max"
print(f"${wholesaler_max.daily_income}")
>>> "$100"
In the code above, there are 3 classes — Trader
, Wholesaler
, Retailer
. The Wholesaler and Retailer are child classes that inherit from the Trader class. Therefore, they both have access to the Trader
class' methods and __init__()
function.
You do not need to create a new __init__()
and sell()
function for the Wholesaler
and Retailer
class.
Below is another example:
class Professional:
def __init__(self, name, age, discipline, experience, salary):
self.name = name
self.age = age
self.discipline = discipline
self.experience = experience
self.salary = salary
def work(self):
print(f"{self.name} is working")
class Lawyer(Professional):
pass
class Doctor(Professional):
pass
class Engineer(Professional):
pass
lawyer = Lawyer("Jude", 25, "Law", "2 years", 10000)
lawyer.work()
>>> "Jude is working"
print(lawyer.experience)
>>> "2 years"
In the code above, the Professional
class refers to all professional workers. It has several attributes and a work method which is common to professional workers. The Lawyer
, Engineer
and Doctor
class inherit from the Professional
class.
With inheritance, you can add more professional workers without setting it up from the scratch. They only need to inherit from the Professional
class to get started.
Add extra attributes and methods
This section will help you add extra attributes and methods to a child class.
Add extra attributes to a child class
In some cases, you may need to add extra attributes to a child class. For example, you can add a num_cases_solved
attribute to the Lawyer
class as seen below:
class Professional:
def __init__(self, name, age, discipline, experience, salary):
self.name = name
self.age = age
self.discipline = discipline
self.experience = experience
self.salary = salary
class Lawyer(Professional):
def __init__(self, name, age, discipline, experience, salary, num_cases_solved):
super().__init__(name, age, discipline, experience, salary)
self.num_cases_solved = num_cases_solved
lawyer = Lawyer("Jude", 25, "Law", "2 years", 10000, 50)
print(f"{lawyer.num_cases_solved} cases solved")
>>> "50 cases solved"
In the example above, the super()
method represents the parent class. The super().__init__(*args, **kwargs)
method initializes the parent's class and its attributes.
Add extra methods to a child class
The code below shows you how to add extra methods to a child class:
class Professional:
def __init__(self, name, age, discipline, experience, salary):
self.name = name
self.age = age
self.discipline = discipline
self.experience = experience
self.salary = salary
def work(self):
print(f"{self.name} is working")
class Lawyer(Professional):
def __init__(self, name, age, discipline, experience, salary):
super().__init__(name, age, discipline, experience, salary)
self.num_cases_solved = 0
# an extra method
def solve_a_case(self):
self.num_cases_solved += 1
print(f"{self.name} solved a case")
emiloju = Lawyer("Emiloju", 25, "Law", "2 years", 2000)
# call the extra method
emiloju.solve_a_case()
>>> "Emiloju solved a case"
emiloju.num_cases_solved
>>> 1
Encapsulation
Definition
Encapsulation means putting something in a small container or enclosing something in a capsule. In OOP, encapsulation is a concept that involves hiding the data or attributes of a class from being modified or accessed directly in a code. The primary goal of encapsulation is to hide the internal details and state of an object while providing controlled access to that object/instance's data and behaviour.
For example, in the Professional
class, professionals should not be able to set salaries themselves. Encapsulation is used to implement this.
Encapsulation examples
Study the code below:
class Professional:
def __init__(self, name):
self.name = name
self.salary = 500
def double_salary(self):
return self.salary * 2
professional = Professional("Emiloju")
professional.salary = 5000
print(f"${professional.salary}")
>>> "$5000"
In the code above, a professional cannot pass in the salary
directly, but they can still change the salary’s value. The same thing applies to every other attribute.
Encapsulation allows you to keep an attribute private by adding a double underscore (__) as prefix. Below is an example:
class Professional:
def __init__(self, name):
self.name = name
self.__salary = 500
def double_salary(self):
new_salary = self.__salary * 2
return f"New salary is ${new_salary}"
professional = Professional("Rilwan")
print(professional.__salary)
>>> "AttributeError"
In the example above, the salary attribute is prefixed with a double underscore (__). You will get an AttributeError
when you try to access this attribute. It is only available within the class. In the same vein, if you try to modify it outside the class, it will not change the value of the salary. See the example below:
professional.__salary = 2000
print(professional.double_salary())
>>> "New salary is $1000"
Polymorphism
Definition
Polymorphism in Python and object-oriented programming (OOP) refers to the ability of different objects to respond to the same method or function in a way that is appropriate for their specific type or class. In Python polymorphism is often achieved through method overriding and inheritance. Here's a simple example below:
Example
class Animal:
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof"
class Cat(Animal):
def speak(self):
return "Meow!"
def animal_sound(animal):
return animal.speak()
dog = Dog()
cat = Cat()
print(animal_sound(dog))
>>> "Woof"
print(animal_sound(cat))
>>> "Meow!"
In this example, both the Dog
and Cat
classes inherit from the Animal
class and override its speak()
method. When the animal_sound()
function is called with the two types of animals, polymorphism allows it to produce the appropriate sound based on the specific class of the object passed to it.
Conclusion
Object-Oriented Programming (OOP) in Python allows you to build more organized, modular, and scalable applications. By focusing on core concepts like classes, inheritance, encapsulation, and polymorphism, you can design software that’s easier to debug, maintain, and expand.
Whether you're building small scripts or complex applications, OOP principles provide a robust framework that allows you to tackle larger programming challenges effectively. Experiment with different designs, build projects, and refine your skills to unlock the full potential of this programming concept.
Thanks for reading to the end. Before you leave, please don't forget to like, share and follow. See you on the next one.
Subscribe to my newsletter
Read articles from Edun Omobolanle directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Edun Omobolanle
Edun Omobolanle
Hi, I'm Rilwan, a software developer who is passionate about building and sharing my craft with people. My ultimate goal is to help newbies in tech understand complex tech-related topics in the simplest possible way.