
8 Object-Oriented Programming

Python is a versatile programming language that supports object-oriented programming (OOP). In Python, everything is an object, and built-in data types are implemented as classes.
8.1 Classes
8.1.1 Built-in Data Types in Python
In Python, every built-in data type is implemented as a class. This includes:
int
float
str
list
tuple
dict
set
NoneType
You can confirm this by using the type()
function or checking an object’s __class__
attribute:
print(type(42))
print(type(3.14))
print(type("hello"))
print(type([1, 2, 3]))
print(type(None))
<class 'int'>
<class 'float'>
<class 'str'>
<class 'list'>
<class 'NoneType'>
# Checking the __class__ attribute
= 42
x print(x.__class__)
<class 'int'>
8.1.2 Understanding Classes in Python
A class is a blueprint for creating objects. It defines the attributes (characteristics) and methods (behaviors) that its objects will have.
For example, consider a Cat
class:
- Attributes: Characteristics shared by all cats, such as
breed
,fur_color
, andage
. - Methods: Actions that a cat can perform, such as
meow()
,run()
, orsleep()
.
For more details, refer to the official Python documentation on classes.
8.1.3 Creating your own classes
Until now, we have worked with built-in Python classes like int
, list
, and dict
. However, in many cases, we need to create our own classes to model real-world entities in a structured way.
Defining your own classes provides several key benefits in programming:
- Encapsulation – Organize data and related functionality together.
- Reusability – Code can be reused by creating multiple instances of the class.
- Abstraction – Hide unnecessary details and expose only the required functionality.
- Inheritance – Reuse existing class behavior in new classes, avoiding redundancy.
When we create a new class, we actually create a new type. Now, we are going to create our own type, which we can use in a way that is similar to the built-in types.
Let’s start with the Car class:
class Car:
def __init__(self, brand, color, speed=0):
self.brand = brand # Attribute
self.color = color # Attribute
self.speed = speed # Attribute
def accelerate(self, increment):
"""Increase the car's speed."""
self.speed += increment
return f"{self.brand} is now moving at {self.speed} mph."
def brake(self, decrement):
"""Decrease the car's speed."""
self.speed = max(0, self.speed - decrement)
return f"{self.brand} slowed down to {self.speed} mph."
We’ll use the example above to explain the following terms:
The
class
statement: We use theclass
statement to create a class. The Python style guide recommends to use CamelCase for class names.The constructor (or the
__init__()
method): A class typically has a method called__init__
. This method is called a constructor and is automatically called when an object or instance of the class is created. The constructor initializes the attributes of the class. In the above example, the constructor accepts Three values as arguments, and initializes its attributesbrand
andcolor
with those values and have a default value forspeed
.The
self
argument: This is the first parameter of instance methods in a class. It represents the instance of the class itself, allowing access to its attributes and methods. When referring to instance attributes or methods within the class, they must be prefixed with self. The purpose of self is to distinguish instance-specific attributes and methods from local variables or other functions in the program.
This example demonstrates how a class encapsulates attributes and behaviors. The Car
class defines three attributes: brand
, color
, and speed
, along with a constructor (__init__()
) and two methods: accelerate()
and brake()
. This structure makes it easy to create multiple car objects and manipulate their states independently.
8.2 Objects
In Python, an object is an instance of a class. A class acts as a blueprint, defining the structure and behavior that its objects will have. Each object has its own attributes (data) and can perform methods (functions associated with the class). Compared to the class (which is just a blueprint), an object is a concrete and tangible entity that exists in memory.
In Python, when you create a variable and assign a value to it, Python internally creates an instance of the corresponding class. Every value in Python is an object,
= 10 # x ia an insance of int class
x = "Hello" # y is an instance of str class
y = [1, 2, 3] # z is an instance of list class
z
print(type(x))
print(type(y))
print(type(z))
<class 'int'>
<class 'str'>
<class 'list'>
Once we define a class as a blueprint, we can create instances of that class to generate objects of its type. In fact, we can create as many objects as we want from a single class, each with its own unique data while sharing the same structure and behavior defined in the class.
Let’s create two objects of the Car
class we defined earlier
To create an object or instance of the class Car
, we’ll use the class name with the values to be passed as argument to the constructor for initializing the object / instance.
# Creating objects (instances of the Car class)
= Car("Toyota", "Red")
car1 = Car("Honda", "Blue")
car2
print(type(car1))
print(type(car2))
<class '__main__.Car'>
<class '__main__.Car'>
Instance: An instance is a specific realization of the object of a particular class. Creating an instance of a class is called Instantiation. Here a particular car is an instance of the class Car. Similarly, in the example above, the object
x
is an instance of the class integer. The words object and instance are often used interchangeably.
The attributes of an instance can be accessed using the .
operator with the object name
print(car1.brand)
print(car2.brand)
print(car1.color)
print(car2.color)
print(car1.speed)
print(car2.speed)
Toyota
Honda
Red
Blue
0
0
What happens if the instance variable doesn’t exist?
car1.engine
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[8], line 1 ----> 1 car1.engine AttributeError: 'Car' object has no attribute 'engine'
Methods are functions inside a class that operate on instance attributes. To call a method use:
print(car1.accelerate(20))
print(car2.brake(10))
Toyota is now moving at 20 mph.
Honda slowed down to 0 mph.
Unlike attributes, methods require parentheses ()
because they need to be executed, just like the functions we learned earlier
A list of all attributes and methods associated with an object can be obtained with the dir()
function. Ignore the ones with underscores - these are used by Python itself. The rest of them can be used to perform operations.
dir(car1)
['__class__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__getstate__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__',
'accelerate',
'brake',
'brand',
'color',
'speed']
Filtering out only user-defined attributes and methods
print([attr for attr in dir(car1) if not attr.startswith('__')])
['accelerate', 'brake', 'brand', 'color', 'speed']
8.2.1 Example: A class that analyzes a string
Let us create a class that analyzes a string.
class AnalyzeString:
#Constructor
def __init__(self, s):
= s.lower()
s self.words = s.split()
#This method counts the numebr of words
def number_of_words(self):
return (len(self.words))
#This method counts the number of words starting with the string s
def starts_with(self,s):
return len([x for x in self.words if x[:len(s)]==s])
#This method counts the number of words of length n
def words_with_length(self,n):
return len([x for x in self.words if len(x)==n])
#This method returns the frequency of the word w
def word_frequency(self,w):
return self.words.count(w)
Let us create an instance of the class AnalyzeString()
to analyze a sentence.
#Defining a string
= 'This sentence in an example of a string that we will analyse using a class we have defined' sentence
#Creating an instance of class AnalyzeString()
= AnalyzeString(sentence) sentence_analysis
#The attribute 'word' contains the list of words in the sentence
sentence_analysis.words
['this',
'sentence',
'in',
'an',
'example',
'of',
'a',
'string',
'that',
'we',
'will',
'analyse',
'using',
'a',
'class',
'we',
'have',
'defined']
#The method 'word_frequncy()' provides the frequency of a word in the sentence
'we') sentence_analysis.word_frequency(
2
#The method 'starts_with()' provides the frequency of number of words starting with a particular string
'th') sentence_analysis.starts_with(
2
8.2.2 Practice exercise 1
Write a class called PasswordManager
. The class should have a list called old_passwords
that holds all of the user’s past passwords. The last item of the list is the user’s current password. There should be a method called get_password
that returns the current password and a method called set_password
that sets the user’s password. The set_password
method should only change the password if the attempted password is different from all the user’s past passwords. It should either print ‘Password changed successfully!’, or ‘Old password cannot be reused, try again.’ Finally, create a method called is_correct
that receives a string and returns a boolean True
or False
depending on whether the string is equal to the current password or not.
To initialize the object of the class, use the list below.
After defining the class:
Check the attribute
old_passwords
Check the method
get_password()
Try re-setting the password to ‘ibiza1972’, and then check the current password.
Try re-setting the password to ‘oktoberfest2022’, and then check the current password.
Check the
is_correct()
method
class PasswordManager:
def __init__(self, initial_passwords):
self.old_passwords = initial_passwords
def get_password(self):
"""Returns the current password (last item in the old_passwords list)."""
return self.old_passwords[-1]
def set_password(self, new_password):
"""Sets a new password only if it has not been used before."""
if new_password in self.old_passwords:
print("Old password cannot be reused, try again.")
else:
self.old_passwords.append(new_password)
print("Password changed successfully!")
def is_correct(self, password):
"""Checks if the provided password matches the current password."""
return password == self.get_password()
# Initialize PasswordManager with given passwords
= ["alpha123", "beta456", "gamma789", "delta321", "ibiza1972"]
initial_passwords = PasswordManager(initial_passwords)
password_manager
# 1. Check the attribute old_passwords
print("Old passwords:", password_manager.old_passwords)
# 2. Check the method get_password()
print("Current password:", password_manager.get_password())
# 3. Try re-setting the password to 'ibiza1972'
"ibiza1972")
password_manager.set_password(print("Current password after attempt:", password_manager.get_password())
# 4. Try re-setting the password to 'oktoberfest2022'
"oktoberfest2022")
password_manager.set_password(print("Current password after attempt:", password_manager.get_password())
# 5. Check the is_correct() method
print("Is 'oktoberfest2022' correct?", password_manager.is_correct("oktoberfest2022"))
print("Is 'wrongpassword' correct?", password_manager.is_correct("wrongpassword"))
Old passwords: ['alpha123', 'beta456', 'gamma789', 'delta321', 'ibiza1972']
Current password: ibiza1972
Old password cannot be reused, try again.
Current password after attempt: ibiza1972
Password changed successfully!
Current password after attempt: oktoberfest2022
Is 'oktoberfest2022' correct? True
Is 'wrongpassword' correct? False
8.3 Class Constructors
A constructor is a special method in a class that is automatically called when an object is created.
- In Python, the constructor method is named
__init__()
.
- It initializes object attributes when an instance is created.
class Car:
def __init__(self, brand, color, speed=0):
"""Constructor to initialize Car attributes"""
self.brand = brand
self.color = color
self.speed = speed # Default value is 0
# Creating instances (objects)
= Car("Toyota", "Red", 50)
car1 = Car("Honda", "Blue") # speed uses default value
car2
# Accessing attributes
print(car1.brand, car1.color, car1.speed) # Toyota Red 50
print(car2.brand, car2.color, car2.speed)
Toyota Red 50
Honda Blue 0
8.3.1 Default Constructor (No Parameters)
If a class does not explicitly define a constructor, Python automatically provides a default constructor. This constructor only includes self and takes no additional parameters.
Let’s create an empty class to demonstrate this:
# Define a class Circle that doesn't have any attributes or methods
class Circle:
pass
# Create an instance of the Circle class
= Circle()
c print(type(c))
<class '__main__.Circle'>
8.3.2 Using Default Values in Constructors
You can set default values for parameters to make them optional.
class Student:
def __init__(self, name, grade="Not Assigned"):
self.name = name
self.grade = grade # Default: "Not Assigned"
= Student("John", "A") # Assigned grade
s1 = Student("Emma") # Uses default grade
s2
print(s1.name, s1.grade) # John A
print(s2.name, s2.grade) # Emma Not Assigned
John A
Emma Not Assigned
8.4 Difference Between Instance Attributes and Class Attributes in Python (OOP)
In Python object-oriented programming, attributes can be defined at two levels:
- Instance Attributes → Specific to each instance of the class.
- Class Attributes → Shared across all instances of the class.
8.4.1 Instance Attributes (Defined in __init__
)
- Defined inside the constructor (
__init__
) usingself
, and accessed usingself.attribute_name
- Each instance has its own copy of instance attributes.
- Changes to an instance attribute affect only that instance.
# Example: Instance attributes using our Car class
class Car:
def __init__(self, brand, color):
self.brand = brand # Instance attribute
self.color = color # Instance attribute
# Creating instances
= Car("Toyota", "Red")
car1 = Car("Honda", "Blue")
car2
# Each instance has different values
print(car1.brand) # Toyota
print(car2.brand) # Honda
# Changing an instance attribute only affects that instance
= "Black"
car1.color print(car1.color) # Black
print(car2.color)
Toyota
Honda
Black
Blue
8.4.2 Class Attributes ( Defined Outside __init__
)
- Defined at the class level (outside
__init__
). - Shared across all instances of the class.
- Changing a class attribute affects all instances (unless overridden at the instance level).
class Car:
= 4 # Class attribute (shared by all instances)
wheels
def __init__(self, brand):
self.brand = brand # Instance attribute
# Creating instances
= Car("Toyota")
car1 = Car("Honda")
car2
# Accessing class attribute
print(car1.wheels) # 4
print(car2.wheels) # 4
# Changing the class attribute affects all instances
= 6
Car.wheels print(car1.wheels) # 6
print(car2.wheels) # 6
4
4
6
6
Note: Just like attributes, methods can be categorized into instance methods and class methods. So far, everything we have defined are instance methods. Class methods, however, are beyond the scope of this data science course and will not be covered.
8.5 Inheritance in Python
8.5.1 What is Inheritance?
Inheritance is a fundamental concept in Object-Oriented Programming (OOP) that allows a child class to inherit attributes and methods from a parent class. This promotes code reuse and hierarchical structuring of classes.
Here are Key Benefits of Inheritance:
- Code Reusability – Avoids redundant code by reusing existing functionality.
- Extensibility – Allows adding new functionality without modifying the original class.
- Improves Maintainability – Easier to manage and update related classes.
8.5.2 Defining a Parent (Base) and Child (Derived) Class
A child class inherits from a parent class by specifying the parent class name in parentheses.
Example: Basic Inheritance
# Parent Class (Base Class)
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "Some sound"
# Child Class (Derived Class)
class Dog(Animal): # Inheriting from Animal
def speak(self):
return "Woof!"
# Creating objects
= Dog("Buddy")
dog1
# Accessing inherited attributes and methods
print(dog1.name)
print(dog1.speak())
Buddy
Woof!
Explanation:
Dog
inherits fromAnimal
, meaning it gets all the properties ofAnimal
.- The
Dog
class overrides thespeak()
method to provide a specialized behavior.
8.5.3 The super()
Function
The super()
function allows calling methods from the parent class inside the child class.
Example: Using super()
to Extend Parent Behavior
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "Some sound"
class Cat(Animal):
def __init__(self, name, color):
super().__init__(name) # Calling Parent Constructor
self.color = color # Additional attribute in Child Class
def speak(self):
return "Meow!"
# Creating an instance
= Cat("Whiskers", "Gray")
cat1
print(cat1.name) # Whiskers (Inherited from Animal)
print(cat1.color) # Gray (Defined in Cat)
print(cat1.speak()) # Meow! (Overridden method)
Whiskers
Gray
Meow!
super().__init__(name)
ensures the parent class constructor is properly called.- This allows the child class to initialize both inherited and new attributes.
8.5.4 Method Overriding in Inheritance
- If a method exists in both the parent and child class, the child class’s method overrides the parent’s method.
- This is useful for customizing behavior.
Example: Overriding a Method
class Parent:
def show(self):
return "This is the Parent class"
class Child(Parent):
def show(self): # Overriding method
return "This is the Child class"
= Child()
obj print(obj.show()) # This is the Child class
This is the Child class
8.5.5 Check Relationship
issubclass(Child, Parent)
→ Checks if a class is a subclass of another.isinstance(object, Class)
→ Checks if an object is an instance of a class.
class Animal:
pass
class Dog(Animal):
pass
= Dog()
dog1
print(issubclass(Dog, Animal)) # True
print(isinstance(dog1, Dog)) # True
print(isinstance(dog1, Animal)) # True (Since Dog inherits from Animal)
True
True
True
8.5.6 The object
class in Python
When you define a class in Python without explicitly specifying a parent class, Python automatically makes it inherit from the built-in object
class.
Let’s confirm this using the issubclss()
method
print(issubclass(Car, object))
True
You can use the __bases__
attribute to check the parent class(es):
print(Car.__bases__)
(<class 'object'>,)
8.5.6.1 What is the object
class?
object
is the base class for all classes in Python.- It provides default methods like:
- str()
- repr()
- eq()
- init()
- And more…
Let’s back to the whole list of car1
dir(car1)
['__class__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__getstate__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__',
'brand',
'wheels']
Many special methods and attributes that start with double underscores (__
) are inherited from the object
class in Python. These are known as dunder (double underscore) methods or magic methods, such as __init__()
, __str__()
, and __eq__()
. These methods are automatically called by Python for specific operations, and users typically do not need to call them directly.
We generally do not recommend modifying these methods unless you are customizing class behavior (e.g., overloading operators). If you need to define private attributes or methods, use a single underscore _
(convention) or double underscore __
(name mangling) to prevent accidental access.
Key Takeaways:
- Inheritance promotes code reuse and hierarchy in OOP.
- A child class can inherit and override parent class methods.
- Use
super()
to call parent class methods inside the child class.
For more details, refer to the official Python documentation on Inheritance.
8.5.7 Practice exercise 2
Define a class that inherits the in-built Python class list
, and adds a new method to the class called nunique()
which returns the number of unique elements in the list.
Define the following list as an object of the class you created. Then:
Find the number of unique elements in the object using the method
nunique()
of the inherited class.Check if the
pop()
method of the parent class works to pop an element out of the object.
= [1,2,5,3,6,5,5,5,12] list_ex
class list_v2(list):
def nuinque(self):
= []
unique_elements for x in self:
if x not in unique_elements:
unique_elements.append(x)return len(unique_elements)
= list_v2(list_ex)
list_ex print("Number of unique elements = ", list_ex.nuinque())
print("Checking the pop() method, the popped out element is", list_ex.pop())
Number of unique elements = 6
Checking the pop() method, the popped out element is 12
8.5.8 Practice exercise 3
Define a class named PasswordManagerUpdated
that inherits the class PasswordManager
defined in Practice exercise 1. The class PasswordManagerUpdated
should have two methods, other than the constructor:
The method
set_password()
that sets a new password. The new password must only be accepted if it does not have any punctuations in it, and if it is not the same as one of the old passwords. If the new password is not acceptable, then one of the appropriate messages should be printed - (a) Cannot have punctuation in password, try again, or (b) Old password cannot be reused, try again.The method
suggest_password()
that randomly sets and returns a password as a string comprising of 15 randomly chosen letters. Letters may be repeated as well.
import random
import string
class PasswordManagerUpdated(PasswordManager):
def __init__(self, initial_passwords):
super().__init__(initial_passwords)
def set_password(self, new_password):
"""Sets a new password if it does not contain punctuation and is not a reused password."""
if any(char in string.punctuation for char in new_password):
print("Cannot have punctuation in password, try again.")
elif new_password in self.old_passwords:
print("Old password cannot be reused, try again.")
else:
self.old_passwords.append(new_password)
print("Password changed successfully!")
def suggest_password(self):
"""Generates and returns a random 15-character password without punctuation."""
= ''.join(random.choices(string.ascii_letters, k=15))
suggested_password self.old_passwords.append(suggested_password)
return suggested_password
Note that ascii_letters
constant in Python is part of the string
module and is a predefined string that contains all the lowercase and uppercase ASCII letters. It It contains all lowercase (‘a’ to ‘z’) and uppercase (‘A’ to ‘Z’) ASCII characters in sequence.
import string
# Accessing ascii_letters
= string.ascii_letters
s print(s)
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
Let’s use the class below:
# Initialize PasswordManagerUpdated with given passwords
= ["alpha123", "beta456", "gamma789", "delta321", "ibiza1972"]
initial_passwords = PasswordManagerUpdated(initial_passwords)
password_manager_updated
# 1. Try setting a password with punctuation
"newPass@word!")
password_manager_updated.set_password(
# 2. Try setting an already used password
"ibiza1972")
password_manager_updated.set_password(
# 3. Set a new valid password
"securePass123")
password_manager_updated.set_password(print("Current password:", password_manager_updated.get_password())
# 4. Generate and set a suggested password
= password_manager_updated.suggest_password()
suggested print("Suggested password:", suggested)
print("Current password after suggestion:", password_manager_updated.get_password())
Cannot have punctuation in password, try again.
Old password cannot be reused, try again.
Password changed successfully!
Current password: securePass123
Suggested password: xXgtHYeOAPsVvvY
Current password after suggestion: xXgtHYeOAPsVvvY