Python classes and objects: a comprehensive guide (2024)

Learn about Object-Oriented Programming (OOP) in Python. This comprehensive guide covers everything from basic concepts to advanced techniques, with practical examples.

Content

Object-oriented programming (OOP) is a computer programming paradigm that tries to model real-world problems by organizing them as classes and objects. Its simplicity and effectiveness make it highly applicable - it's hard to think of any software application where OOP is not used. Considering their importance, I have specifically decided to cover them today in the context of Python. This is a comprehensive review with examples that can be helpful for beginners to the intermediate.

If you would like to learn more about using Python to scrape websites, please check out our 2024 step-by-step Python web scraping tutorial.

What are classes in Python?

Before going into the programming terminologies, I will begin with a simple example. We have some books in a bookshop. Some attributes can differ (or be similar), but one thing is the same: they are books and can be classified as a specific category.

In OOP, we identify similar objects and model them as a class. A class can then have the attributes shared by all the respective objects, like author, genre, edition, etc., for a book class.

Classes are not restricted to attributes only. We will come to this point shortly.

Why use classes?

Classes are helpful in several ways. First, it is always easier or more manageable to modify (or add/delete) attributes in a single class than change them across several objects.

There are plenty of other uses, too, but before we discuss them, we need to review the basic class structure.

Basic class structure

Let's create a class and see it in action. Defining the classes in Python is pretty straightforward - we specify the keyword class followed by the class’s name. For example, a Car class can be defined as:

class Car:
#Attributes

This “Car” is quite generic and needs some attributes.

class Car:
    model = "Toyota"
    make = "Corolla"
    colour = "Red"

Creating objects (instances)

A class is just a blueprint. To use an object, we need to instantiate it from a class. An object can be instantiated from the just-defined Car class as:

c = Car()

Since we didn’t specify any attribute value, c (attributes) will take just the default value specified in the class above. We can verify it:

print(c.colour)
#Red
print(c.make)
#Corolla

Usually, some attributes will take non-default values, so we can override them too.

c.colour = 'Black'

c.colour
#Black

Class methods and attributes

We mentioned earlier that a class is much more than mere attributes. Consider a Car and its functionalities: it may apply brakes, switch gears, turn, etc.

These functionalities can be covered using functions. Functions in a class can be defined similarly to those defined normally and specifically known as methods.

Using the same Car class above, we expand it by adding a couple of methods.

class Car:
    model = "Toyota"
    make = "Corolla"
    colour = "Red"

    def brake():
        print('Brakes are applied. Decelerating..')

    def accelerate():
        print('Car speed is increasing')

Similar to attributes, we can call methods from an object in a similar manner.

c = Car()
c.accelerate()

Unlike normal functions, this code will not work and will send an unfamiliar error. The reason is simple: whenever we call a method, Python automatically attaches the caller object (c in our case) as an argument to the method. This can be new for someone coming from a non-Python background.

To fix it, we specify the self attribute in every method of a class. The updated class will be:

class Car:
    model = "Toyota"
    make = "Corolla"
    colour = "Red"

    def brake(self):
        print('Brakes are applied. Decelerating..')

    def accelerate(self):
        print('Car speed is increasing')

