An Introduction to Object-Oriented Programming In Python

Muhammad Usman
9 min readDec 12, 2022

--

This essay is intended for someone learning the Python language who has yet to learn much about the Object-Oriented Programming (OOP) paradigm. The goal is to explain OOP and its basic principles in a way that is easy to understand for the beginner Python programmer.

Before getting into the industry, you should understand OOP well because it is the most common way to write code and gives developers powerful tools to make high-quality software quickly. After reading this essay, the reader should be able to understand what OOP is and how it is implemented in Python.

Advantages of OOP:

If you’re unfamiliar with the concept of OOP, you’ll miss out on its many advantages, such as code reuse and improved scalability.
OOP also makes code much easier to maintain, as it facilitates the separation of concerns and encourages modularity, which helps with readability.

Fundamentals of OOP:

Understanding the core fundamentals of OOP, such as Object, Class, Method, Encapsulation, Inheritance, Abstraction, and Polymorphism, is essential.

So let’s get started.

What is OOP?

As its name suggests, Object-Oriented Programming (OOP) is the programming approach that models real-world objects and their interactions.
In layman’s terms, the tasks of an application are broken down in OOP and then modeled as objects that interact with one another. Because of this, the code can be easily organized, maintained, and modified to meet the application's changing needs over time without much effort or rewriting of the code.

Object:

It’s common knowledge among Python programmers that everything in Python is an object. An object has three attributes:
1. identity
2. type
3. value.
The term “identity” refers to the address in memory, “type” refers to the class to which it belongs, and “value” refers to the value of the object. Let’s look at this by creating an object:

name= 'Muhammad Usman'

print('Value of object: ', name)
print('Type of object: ', type(name))
print('Identity of object: ', id(name))

This indicates that an object will always be a member of some class. In addition, each time we make a new object, we create a new instance of the corresponding Class. In the above example, “Str” is the Class, and “Muhammad Usman” is the instance of that Class.

Class:

Class is the blueprint for defining how the objects will behave. The class contains two types of information about the object:

  1. Structural information:
    It is the description of the attributes of the object. We kept this information private in the class and could access it using getters and setters.
  2. Behavioral Information:
    Functions or methods associated with the object that control its behavior. This information is often available to the public and can be accessed by calling the function with the appropriate parameters.

Methods Vs Functions:

Functions are not necessary to be written in class. It can be defined anywhere in the program and called upon when needed. A method, on the other hand, is a function defined within a class and can only be accessed by the objects that belong to that class.
The second difference is how we call them.

# Creating an Object of Class List
lst= [1, 2, 3]

# Function - To count the elements in lst, we can use the "len" function
len(lst)

# Method - To append an element in lst, we can use the "append" method that is the property of Class List
lst.append(4)

Constructor:

When an object is instantiated from the class, the code in the class constructor will be executed automatically. This ensures that any initial data or setup required by the object can be taken care of before it is used.
In Python, the constructor is the __init__ method.

# Creating Class 
class Dog:

# Constructuor of Dog Class
def __init__(self):
# Instance Variables
self.name= 'Brain'
self.breed= 'White Labrador Retriever'

# Method
def bark(self):
print (f'{self.name} says whip and its breed is {self.breed}')

# Function
def length(strng):
return len(strng)

# Object of the Class
pet= Dog()
pet.bark() # Calling method
length(pet.name) # Calling function

Special/Magic/Dunder Methods:

Always use double underscores (__) at the beginning and end of the names of special methods. Because these methods are pre-defined, we cannot build new versions of them or define existing ones. Moreover, the interpreter automatically triggers these methods whenever a code is being executed; thus, objects do not have any control over them. Furthermore, the interpreter will show an error message if we try to use the same name for a method and a variable.
If we simply print the object of the above class, we get the following output: <__main__.Dog object at ……>

We need to tell the class how we want to see our object. We’ll use the __str__ magic method.

# special/magic/dunder method
def __str__(self):
return f''' The value of instance variable name is {self.name}
and value of instance variable breed is {self.breed}
'''

Add this method inside the Dog Class and rerun the print statement. The Python interpreter will search for this class and understands how to represent our object.

Instance & Static Variables:

The value of static variables is the same across all objects. However, that is not the case for instance variables. The value of instance variables is or can be different for all objects.

class Dog:
# Static or Class Variables
legs = 4

# Constructuor of Atm Class
def __init__(self, name, breed):
# Instance Variables
self.name= name
self.breed= breed

Static or Class variables are always defined below the class, while instance variables are defined in the constructor.
We’ll call the static variables using the class name shown in the above code. Now, let’s create the two different instances of the above class and print them.

As you can notice, the value of instance variables is different for both objects, which is not valid for static variables.

Instance Vs Static Methods:

Because static methods work on the characteristics of the class rather than the object, we do not need to supply the self-argument to them. Nevertheless, we have to write decorator @staticmethod above these functions. On the other hand, instance variables are not subject to this limitation.

