OOP stands for Object Oriented Programming. Like most modern programming languages, Python is also an object oriented language, in fact, it is a characteristic feature of the language. This might be a little tricky to understand for beginners, but I'll try my best. Buckle up! Because this is gonna be a bit challenging. If you are not confident enough, you can practice the previous lessons to get hold of the syntax.
Everything in Python is an object, and I mean EVERYTHING (almost).
Now, when you create a variable called
fruit: str = "Apple", an apple won't be produced in the
computer, obviously. They are treated as objects in the sense that
they have certain attributes/properties or qualities which describe
that particular object. This is the core of Python and is the
underlying mechanism of everything you can do in Python.
Then, we have classes. Classes are blueprints which are like frameworks for different types of objects. If 'Car' is a class, it will have certain properties such as brand, model name, colour, seats, fuel capacity, mileage or range etc.. And every single car is an object; not "Mahindra Thar is an object", "every individual car is an object": if it is a Mahindra Thar, then it's brand will be Mahindra and model will be Thar. I hope you understand and this analogy will be used often in this course. Each car is an "instance" of the class Car. So, objects are instances of a particular class.
You might have noticed the word 'class' in result of the
type() function:
>>> print(type(777))
< class 'int' >
Yes! 777 is an instance of the class int.
That's what I meant by "everything in Python is an object". That
"Apple" you created was an object of type string; meaning all the
data types are classes! Even functions are objects of the class
function(). Now let's see how we can create our own
classes.
Classes are defined using the class key word. Every
class must have the __init__ method in them. Speaking
of methods, objects have not only attributes, but also methods which
are defined in the respective class. Essentially, methods are
functions specific to that object; they act in some way on the
object itself. If you convert 45 into a string with
str(45), that string function is a method of class int.
The .lower() function is also a method, of type str.
(Note the dot notation of the method)
class Car: # A class with the name Car is defined
def __init__(self, brand: str, model: str, year: int, colour: str): #The parameters necessary to declare all the attributes
self.brand: str = brand #The attribute self.brand is declared and its value is set to the argument brand
self.model: str = model #The attribute self.model is declared and its value is set to the argument model
self.year: int = year #The attribute self.year is declared and its value is set to the argument year
self.colour: str = colour #The attribute self.colour is declared and its value is set to the argument colour
# Creating an instance of the class Car
my_car: Car = Car("Tesla", "S Plaid", 2025, "Black")
print(my_car)
print(type(my_car))
# As you might have seen in the type hint,
# 'Car' is now its own type!
# The object my_car is of the type Car
print(my_car.model)
# You can access specific attributes with the 'object.attribute' syntax
# You can also change attributes with this syntax
my_car.brand = "Tesla"
my_car.model = "S Plaid"
print(my_car.model)
Now, you must be asking, what the heck is self? Self is
the parameter that refers to a particular instance of a class. In
the above example, 'Car' is a class and 'my_car' is an instance of
that class, as you must have understood by now. Every instance can
have different values for an attribute. E.g.,
neighbour_car can have the brand 'Tata'. The
self parameter just tells the method or the attribute
to refer to that instance. When you printed my_car.model, the my_car
object was passed to the self parameter automatically and it
returned the model of my_car. If you had another car called
neighbour_car, and you asked for neighbour_car.model, it would
return something like Tata; because self would then become
neighbour_car. A simple way to think about it is that a specific
object replaces the self keyword. You don't need to
pass it when calling a method or accessing an attribute, it is
automatically passed. The first parameter is always self, and it is
just a convention to call it 'self'; you can call it anything but I
recommend not to.
Apart from attributes, classes can also have methods, which are just
class-specific functions. The __init__() function is
also a method which runs when an instance of the class is created or
initialised. Let's see an example:
from datetime import datetime
# I will not include the complete context every time
# The following function should be placed under the class after __init__()
def age(self) -> int:
return datetime.now().year - self.year
# Gets the current year and subtracts the purchase year of the car
print(my_car.age(), "years")
We just created a method called age() which returns the age of the car in years. Here is another example to make you understand the concept:
# A Fruit class with an __init__() method
# and a price() method
class Fruit:
def __init__(self, name: str, calories: int, rate: float) -> None:
self.name: str = name
self.calories: int = calories
self.rate: float = rate
def price(self, weight) -> int:
return self.rate * weight # Returns weight times rate
apple: Fruit = Fruit("Apple", 530, 150.00)
banana: Fruit = Fruit("Banana", 800, 70.00)
print("₹", apple.price(2)) # Price of 2 kilos of apples
print("₹", banana.price(5)) # Price of 5 kilos of apples
There are special methods called as 'dunder methods' which is short
for 'Double Underscore methods'. They are also called as magic
methods in Python because they provide most of the functionality to
the language. There are numerous dunder methods which you can look
up on the Internet. You will be surprised to know even basic
arithmetic operations like addition and multiplication are also
dunder methods. Integer and float objects have a method called
__add__ which called when the + operator is used. Here
is an example:
# A money class that has attributes amount and currency
class Money:
def __init__(self, amount: float, currency: str):
self.amount: float = amount
self.currency: str = currency
# Checks whether the currency is the same and returns a new Money object
def __add__(self, other):
if self.currency != other.currency:
raise ValueError("Can't add amounts with different currencies.")
# Oh yes, you can raise exceptions on-demand!
return Money(self.amount + other.amount, self.currency)
balance: Money = Money(35000, "INR")
deposit: Money = Money(6000, "INR")
balance = balance + deposit
# The + operator calls the __add__ method
# The object to the right is passed as the second argument, 'other'
print(balance.amount)
# You can directly call the add method as well
deposit: Money = Money(2500, "INR")
balance = balance.__add__(deposit)
print(balance.amount)
# Create another object of a different currency and try to use the + operator
One of the most commonly used dunder method is the __str__() method. Without this, when you print a custom object, it just says that it is an object of a certain type; but nothing useful. This method/function is manually defined to output a user-friendly text. This method is automatically called when you print the object or convert it into text. For the Car class used earlier, this can be a string method:
def __str__(self):
return f"A/An {self.colour} {self.brand} {self.model}, purchased in {self.year}."
# You can now print the car objects or convert them to text
Just like properties are inherited from generation to generation in real life, attributes and methods can be inherited from one class to another. More specifically, we can create what are 'child classes' which have the attributes and methods of the parent class, as well as some of their own. And example will clarify it:
class Person:
def __init__(self, name: str, age: int):
self.name: str = name
self.age: int = age
def introduce(self):
print(f"Hello, my name is {self.name} and I am {self.age} years old.")
class Student(Person): # The Person class is passed as a parameter to the Student class
# The Student class inherits the attributes and methods of the Person class
def __init__(self, name: str, age: int, standard: int, roll_no: int):
super().__init__(name, age) # Calls the __init__ method of the Person class
self.standard: int = standard # The Student class specific attribute
self.roll_no: int = roll_no
def introduce(self): # This method overrides the introduce() method of the Person class
print(f"Hello, my name is {self.name}. I am {self.age} years old and I am studying in standard {self.standard}")
class Teacher(Person):
def __init__(self, name: str, age: int, subject: str):
super().__init__(name, age)
self.subject: str = subject
# Though the introduce() method is not defined here, it is inherited and can be called
def teach(self): # A method unique to the Teacher class
return f"[Teaching {self.subject}...]"
In the above example, we first created a Person class with
attributes
name & age and a method
introduce(). We then created two child classes, Student
and Teacher. Both the Student and the Teacher are a Person and they
inherit the attributes and methods of the parent class, Person. When
we defined the __init__() method of the child class and
provided some parameters, the parameters of the parent class were
actually overridden. But instead of declaring the attributes again,
we use the super() function to refer to the parent
class and call its initialisation method. After that, we can declare
the child class-specific attributes or methods. Similar to
attributes, methods will also be overridden if defined again. Let us
create some objects.
me: Person = Person("S S Shetty", 25)
alexander: Student = Student("Alexander The Great", 2381, 13, 001)
aristotle: Teacher = Teacher("Aristotle", 2410, "Philosophy")
me.introduce()
alexander.introduce()
aristotle.introduce()
print(aristotle.teach())
Try and analyse the output and figure out which methods are being called. Also, find out the types of the three objects to see how they created
from dataclasses import dataclass
@dataclass
class Car:
brand: str
model: str
year: int
colour: str
my_car: Car = Car("Tesla", "S Plaid", 2025, "Black")
print(my_car)
That's it! The above code is equivalent to the Car class we
created earlier, but with less boilerplate code. And some dunder
methods (like
__str__, __eq__, etc.)are
automatically created. Although you can still define other
methods, it is not generally done.