Now, calling it won’t cause any issues and will have the expected outcome (i.e., printing that Car's speed is increasing).

Constructors

While instantiating an object, we saw a similar notation to calling a method (parentheses with the class’s name). Basically, that’s what happens: whenever we instantiate an object, a method is implicitly called. This method is known as a constructor.

The interpreter itself makes a constructor. If we want to define it explicitly, we can define it using the special keyword, __init__(). Like other methods, it also requires the self argument by default.

def __init__(self):
    print('Constructor is called')

We can use constructors to perform a desired functionality at the object’s creation, like specifying the value of some attribute, triggering the call of some method, and so on.

💡 While we can define a constructor with several arguments, it cannot be overloaded in Python.

Static methods

Let's add a method to check the model of a car.

def print_model(self):
        print(self.model)

Now, I'll make a couple of objects with separate models and call this method for both.

c1 = Car()
c1.print_model()

c2 = Car()
c2.model = 'Camry'
c2.print_model()

It will print the respective models for both. This is in contrast with the (too general) brake() and accelerate() methods we defined above.

The methods which execute exclusively for each object are known as instance methods, while those which are independent of the calling object are known as static methods. Static methods are used less frequently than the default instance methods but have their own applications in some cases (like we saw for the general functions above).

If static methods are independent of the calling object, why should we call them using an object?! Instead, a better approach should be to address them using the class’s name. However, if we try to call brake() using the class name, it won’t work. The error, which we encountered before, is demanding an object here.

The solution is quite simple: decorate the desired function using the @staticmethod decorator. Now, we can call this method easily from either the class name or the object’s name.

class Car:
    make = "Toyota"
    model = "Corolla"
    colour = "Red"

    def __init__(self):
        print('Constructor is called')

    @staticmethod
    def brake():
        print('Brakes are applied. Decelerating..')

    @staticmethod
    def accelerate():
        print('Car speed is increasing')

    def print_model(self):
        print(self.model)

And it can be called either way.

c2.brake()
Car.brake()

Inheritance

Now, our engineers are interested in making an electric car, so we will make another class.

class ElectricCar:
    make = None
    model = None
    horse_power = None
    registration_number = None
    cell_power = None

    def drive(self):
            print('Car '+self.registration_number+' is driving in environment-friendly manner.')

We can confirm it by instantiating it and calling the Drive() method.

ec1 = ElectricCar()
ec1.registration_number = 'AB-230'
ec1.drive()

While the class is created well and functioning in the desired manner, something is lacking. We can see that all the attributes in this class (except the fuel cell’s power) are attributes of any car. Similarly,drive() was also defined in the original Car class. We can see that both Car andElectricCar classes are related. But is there any way of relating them in OOP?

The answer, luckily, is yes. Inheritance is a concept that allows us to derive (or inherit) classes from existing classes. Although simple, inheritance is very powerful and is considered one of the pillars of OOP. We can inherit ElectricCar from Car as:

class Car:
    make = None
    model = None
    horse_power = None
    registration_number = None

    def drive(self):
        print('Car with registration number '+self.registration_number+' is driving.')

class ElectricCar(car):
    cell_power = None
💡 Since make , model , registration_number , cell_power are related to every car, these attributes have been taken into the Car class.

If we make an electric car object, it will display the same attributes as the original Car objects, plus allow us to access the additional cell_power, too.

ec1 = ElectricCar()
ec1.registration_number = 'AB-230'
ec1.cell_power = 12000

But calling the drive() method leads to calling the parent class (Car)’s method and needs to be updated.

Method overriding

To override the parent class’s driving behavior, we will override drive() in the ElectricCar class.

class ElectricCar(Car):
    cell_power = None

    def drive(self):
        print('Car ' + self.registration_number + ' is driving in environment-friendly manner.')

While apparently, little has changed. We have defined the samedrive() function again. But now there is something different: we can call the same drive() for any car instance, and it will call the respective method.

💡 As you can see, every car - whether it is a normal Car, ElectricCar or some other derived class- is a Car instance.

super() function

Most things have stayed the same. However, there is something different: we can call the same drive() for any car instance, and it will call the respective method. What if we want to call the parent class’s drive() method too?

We can call any base class method using the super(). Check the change in ElectricCar's drive() method after calling it.

class ElectricCar(Car):
    cell_power = None

    def drive(self):
        super().drive()
        print('Car is driving in environment-friendly manner.')

ec2 = ElectricCar()
ec2.registration_number = 'LH-239'
ec2.drive()

As you can see, the output will be from both the parent and derived classes.

Polymorphism

Now, let's pause the technical details and discuss the real world. How would you try to recognize a soccer player - say Zidane? Some will remember him as a midfielder, some as a captain, some as a Real Madrid player, and so on, but one thing will remain the same: he is a soccer player, and this is common for every soccer player - irrespective of playing position, league or any other class.

Let's translate it back to OOP: every object of a child class can also be treated as a base class’s object. Returning to our example, every car - whether a normal Car, ElectricCar or some other derived class like SportsCar- is a Car instance too.

class SportsCar(Car):

    def drive(self):
        print('Sports car is driving.')

Let's make some objects of all three types to see the point above.

c1 = Car()
sc1 = SportsCar()
sc2 = SportsCar()
# ec2 is defined in earlier code examples
ec3 = ElectricCar()
ec3.registration_number = 'CV-782'

Now, I put them in a list and call the drive(). By the way, ec2 was already defined (under the super() function discussion).

cars = [c1, sc1, sc2, ec2, ec3]  
for car in cars:
    car.drive()

💡 Car definition has been updated to add a default ‘XX-000’ as registration_number due to its requirement in the drive() method.

Here output shows that each object has been identified accordingly to the execution time, and the respective drive() method is called:

# Car with registration number XX-000 is driving.
# Sports car is driving.
# Sports car is driving.
# Car with registration number LH-239 is driving.
# Car is driving in environment-friendly manner.
# Car with registration number CV-782 is driving.
# Car is driving in environment-friendly manner.

This phenomenon is known as polymorphism where we can call a (parent class) function without caring much about the caller’s exact type, as this is something the interpreter will take care of.

Encapsulation

Encapsulation is the third pillar of OOP, along with inheritance and polymorphism. It means we can make class members (usually attributes) inaccessible outside the class by declaring them private.

In Python, however, private members don’t exist per se. We have name mangling, where we enclose the respective class members within double underscores.

Advanced concepts

OOP is a big topic. No wonder there are dedicated semester courses on it. It’s hard to cover in a single post. But now, I will try to cover some of the advanced concepts supported by Python.

Multiple inheritance

We can also derive a class from multiple classes. For example, I have a class UnmannedVehicle for automated vehicles - anything from a small Drone or robot to a self-driving bus.

class UnmannedVehicle:
    navigation_algorithm = "A*"
    camera_max_resolution = "1080p"

I want to make a class forSelfDrivingCar. Obviously, there are some things common between an UnmannedVehicle and SelfDrivingCar, but it's a Car too. So, in this case, we will inherit it from both classes:

class SelfDrivingCar(Car,UnmannedVehicle):
    license_region = 'Nevada'

Multiple inheritance is perfectly allowed in Python (some languages like C# or Java don’t allow it), and we can confirm it by accessing the respective parent class members:

sdc1 = SelfDrivingCar()
sdc1.registration_number = "SD-102"
print(sdc1.navigation_algorithm)
print(sdc1.registration_number)

Abstract classes

Sometimes, we want a class to implement some methods from a base class but want to leave their implementation up to those classes. In such a case, there’s no need to have the method’s implementation in the base class. For example, we don’t want to care about how a car switches its gears - should it show it on the LED or not, etc. So we can simply make the switch_gear() an abstract method (i,e., no implementation at all) and leave it up to the respective cars (derived classes) to implement it as per their requirements. An abstract method is specified by the @abstractmethod decorator.

Any class with an abstract method is an abstract class (it may have non-abstract methods, too), and since it is more for inheritance, it cannot have an object (that’s perfectly intuitive). Abstract classes were introduced much later in Python, but rightly so. They are defined in the ABC (Abstract Base Classes) module. We can inherit from this class to declare abstract classes.

from abc import ABC, abstractmethod

class GearSystem(ABC):
    @abstractmethod
    def change_gear(self):
        pass
💡 As you can see, the naming of this module is a bit confusing. abc is the module, and ABC is the class.

Using this class, I define another class, AutomaticGearingCar which inherits from both Car and GearSystem.

class AutomaticGearingCar(Car, GearSystem):
    def change_gear(self):
        print('Automatically switching the gears')

TL;DR: Abstract classes are sort of a contract that needs to be fulfilled. You can derive from as many abstract (applies true to non-abstract too) classes as you would like, but failing to override/implement any of their abstract methods will result in the derived class’s abstraction - we will not be able to instantiate it.

How to import classes in Python

In the above example, we saw the ABC class being imported. Can we do the same for any self-defined class? Yes! It is much easier than it appears.

To import a class, either place it in the current directory or refer to its path. For example, I create a Python filesample_module.py and define some classes in it.

Now, any class within the module can easily accessed as:

from sample_module import class_a

Practical examples of using classes

Let's round this off with a practical example of a library system. To make a library system, inevitably we will begin with the Book class.

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author
        self.issued = False

Then, we will add the issue and return methods:

    def issue_book(self):
        if not self.issued:
            self.issued= True

    def return_book(self):
        if self.issued:
            self.issued=False

Now, we will create the library class. A library is meant to keep books, so we will create a list of books as its member attribute. This is an interesting case where a collection of other classes’ objects is a member attribute of another class (and something we will often see in other practical examples, like having a deck of cards for a card game, a collection of different objects in an arcade game, assets for a firm, and so on).

Further, it will have methods for adding, removing or finding a certain book.

💡 To make things simpler, try to modify this code to include ISBN and perform searches accordingly.
class Library:
    def __init__(self):
        self.books = []

    def add_book(self, book):
        self.books.append(book)

    def remove_book(self, title):
        for book in self.books:
            if book.title == title:
                self.books.remove(book)
                return f"{title} was succesfully removed from the library."
        return "Book not found."

    def find_book(self, keyword):
        for book in self.books:
            if keyword in book.title:
                return book.title
        return "Book not found."

    def list_books(self):
        if not self.books:
            return "No books in the library."
        for book in self.books:
            print(book.title)

Our humble library is now ready, and we can add, delete, or find books. Here’s a sample testing.

library = Library()

book1 = Book("Great Expectations", "Charles Dickens")
book2 = Book("The Count of Monte Cristo", "Alexandar Dumas")

library.add_book(book1)
library.add_book(book2)

library.list_books() 

library.find_book('Count')
library.find_book('Jane') #No books found

Putting OOP into practice

Object-oriented Programming (OOP) is a powerful yet simple paradigm for programming in almost all the major languages. While Python doesn’t support all the OOP features (like encapsulation or method overloading), it’s still powerful enough for a number of applications. This article could go on and on, but we have kept it concise while covering the majority of OOP in Python. Obviously, it doesn’t conclude here and needs to be complimented with practical examples at your end. To recap the blog, here are some questions:

If MobilePhone is a class, will Samsung be its object or a child class?
What will happen if two classes (B and C) derive from the same class A and then class D derives from B and C?
Why don’t we study the “virtual functions” specifically in Python?
Can a class or its object be a part of another class?
Talha Irfan
Talha Irfan
I love reading Russian Classics, History, Cricket, Nature, Philosophy, and Science (especially Physics)— a lifelong learner. My goal is to learn and facilitate the learners.

Get started now

Step up your web scraping and automation