class Dog:

# Instance Method
def bark(self):
print (f'{self.name} says whip and its breed is {self.breed}')

# Static Method
@staticmethod
def num_of_legs():
print(f"I've {Dog.legs} legs")

Encapsulation:

The general idea of encapsulation is simple. Encapsulation allows us to hide specific information (either structural or behavioral) within an object or class, limiting the information shown to the outside world. Thus, it helps us to maintain data integrity by ensuring that data is only accessed and manipulated in pre-defined ways. In addition, it limits access to the information that has been secured by methods known as getter and setter methods.
In python, a double underscore at the beginning of the name is used to hide it. Let’s assume we want to secure the following information:
Static variable — weakness and instance variable — owner’s name.

class Dog:
# Static or Class Variables
__weakness= "Quagmire's dad"

# Constructuor of Atm Class
def __init__(self):
# Instance Variables
self.__owner = 'Peter'

# Getter method for static variable
@staticmethod
def get_weakness():
return Dog.__weakness

# Setter method for static variable
@staticmethod
def set_weakness(new):
if type(new) == str:
Dog.__weakness= 'Lois body'
else:
print('Cannot change the weakness')

Now create the object of this class. You’ll notice that you can’t get access to the secured information, but you can still change it via the getter and setter method. That’s the main idea of encapsulation.

Relationship Between Classes:

Relationships between classes exist in two ways:

  1. Association (Has-A)
  2. Inheritance (Is-A)

Association:

An association relationship between two classes is a “has a” relationship. For example:

A Car has an Engine, and the Wheels
A Book has Pages

Association connections may be divided into two types: composition and aggregation.

1. Composition

In this type of connection, a child class cannot exist without a parent class.
Consider the Book and Page classes as an example. Without the book, the pages cannot exist; even if they did, they would have no significance. Similarly, the pages will also get destroyed if the book is destroyed.

2. Aggregation

In aggregation, it is possible for the child class to exist independently of the parent class.
Take, for instance, the Car and the Engine. The engine does not always have to be destroyed when the car is destroyed.

Inheritance

In object-oriented programming, inheritance is a powerful idea that lets one class's properties and behaviors be used by another class. This makes it easier to reuse code. The new class formed from an existing class is called the “derived class or child class,” while the existing class from which the new class was derived is called the “base class or parent class.”

It also helps minimize the code's complexity by allowing objects with similar behaviors to be grouped together and reused. This, in turn, reduces the amount of code that has to be written and maintained, which is a huge benefit.

# Base Class
class Animal():

# Construnctor
def __init__(self):
print ("Animal Created")

def who_am_i(self):
print ("I'm an animal")

def eat(self):
print ("I'm eating")

# Child Class
class Dog(Animal):

# Method Overloading. To overwrite the base method, define the method again (make sure to match the name).
def who_am_i(self):
print ("I'am a dog")

# To add a new method, add new method. Simple!
def bark(self):
print("WOOF!")

pet= Dog()

Now the pet object of the Dog class has all three methods, i.e., who_am_i, eat and bark.

Inheritance is further of 4 types:

  1. Single Inheritance: A child class inherits from one parent class.
  2. Multiple Inheritance: When a child class inherits from multiple parent classes.
  3. Multilevel Inheritance: A child class inherits properties from its parent class, which inherits properties from another parent class.
  4. Hierarchical Inheritance: More than one child class inherits properties from one parent class.

Polymorphism:

Polymorphism has three concepts in it:

  1. Method Overriding
  2. Method Overloading
  3. Operator Overloading

Method Overriding:

When you have a method in a Parent Class and a Child Class with the same name, and when an object of the Child Class calls that method, it will execute the method present in the Child Class. This is a method overriding in Python.
In the Inheritance example, call the who_am_i method and see the output.

Method Overloading:

Method overloading is the ability of a function to behave in different ways. This behavior allows the same function to handle various tasks, reducing code complexity and improving readability.

class Geomerty:

def area(self, a, b= 0):
if b == 0:
return f'Area of Circle: {3.14 * a * a}'
else:
return f'Area of rectangle: {a * b}'

Take the above code as an example. You can calculate the area of a circle and a rectangle via a single function.

Operator Overloading:

It’s like overloading methods but with operators instead.

Abstraction:

The process by which data and functions are defined in such a way that only essential details can be seen and unnecessary implementations are hidden is called Data Abstraction.
These are specifically defined to lay a foundation for other classes that exhibit common behavior or characteristics. An abstract class serves as a template for other classes by defining a list of methods the classes must implement.

I really hope that you now have a decent understanding of OOPs as well as their fundamental principles. This essay will not turn you into an expert in OOP, but it will provide you with solid direction toward gaining that understanding. With the basics in hand, you can now explore the features of Object-Oriented Programming more deeply.

Thank you for reading! :)

--

--