Object-oriented Programming in Python: An Introduction

    Lorenzo Bonannella
    Lorenzo Bonannella
    Share
    In this article, we’ll dig into object-oriented programming (OOP) in Python. We won’t go too deeply into the theoretical aspects of OOP. The main goal here is to demonstrate how we can use the object-oriented paradigm with Python.
    According to Statista, Python is the fourth most used programming language among developers. Why is that? Well, some say that it’s because of Python’s simplified syntax; others say it’s because of Python’s versatility. Whatever the reason is, if we want to study a trending programming language, Python should be one of our choices. Contents:
    1. The Fundamentals of OOP
    2. Classes and Objects
    3. Defining a New Method
    4. Access Modifiers: Public, Protected and Private
    5. Inheritance
    6. Polymorphism
    7. Method Overloading
    8. Method Overriding

    Key Takeaways

    1. Python’s Versatility in OOP: Python, being the fourth most used programming language, excels in implementing Object-Oriented Programming (OOP) principles, thanks to its simplified syntax and versatile nature. This article focuses on demonstrating Python’s capabilities in OOP without delving too deep into theoretical aspects, making it a practical guide for developers.
    2. Classes and Objects as Core Concepts: At the heart of OOP in Python are classes and objects. The article introduces these concepts with straightforward examples, illustrating how classes serve as blueprints for objects and how objects are instances of these classes, complete with attributes and methods to model real-world entities effectively.
    3. Advanced OOP Features in Python: Through concise examples, the article explores advanced OOP features such as inheritance, polymorphism, method overloading, and overriding. These concepts are crucial for writing efficient and reusable code, emphasizing Python’s power in facilitating complex application development with OOP.

    The Fundamentals of OOP

    Let’s start with a gentle summary of object-oriented programming. Object-oriented programming is a programming paradigm — a group of ideas that set a standard for how things must be done. The idea behind OOP is to model a system using objects. An object is a component of our system of interest, and it usually has a specific purpose and behavior. Each object contains methods and data. Methods
    are procedures that perform actions on data. Methods might require some parameters as arguments. Java, C++, C#, Go, and Swift are all examples of object-oriented programming languages. The implementation of the OOP principles in all of those languages is different, of course. Every language has its syntax, and in this article, we’ll see how Python implements the object-oriented paradigm. To learn more about OOP in general, it’s worth reading this article from MDN, or this interesting discussion about why OOP is so widespread.

    Classes and Objects

    The first important concept of OOP is the definition of an object. Let’s say you have two dogs, called Max and Pax. What do they have in common? They are dogs and they represent the idea of a dog. Even if they are of a different breed or color, they still are dogs. In this example, we can model Max and Pax as objects or, in other words, as instances of a dog. But wait, what is a dog? How can I model the idea of a dog? Using classes. Diagram of a class and two objects As we can see in the picture above, a class is a template that defines the data and the behavior. Then, starting from the template provided by the class, we create the objects. Objects are instances of the class. Let’s have a look at this Python code:
    class Dog():
      def __init__(self, name, breed):
        self.name = name
        self.breed = breed
    
      def __repr__(self):
        return f"Dog(name={self.name}, breed={self.breed})"
    
    max = Dog("Max", "Golden Retriever")
    pax = Dog("Pax", "Labrador")
    print(max)
    print(pax)
    
    On line 1, we declare a new class using the name Dog. Then we bump into a method called __init__. Every Python class has this, because it’s the default constructor. This method is used to initialize the object’s state, so it assigns values to the variables of the newly created object. As arguments of the constructor, we have the name, the breed
    , and a special keyword called self. It’s not a coincidence that this is the first argument of the method. Inside the class code, the self keyword represents the current instance of the class. This means that each time we want to access a certain method or variable that belongs to an instance of the class (max or pax are two different instances), we must use the self
    keyword. Don’t worry if it’s not completely clear now; it will be in the next sections. Look at the first line of the __init__ method — self.name = name. In words, this says to the Python interpreter: “Okay, this object that we’re creating will have a name (self.name), and this name is inside the name argument”. The same thing happens for the breed
    argument. Okay, so we could have stopped here. This is the basic blueprint used to define a class. Before jumping to the execution of this snippet, let’s look at the method that was added after the __init__. The second method is called __repr__. In Python, the __repr__ method represents the class object as a string. Usually, if we don’t explicitly define it, Python implements it in its own way, and we’ll now see the difference. By default, if we don’t explicitly define a __repr__ method, when calling the function print()
    or str(), Python will return the memory pointer of the object. Not quite human-readable. Instead, if we define a custom __repr__ method, we have a nice version of our object in a stringed fashion, which can also be used to construct the object again. Let’s make a change to the code above:
    class Dog:
      def __init__(self, name, breed):
        self.name = name
        self.breed = breed
    
    max = Dog("Max", "Golden Retriever")
    pax = Dog("Max", "Golden Retriever")
    # Default (internal) implementation of __repr__
    print(max)
    print(pax)
    print(max == pax)
    
    If we save and run this code, this is what we get:
    <__main__.Dog object at 0x0000026BD792CF08>
    <__main__.Dog object at 0x0000026BD792CFC8>
    False
    
    Wait, how can it be possible that they aren’t two equal dogs, if they have the same name and the same breed? Let’s visualize it using the diagram we made before. Diagram of a class and two objects showing the memory addresses
    First, when we execute print(max), Python will see that there’s no custom definition of a __repr__ method, and it will use the default implementation of the __repr__ method. The two objects, max and pax
    , are two different objects. Yes, they have the same name and the same breed, but they’re different instances of the class Dog. In fact, they point to different memory locations, as we can see from the first two lines of the output. This fact is crucial for understanding the difference between an object and a class. If we now execute the first code example, we can see the difference in the output when we implement a custom __repr__ method:
    Dog(name=Max, breed=Golden Retriever)
    Dog(name=Pax, breed=Labrador)
    

    Defining a New Method

    Let’s say we want to get the name of the max
    object. Since in this case the name attribute is public, we can simply get it by accessing the attribute using max.name. But what if we want to return a nickname for the object? Well, in that case, we create a method called get_nickname() inside our class. Then, outside the definition of the class, we simply call the method with max.get_nickname():
    class Dog:
      def __init__(self, name, breed):
        self.name = name
        self.breed = breed
    
      def get_nickname(self):
        return f"{self.name}, the {self.breed}"
    
      def __repr__(self):
        return f"Dog(name={self.name}, breed={self.breed})"
    
    max = Dog("Max", "Golden Retriever")
    pax = Dog("Pax", "Labrador")
    
    print(max.name)
    print(max.get_nickname())
    
    If we run this snippet, we get the following output:
    > python snippet.py
    Max
    Max, the Golden Retriever
    

    Access Modifiers: Public, Protected and Private

    Let’s now consider access modifiers. In OOP languages, access modifiers are keywords used to set the accessibility of classes, methods or attributes. It’s a different situation in C++ and Java, where access modifiers are explicit keywords defined by the language. In Python, there’s no such thing. Access modifiers in Python are a convention rather than a guarantee over access control. Let’s look at what this means with a code sample:
    class BankAccount:
      def __init__(self, number, openingDate):
        # public access
        self.number = number
        # protected access
        self._openingDate = openingDate
        # private access
        self.__deposit = 0
    
    In this snippet, we create a class called BankAccount. Any new BankAccount object must have three attributes: a number, an opening date and an initial deposit set to 0. Notice the single underscore (_) before openingDate and the double underscore (__
    ) before deposit. Great! According to Python’s convention, the single underscore is used as a prefix for protected members, while the double underscore is for private members. What does this mean in practice? Let’s try to add the code below under the class definition:
    account = BankAccount("ABXX", "01/01/2022")
    print(account.number)
    print(account._openingDate)
    print(account.__deposit)
    
    If we try to execute this code, we’ll get something like this:
    > python snippet.py
    ABXX
    01/01/2022
    Traceback (most recent call last):
      File "snippet.py", line 14, in <module>
        print(account.__deposit)
    AttributeError: 'BankAccount' object has no attribute '__deposit'
    
    We can print the account number because it’s a public attribute. We can print the openingDate, even if, according to the convention, it’s not advised. We can’t print the deposit. In the case of the deposit attribute, the proper way to read or modify its value should be through get() and set()
    methods. Let’s see an example of this:
    class BankAccount:
      def __init__(self, number, openingDate):
        self.number = number
        self._openingDate = openingDate
        self.__deposit = 0
    
      def getDeposit(self):
        return self.__deposit
    
      def setDeposit(self, deposit):
        self.__deposit = deposit
        return True
    
    account = BankAccount("ABXX", "01/01/2022")
    print(account.getDeposit())
    print(account.setDeposit(100))
    print(account.getDeposit())
    
    In the code above, we define two new methods. The first one is called getDeposit, and the second one is setDeposit. As their names imply, they’re used to get or set the deposit. It’s a convention in OOP to create get and set
    methods for all of the attributes that need to be read or modified. So, instead of directly accessing them from outside the class, we implement methods to do that. As we can easily guess, executing this code gives the following output:
    > python snippet.py
    0
    True
    100
    

    Inheritance

    DRY. Don’t repeat yourself. Object-oriented programming encourages the DRY principle, and inheritance is one of the strategies used to enforce the DRY principle. In this section, we’ll see how inheritance works in Python. Please note that we’ll use the terms parent class and child class
    . Other aliases might include base class for the parent and derived class for the children. Since inheritance defines a hierarchy of classes, it’s pretty convenient to differentiate between the parent and all the children. Okay, so let’s start with an example. Let’s say we want to model a classroom. A classroom is made by a professor and a number of students. What do they all have in common? What relationship do they all share? Well, they’re certainly all humans. As such, they share a certain number of features. For simplicity here, we define a class Person as having two private attributes, name and surname. This class also contains the get() and set() methods. The image below shows a parent class and two children. Image diagram showing a parent class and two children As we can see, in both Student
    and Professor classes we have all the methods and attributes defined for the Person class, because they inherit them from Person. Additionally, there are other attributes and methods highlighted in bold that are specific to the child class. Here’s the code for this example:
    class Person:
      def __init__(self, name, surname):
        self.__name = name
        self.__surname = surname
    
      def getName(self):
        return self.__name
    
      def getSurname(self):
        return self.__surname
    
      def setName(self, newName):
        self.__name = newName
    
      def setSurname(self, newSurname):
        self.__surname = newSurname
    
    Then, we have two entities to model, the Student
    and the Professor. There’s no need to define all the things we define above in the Person class for Student and Professor also. Python allows us to make the Student
    and the Professor class inherit a bunch of features from the Person class (parent). Here’s how we can do that:
    class Student(Person):
      def __init__(self, name, surname, grade):
        super().__init__(name, surname)
        self.__grade = grade
    
      def getGrade(self):
        return self.__grade
    
      def setGrade(self, newGrade):
        self.__grade = newGrade
    
    In the first line, we define a class using the usual class Student()
    syntax, but inside the parentheses we put Person. This tells the Python interpreter that this is a new class called Student that inherits attributes and methods from a parent class called Person. To differentiate this class a bit, there’s an additional attribute called grade. This attribute represents the grade the student is attending. The same thing happens for the Professor
    class:
    class Professor(Person):
      def __init__(self, name, surname, teachings):
        super().__init__(name,surname)
        self.__teachings = teachings
    
      def getTeachings(self):
        return self.__teachings
    
      def setTeachings(self, newTeachings):
        self.__teachings = newTeachings
    
    There’s a new element we haven’t seen before. On line 3 of the snippet above, there’s a strange function called super().__init__(name,surname). The super() function in Python is used to give the child access to members of a parent class. In this case, we’re calling the __init__ method of the class Person
    .

    Polymorphism

    The example introduced above shows a powerful idea. Objects can inherit behaviors and data from other objects in their hierarchy. The Student and Professor classes were both subclasses of the Person
    class. The idea of polymorphism, as the word says, is to allow objects to have many shapes. Polymorphism is a pattern used in OOP languages in which classes have different functionalities while sharing the same interface. Speaking of the example above, if we say that a Person object can have many shapes, we mean that it can be a Student, a Professor
    or whatever class we create as a subclass of Person. Let’s see some other interesting things about polymorphism:
    class Vehicle:
      def __init__(self, brand, color):
        self.brand = brand
        self.color = color
    
      def __repr__(self):
        return f"{self.__class__.__name__}(brand={self.brand}, color={self.color})"
    
    class Car(Vehicle):
      pass
    
    tractor = Vehicle("John Deere", "green")
    red_ferrari = Car("Ferrari", "red")
    
    print(tractor)
    print(red_ferrari)
    
    So, let’s have a look. We define a class Vehicle. Then, we create another class called Car as a subclass of Vehicle
    . Nothing new here. To test this code, we create two different objects and store them in two separate variables called tractor and red_ferrari. Note here that the class Car doesn’t have anything inside. It’s just defined as a different class, but till now it has had no different behavior from its parent. Don’t bother about what’s inside the __repr__ method for now, as we’ll come back to it later. Can you guess the output of this code snippet? Well, the output is the following:
    Vehicle(brand=John Deere, color=green)
    Car(brand=Ferrari, color=red)
    
    Note the magic happening here. The __repr__ method is defined inside the Vehicle class. Any instance of Car will adopt it, since Car is a subclass of Vehicle
    . But Car doesn’t define a custom implementation of __repr__. It’s the same as its parent. So the question here is why the behavior is different. Why does the print show two different things? The reason is that, at runtime, the Python interpreter recognizes that the class of red_ferrari is Car. self.__class__.__name__
    will give the name of the class of an object, which in this case is the self object. But remember, we have two different objects here, created from two different classes. If we want to check whether an object is an instance of a certain class, we could use the following functions:
    print(isinstance(tractor, Vehicle)) # Yes, tractor is a Vehicle object!
    print(isinstance(tractor, Car)) # No, tractor is only a Vehicle object. Not a Car object.
    
    On the first line, we’re asking the following question: is tractor an instance of the class Vehicle? On the second line, we’re instead asking: is tractor
    an instance of the class Car?

    Method Overloading

    In Python, like in any other OOP language, we can call the same method in different ways — for example, with a different number of parameters. That might be useful when we want to design a default behavior but don’t want to prevent the user from customizing it. Let’s see an example:
    class Overloading:
      def sayHello(self, i=1):
        for times in range(i):
          print("Nice to meet you!")
    
    a = Overloading()
    print("Running a.sayHello():")
    a.sayHello()
    print("Running a.sayHello(5):")
    a.sayHello(5)
    
    Here, we define a method called sayHello. This method has only one argument, which is i
    . By default, i has a value of 1. In the code above, when we call a.sayHello for the first time without passing any argument, i will assume its default value. The second time, we instead pass 5 as a parameter. This means i=5. What is the expected behavior then? This is the expected output:
    > python snippet.py
    Running a.sayHello():
    Nice to meet you!
    Running a.sayHello(5):
    Nice to meet you!
    Nice to meet you!
    Nice to meet you!
    Nice to meet you!
    Nice to meet you!
    
    The first call to a.sayHello() will print the message "Nice to meet you!" only once. The second call to a.sayHello() will print "Nice to meet you!" five times.

    Method Overriding

    Method overriding happens when we have a method with the same name defined both in the parent and in the child class. In this case, we say that the child is doing method overriding. Basically, it can be demonstrated as shown below. The following diagram shows a child class overriding a method. Diagram showing a child class overriding a method The sayHello() method in Student
    is overriding the sayHello() method of the parent class. To show this idea in practice, we can modify a bit the snippet we introduced at the beginning of this article:
    class Person:
      def __init__(self, name, surname):
        self.name = name
        self.surname = surname
    
      def sayHello(self):
        return ("Hello, my name is {} and I am a person".format(self.name))
    
    class Student(Person):
      def __init__(self, name, surname, grade):
        super().__init__(name,surname)
        self.grade = grade
    
      def sayHello(self):
        return ("Hello, my name is {} and I am a student".format(self.name))
    
    a = Person("john", "doe")
    b = Student("joseph", "doe", "8th")
    print(a.sayHello())
    print(b.sayHello())
    
    In this example, we have the method sayHello(), which is defined in both classes. The Student implementation of sayHello()
    is different, though, because the student says hello in another way. This approach is flexible, because the parent is exposing not only an interface but also a form of the default behavior of sayHello, while still allowing the children to modify it according to their needs. If we run the code above, this is the output we get:
    > python snippet.py
    Hello, my name is john and I am a person
    Hello, my name is joseph and I am a student
    

    Conclusion

    By now, the basics of OOP in Python should be pretty clear. In this article, we saw how to create classes and how to instantiate them. We addressed how to create attributes and methods with different visibility criteria. We also discovered fundamental properties of OOP languages like inheritance and polymorphism, and most importantly how to use them in Python.

    Frequently Asked Questions (FAQs) about Python Object-Oriented Programming (OOP)

    What is the significance of the

    init method in Python OOP?

    The init method in Python is a special method that is automatically called when an object of a class is created. It is also known as a constructor. The primary use of this method is to initialize the attributes of the class. For instance, if we have a class ‘Car’ with attributes like ‘color’ and ‘model’, we can use the init method to assign values to these attributes at the time of object creation.

    How does inheritance work in Python OOP?

    Inheritance is a fundamental concept in OOP that allows one class to inherit the attributes and methods of another class. In Python, a class that inherits from another class is called a subclass, and the class it inherits from is called a superclass. Inheritance is used to promote code reusability and to represent real-world relationships well.

    What is the difference between a class and an object in Python OOP?

    A class in Python is a blueprint for creating objects. It defines a set of attributes and methods that characterize any object of the class. On the other hand, an object is an instance of a class. It is a specific realization of the class, with actual values assigned to the attributes defined in the class.

    What is the concept of encapsulation in Python OOP?

    Encapsulation is one of the fundamental concepts in OOP. It refers to the bundling of data (attributes) and methods that manipulate the data into a single unit, which is called a class. In Python, encapsulation is used to restrict access to methods and variables, preventing data from direct modification which is also known as data hiding.

    How does polymorphism work in Python OOP?

    Polymorphism is a concept in OOP that allows a single interface to represent different forms. In Python, polymorphism allows us to define methods in the child class with the same name as defined in their parent class. As a result, we can perform a single action in different ways. For instance, you can have a ‘draw’ method in both the ‘Rectangle’ and ‘Circle’ classes, but the method would perform differently in each class.

    What is the role of self in Python OOP?

    In Python, ‘self’ is a convention used in instance methods to refer to the object on which the method is being called. It’s not a keyword; any name can be used, but ‘self’ is widely accepted and understood by the community. It’s the first parameter in any instance method, and Python passes the object to the method using this parameter.

    What is the concept of abstraction in Python OOP?

    Abstraction in OOP is the process of hiding the complex details and showing only the essential features of the object. In Python, abstraction can be achieved by using abstract classes and interfaces. An abstract class is a class that contains one or more abstract methods, which are methods declared but not implemented.

    How can we create a class in Python?

    In Python, we can create a class using the ‘class’ keyword followed by the name of the class. The name of the class usually starts with a capital letter. The attributes and methods of the class are defined in an indented block under the class definition.

    What is method overriding in Python OOP?

    Method overriding is a feature of OOP that allows a subclass to provide a different implementation of a method that is already defined in its superclass. It is used when the method inherited from the superclass doesn’t fit well with the subclass.

    What is the concept of multiple inheritance in Python OOP?

    Multiple inheritance is a feature in OOP where a class can inherit from more than one superclass. Python supports multiple inheritance by allowing the subclass to state multiple superclasses in its definition. The subclass then inherits the attributes and methods of all the superclasses.