PYnative

Python Programming

  • Learn Python
    • Python Tutorials
    • Python Basics
    • Python Interview Q&As
  • Exercises
    • Python Exercises
    • C Programming Exercises
    • C++ Exercises
  • Quizzes
  • Code Editor
    • Online Python Code Editor
    • Online C Compiler
    • Online C++ Compiler
Home » Python Exercises » 30+ Python Object-Oriented Programming (OOP) Exercise: Classes and Objects Exercises

30+ Python Object-Oriented Programming (OOP) Exercise: Classes and Objects Exercises

Updated on: May 6, 2026 | 56 Comments

This article provides 31 Python Object-Oriented Programming (OOP) practice questions with solutions.

These exercises cover classes, attributes, methods, logic, inheritance, polymorphism, magic methods, encapsulation, type checking, and advance OOP concepts.

Each coding challenge includes a Practice Problem, Hint, Solution code, and detailed Explanation, ensuring you don’t just copy code, but genuinely practice and understand how and why it works.

  • All solutions have been fully tested on Python 3.
  • Use our Online Code Editor to solve these exercises in real time.
  • If you have other solutions, please share them in the comments to help fellow developers.

Also See:

  • Python OOP: Learn OOP (Object Oriented Programming) in Python to solve exercises.
  • Python OOP Interview Questions
+ Table Of Contents (31 Exercises)

Table of contents

  • Exercise 1: Define an Empty Vehicle Class
  • Exercise 2: Vehicle Class with Instance Attributes
  • Exercise 3: Rectangle Class with Area & Perimeter
  • Exercise 4: Student Class with Average Grade
  • Exercise 5: Product Class with Stock Value Calculator
  • Exercise 6: Bank Account with Deposit & Overdraw Protection
  • Exercise 7: Light Class with On/Off State Toggle
  • Exercise 8: User Class with Password Validation
  • Exercise 9: Temperature Class with Unit Converters
  • Exercise 10: Notebook Class with Add & Display Notes
  • Exercise 11: Coffee Machine with Multi-Resource Tracking
  • Exercise 12: Shared Class Attribute Across Instances
  • Exercise 13: Bus Subclass Inheriting from Vehicle
  • Exercise 14: verride Parent Method Using super()
  • Exercise 15: Add Maintenance Fee in Child Class via super()
  • Exercise 16: Polymorphism with Dog & Cat speak()
  • Exercise 17: Full-Time vs Part-Time Employee Pay Logic
  • Exercise 18: Shape Subclasses with Custom area() Methods
  • Exercise 19: Media Subclasses with Type-Specific Attributes
  • Exercise 20: Discounted Order Subclass with 10% Off
  • Exercise 21: Vehicle Class Hierarchy with Bike, Truck & Bus
  • Exercise 22: Identify Object’s Class Using type()
  • Exercise 23: Type Checking with isinstance() & issubclass()
  • Exercise 24: Vector Addition Using add Overloading
  • Exercise 25: Cart Length Using len Overloading
  • Exercise 26: Private Balance with Property Getter & Setter
  • Exercise 27: Callable Object Class Using call
  • Exercise 28: Flight Class with Passenger Capacity Check
  • Exercise 29: Zoo Class that Feeds All Animals
  • Exercise 30: Character Class with Auto Level-Up Logic
  • Exercise 31: Playlist Class with Add, Remove & Shuffle

Exercise 1: Define an Empty Vehicle Class

Problem Statement: Write a Python program to create a class named Vehicle that has no variables or methods defined inside it.

Purpose: This exercise introduces the most basic form of a Python class definition. It teaches you the required syntax for declaring a class and the use of the pass keyword as a placeholder, which is essential when you want to define an empty class or function body without causing a syntax error.

Given Input: No input required.

Expected Output: <class '__main__.Vehicle'>

Refer: Classes and Objects in Python

▼ Hint
  • Use the class keyword followed by the class name and a colon.
  • Use pass inside the class body to make it valid Python without adding any attributes or methods.
  • After defining the class, you can confirm it exists by printing Vehicle directly.
▼ Solution & Explanation
class Vehicle:
    pass

print(Vehicle)Code language: Python (python)

Explanation:

  • class Vehicle:: Declares a new class named Vehicle. The colon marks the start of the class body.
  • pass: A no-op placeholder that satisfies Python’s requirement for a non-empty class body. It does nothing at runtime but prevents a SyntaxError.
  • print(Vehicle): Prints the class object itself, confirming it was created successfully. The output shows the class name and the module it belongs to (__main__ when run as a script).

Exercise 2: Vehicle Class with Instance Attributes

Problem Statement: Write a Python program to create a Vehicle class with two instance attributes: max_speed and mileage. Create an object of the class and print both attributes.

Purpose: Learn to define instance attributes using the __init__ constructor method. Instance attributes are unique to each object, meaning different Vehicle objects can hold different values for speed and mileage. This is a foundational concept in object-oriented programming.

Given Input: vehicle1 = Vehicle("Tesla Model S", 250, 18)

Expected Output: Vehicle Name: Tesla Model S, Speed: 250, Mileage: 18

Refer:

  • Instance variables in Python
  • Python Instance Methods
  • Constructors in Python
▼ Hint
  • Define an __init__ method that accepts self, name, max_speed, and mileage as parameters.
  • Inside __init__, assign each parameter to self to store them as instance attributes.
  • Create an instance by calling Vehicle(...) with the required arguments, then access attributes using dot notation.
▼ Solution & Explanation
class Vehicle:
    def __init__(self, name, max_speed, mileage):
        self.name = name
        self.max_speed = max_speed
        self.mileage = mileage

vehicle1 = Vehicle("Tesla Model S", 250, 18)
print(f"Vehicle Name: {vehicle1.name}, Speed: {vehicle1.max_speed}, Mileage: {vehicle1.mileage}")Code language: Python (python)

Explanation:

  • def __init__(self, name, max_speed, mileage): The constructor method, called automatically when a new object is created. self refers to the specific instance being initialized.
  • self.name = name: Binds the argument passed during object creation to the instance, making it accessible as an attribute on that object.
  • vehicle1 = Vehicle("Tesla Model S", 250, 18): Creates a new Vehicle instance. Python passes the arguments to __init__ automatically.
  • vehicle1.max_speed: Dot notation is used to access instance attributes. Each object maintains its own copy of these values.

Exercise 3: Rectangle Class with Area & Perimeter

Problem Statement: Write a Python program to create a Rectangle class with length and width as instance attributes, and two methods: area() that returns the area and perimeter() that returns the perimeter.

Purpose: Learn how to add instance methods to a class. Methods allow objects to perform operations using their own data, which is a key principle of encapsulation in OOP. Calculating geometric properties is a clean, practical context for understanding how self connects methods to instance data.

Given Input: rect = Rectangle(10, 4)

Expected Output: Area = 40 and Perimeter = 28

▼ Hint
  • Store length and width as instance attributes inside __init__.
  • Define area(self) that returns self.length * self.width.
  • Define perimeter(self) that returns 2 * (self.length + self.width).
▼ Solution & Explanation
class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * (self.length + self.width)

rect = Rectangle(10, 4)
print("Area =", rect.area())
print("Perimeter =", rect.perimeter())Code language: Python (python)

Explanation:

  • def area(self): An instance method that uses self.length and self.width to compute and return the area. The self parameter gives the method access to the object’s own attributes.
  • def perimeter(self): Applies the standard perimeter formula for a rectangle: 2 * (length + width). Like area(), it reads directly from instance attributes.
  • rect.area(): Calling a method on an instance automatically passes that instance as self. You do not pass self explicitly when calling the method.

Exercise 4: Student Class with Average Grade

Problem Statement: Write a Python program to create a Student class that stores a student’s name and a list of marks. Add a method average() that calculates and returns the average of all marks.

Purpose: This exercise shows how instance attributes can store complex data types such as lists, not just simple values. It also practices combining OOP with list operations and arithmetic, a pattern common in gradebooks, dashboards, and reporting tools.

Given Input: s1 = Student("Alice", [85, 90, 78, 92, 88])

Expected Output: Alice's Average Grade: 86.6

▼ Hint
  • Accept name and marks (a list) in the __init__ method and assign them to self.
  • In the average() method, use sum(self.marks) / len(self.marks) to compute the mean.
  • Use round() if you want to control the number of decimal places in the output.
▼ Solution & Explanation
class Student:
    def __init__(self, name, marks):
        self.name = name
        self.marks = marks

    def average(self):
        return sum(self.marks) / len(self.marks)

s1 = Student("Alice", [85, 90, 78, 92, 88])
print(f"{s1.name}'s Average Grade: {s1.average()}")Code language: Python (python)

Explanation:

  • self.marks = marks: Stores the entire list as an instance attribute. Each Student object holds its own independent list of marks.
  • sum(self.marks): Uses Python’s built-in sum() to total all elements in the marks list without needing an explicit loop.
  • len(self.marks): Returns the count of items in the list, used as the divisor to compute the mean. This works correctly regardless of how many marks are stored.
  • s1.average(): Calls the method on the instance. The result is a float because Python 3’s / operator always returns a float.

Exercise 5: Product Class with Stock Value Calculator

Problem Statement: Write a Python program to create a Product class with three instance attributes: name, price, and quantity. Add a method total_value() that returns the total stock value by multiplying price by quantity.

Purpose: This exercise models a real-world business scenario using OOP. It reinforces how instance methods can derive new information from existing attributes, a pattern widely used in inventory management, e-commerce, and financial applications.

Given Input: p1 = Product("Laptop", 899.99, 5)

Expected Output: Total stock value of Laptop: $4499.95

▼ Hint
  • Define __init__ with name, price, and quantity as parameters and assign each to self.
  • In total_value(), return self.price * self.quantity.
  • Use an f-string with :.2f formatting to display the result as a currency value with two decimal places.
▼ Solution & Explanation
class Product:
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity

    def total_value(self):
        return self.price * self.quantity

p1 = Product("Laptop", 899.99, 5)
print(f"Total stock value of {p1.name}: ${p1.total_value():.2f}")Code language: Python (python)

Explanation:

  • self.price and self.quantity: Stored as instance attributes so each Product object independently tracks its own price and stock level.
  • def total_value(self): A computed method that multiplies self.price by self.quantity to derive the total stock value. No external data is needed since all values are already on the instance.
  • :.2f: A format specifier inside an f-string that rounds the float to exactly two decimal places, which is the standard format for displaying currency values.

Exercise 6: Bank Account with Deposit & Overdraw Protection

Problem Statement: Write a Python program to create a BankAccount class with a balance attribute and two methods: deposit(amount) that adds funds to the balance, and withdraw(amount) that deducts funds but prevents the balance from going below zero.

Purpose: Learn data validation and conditional logic inside instance methods. Preventing overdraw is a real-world business rule, and implementing it here teaches you how classes can enforce constraints on their own data, a core idea behind encapsulation in OOP.

Given Input: Starting balance of 1000, deposit 500, withdraw 200, then attempt to withdraw 2000.

Expected Output:

Balance after deposit: 1500
Balance after withdrawal: 1300
Insufficient funds. Current balance: 1300
▼ Hint
  • Initialize self.balance in __init__.
  • In deposit(), add the amount directly to self.balance.
  • In withdraw(), use an if statement to check whether amount <= self.balance before deducting. If not, print an insufficient funds message instead.
▼ Solution & Explanation
class BankAccount:
    def __init__(self, balance):
        self.balance = balance
    def deposit(self, amount):
        self.balance += amount
        print(f"Balance after deposit: {self.balance}")
    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            print(f"Balance after withdrawal: {self.balance}")
        else:
            print(f"Insufficient funds. Current balance: {self.balance}")
account = BankAccount(1000)
account.deposit(500)
account.withdraw(200)
account.withdraw(2000)Code language: Python (python)

Explanation:

  • self.balance += amount: The deposit() method directly mutates the instance’s balance. Because the attribute is stored on self, the change persists across all future method calls on that object.
  • if amount <= self.balance: Guards the withdrawal by checking that sufficient funds exist before modifying the balance. This enforces the business rule that the balance cannot go negative.
  • else: print(...): Provides feedback when a withdrawal is rejected. In a real application this could raise a custom exception instead, but a printed message is appropriate for an introductory exercise.

Exercise 7: Light Class with On/Off State Toggle

Problem Statement: Write a Python program to create a Light class with three methods: turn_on() that switches the light on, turn_off() that switches it off, and status() that reports whether the light is currently on or off.

Purpose: This exercise models a simple stateful object, where the object remembers and changes its own condition over time. It introduces the concept of state management within a class, a pattern found everywhere from UI components and IoT devices to game objects and workflow engines.

Given Input: Create a Light object, call turn_on(), check status(), call turn_off(), and check status() again.

Expected Output:

Light is ON
Current status: ON
Light is OFF
Current status: OFF
▼ Hint
  • Use a boolean attribute such as self.is_on = False in __init__ to track the current state.
  • turn_on() should set self.is_on = True and print a confirmation message.
  • turn_off() should set self.is_on = False and print a confirmation message.
  • In status(), use a conditional to print "ON" or "OFF" based on the value of self.is_on.
▼ Solution & Explanation
class Light:
    def __init__(self):
        self.is_on = False

    def turn_on(self):
        self.is_on = True
        print("Light is ON")

    def turn_off(self):
        self.is_on = False
        print("Light is OFF")

    def status(self):
        state = "ON" if self.is_on else "OFF"
        print(f"Current status: {state}")

light = Light()
light.turn_on()
light.status()
light.turn_off()
light.status()Code language: Python (python)

Explanation:

  • self.is_on = False: Sets the initial state of the light to off when the object is first created. Using a boolean is the most direct way to represent a two-state condition like on/off.
  • turn_on() and turn_off(): Each method simply flips self.is_on to the appropriate boolean value and prints a message. Because the value is stored on self, the change is retained for all subsequent method calls.
  • "ON" if self.is_on else "OFF": A Python ternary expression that converts the boolean state into a human-readable string. This is more concise than writing a full if/else block for a simple two-branch output.

Exercise 8: User Class with Password Validation

Problem Statement: Write a Python program to create a User class that stores a username and a password. Add a check_password(input_password) method that returns True if the input matches the stored password, and False otherwise.

Purpose: This exercise introduces the idea of controlled access to sensitive data inside a class. Rather than exposing the password directly, the class provides a dedicated method to verify it. This pattern reflects a core principle of encapsulation in OOP, where internal data is protected and accessed only through defined interfaces.

Given Input: u1 = User("alice", "secure123")

Expected Output:

True
False
▼ Hint
  • Store username and password as instance attributes in __init__.
  • In check_password(self, input_password), compare input_password to self.password using == and return the result directly.
  • Call the method with a correct password and then an incorrect one to verify both outcomes.
▼ Solution & Explanation
class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def check_password(self, input_password):
        return self.password == input_password

u1 = User("alice", "secure123")
print(u1.check_password("secure123"))
print(u1.check_password("wrongpass"))Code language: Python (python)

Explanation:

  • self.password = password: Stores the password as an instance attribute. In real applications you would never store a plain-text password; instead you would hash it using a library like bcrypt. Here, plain text is used to keep the focus on OOP fundamentals.
  • def check_password(self, input_password): Accepts a candidate password and compares it to the stored one. Exposing a method rather than the attribute directly means the calling code never needs to touch self.password itself.
  • return self.password == input_password: The == comparison evaluates to a boolean, so the result can be returned directly without wrapping it in an explicit if/else.

Exercise 9: Temperature Class with Unit Converters

Problem Statement: Write a Python program to create a Temperature class that stores a temperature in Celsius. Add two methods: to_fahrenheit() that converts and returns the value in Fahrenheit, and to_kelvin() that converts and returns the value in Kelvin.

Purpose: This exercise demonstrates how a class can act as a data container with built-in conversion logic. It reinforces writing multiple methods that all operate on the same instance attribute, and applies straightforward mathematical formulas in a practical scientific context.

Given Input: t = Temperature(100)

Expected Output:

Celsius: 100
Fahrenheit: 212.0
Kelvin: 373.15
▼ Hint
  • Store the Celsius value as self.celsius in __init__.
  • For Fahrenheit, use the formula: (celsius * 9/5) + 32.
  • For Kelvin, use the formula: celsius + 273.15.
▼ Solution & Explanation
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    def to_fahrenheit(self):
        return (self.celsius * 9 / 5) + 32

    def to_kelvin(self):
        return self.celsius + 273.15

t = Temperature(100)
print("Celsius:", t.celsius)
print("Fahrenheit:", t.to_fahrenheit())
print("Kelvin:", t.to_kelvin())Code language: Python (python)

Explanation:

  • self.celsius = celsius: The single source of truth for this object. Both conversion methods derive their results from this one attribute, so updating it would automatically affect all conversions.
  • (self.celsius * 9 / 5) + 32: The standard Celsius-to-Fahrenheit formula. In Python 3, 9 / 5 evaluates to 1.8 as a float, so the result is always a decimal number.
  • self.celsius + 273.15: The Celsius-to-Kelvin conversion adds the absolute zero offset. Adding a float literal ensures the return value is always a float, consistent with scientific notation.

Exercise 10: Notebook Class with Add & Display Notes

Problem Statement: Write a Python program to create a Notebook class that maintains an internal list of notes. Add an add_note(note) method that appends a new note to the list, and a show_notes() method that prints all stored notes.

Purpose: This exercise shows how a class can manage a growing collection of data over its lifetime. It practices initializing a mutable data structure inside __init__ and writing methods that both modify and read that structure, a pattern that appears in todo lists, message queues, logs, and many other applications.

Given Input: Add three notes: "Buy groceries", "Read a book", "Call the doctor".

Expected Output:

1. Buy groceries
2. Read a book
3. Call the doctor
▼ Hint
  • Initialize self.notes = [] inside __init__ so each Notebook object starts with its own empty list.
  • In add_note(), use self.notes.append(note) to add the new entry.
  • In show_notes(), use enumerate(self.notes, start=1) to print each note with a numbered prefix.
▼ Solution & Explanation
class Notebook:
    def __init__(self):
        self.notes = []

    def add_note(self, note):
        self.notes.append(note)

    def show_notes(self):
        for i, note in enumerate(self.notes, start=1):
            print(f"{i}. {note}")

nb = Notebook()
nb.add_note("Buy groceries")
nb.add_note("Read a book")
nb.add_note("Call the doctor")
nb.show_notes()Code language: Python (python)

Explanation:

  • self.notes = []: Initializing the list inside __init__ is critical. If it were defined at the class level instead, all instances would share the same list, causing notes from one notebook to appear in another.
  • self.notes.append(note): Mutates the instance’s own list in place. Each call to add_note() grows the list by one entry, and the change persists on the object until it is destroyed.
  • enumerate(self.notes, start=1): Produces pairs of (index, value) starting from 1, allowing the loop to print a numbered list without manually tracking a counter variable.

Exercise 11: Coffee Machine with Multi-Resource Tracking

Problem Statement: Write a Python program to create a CoffeeMachine class that tracks three resource attributes: water, coffee, and milk (in ml/g). Add a make_latte() method that checks whether sufficient resources are available, deducts them if so, and prints an appropriate message in either case.

Purpose: This exercise combines state management, resource tracking, and conditional logic inside a single class. It mirrors how real-world stateful systems (vending machines, inventory systems, game resource managers) check preconditions before executing an action and update their internal state only when the action is valid.

Given Input: CoffeeMachine(water=300, coffee=100, milk=200). A latte requires 200ml water, 20g coffee, and 150ml milk.

Expected Output:

Latte made! Remaining - Water: 100ml, Coffee: 80g, Milk: 50ml
Not enough resources to make a latte.
▼ Hint
  • Store water, coffee, and milk as instance attributes in __init__.
  • In make_latte(), define the required amounts as local variables and use a single if condition to check all three resources at once.
  • If the check passes, deduct the required amounts from each attribute and print the remaining levels. Otherwise, print a failure message.
▼ Solution & Explanation
class CoffeeMachine:
    def __init__(self, water, coffee, milk):
        self.water = water
        self.coffee = coffee
        self.milk = milk

    def make_latte(self):
        water_needed = 200
        coffee_needed = 20
        milk_needed = 150

        if self.water >= water_needed and self.coffee >= coffee_needed and self.milk >= milk_needed:
            self.water -= water_needed
            self.coffee -= coffee_needed
            self.milk -= milk_needed
            print(f"Latte made! Remaining - Water: {self.water}ml, Coffee: {self.coffee}g, Milk: {self.milk}ml")
        else:
            print("Not enough resources to make a latte.")

machine = CoffeeMachine(water=300, coffee=100, milk=200)
machine.make_latte()
machine.make_latte()Code language: Python (python)

Explanation:

  • water_needed, coffee_needed, milk_needed: Defined as local variables inside the method to represent the recipe. Keeping them local (rather than hardcoded inline) makes the method easier to read and the values easy to change in one place.
  • if self.water >= water_needed and ...: All three resource checks are combined into one condition using and. The deduction only happens when every condition is met, which prevents partial consumption of resources on a failed brew.
  • self.water -= water_needed: Mutates the instance attribute in place. After a successful latte, the machine’s state is permanently updated, so a second call to make_latte() reflects the reduced levels.
  • Second call to make_latte(): With only 100ml water remaining (less than the required 200ml), the condition fails and the insufficient resources message is printed, demonstrating state persistence across calls.

Exercise 12: Shared Class Attribute Across Instances

Problem Statement: Write a Python program to create a Vehicle class with a class attribute color = "White" that is shared by all instances. Create two vehicle objects and demonstrate that both share the same default color, then show that changing the class attribute updates all instances that have not overridden it.

Purpose: This exercise clarifies the distinction between class attributes and instance attributes. Class attributes are defined directly on the class and shared across every instance, making them ideal for default values or constants that apply universally. Understanding this difference prevents subtle bugs when mutable data is accidentally shared between objects.

Given Input: v1 = Vehicle("Tesla", 250) and v2 = Vehicle("BMW", 200).

Expected Output:

Tesla - Color: White, Speed: 250
BMW - Color: White, Speed: 200
Tesla - Color: Red, Speed: 250
BMW - Color: Red, Speed: 200

Refer: Python Class Variables

▼ Hint
  • Define color = "White" directly inside the class body, outside of any method, to make it a class attribute.
  • Instance attributes like name and max_speed are still defined in __init__ as usual.
  • To update the shared attribute for all instances, reassign it via the class itself: Vehicle.color = "Red".
▼ Solution & Explanation
class Vehicle:
    color = "White"

    def __init__(self, name, max_speed):
        self.name = name
        self.max_speed = max_speed

v1 = Vehicle("Tesla", 250)
v2 = Vehicle("BMW", 200)

print(f"{v1.name} - Color: {v1.color}, Speed: {v1.max_speed}")
print(f"{v2.name} - Color: {v2.color}, Speed: {v2.max_speed}")

Vehicle.color = "Red"

print(f"{v1.name} - Color: {v1.color}, Speed: {v1.max_speed}")
print(f"{v2.name} - Color: {v2.color}, Speed: {v2.max_speed}")Code language: Python (python)

Explanation:

  • color = "White": Declared at the class level, outside __init__. This means it belongs to the class itself, not to any single instance. All objects share the same value unless they individually override it.
  • v1.color: When Python looks up color on an instance and does not find it as an instance attribute, it walks up to the class and finds the class attribute there. This lookup chain is part of Python’s attribute resolution order.
  • Vehicle.color = "Red": Reassigning via the class name updates the attribute at the class level, so all instances that still rely on the class attribute immediately reflect the new value. By contrast, doing v1.color = "Red" would only create a new instance attribute on v1, leaving v2 unaffected.

Exercise 13: Bus Subclass Inheriting from Vehicle

Problem Statement: Write a Python program to create a Vehicle parent class with name and max_speed attributes and a display() method. Then create a Bus child class that inherits everything from Vehicle without adding anything new, and confirm that an instance of Bus can access the parent’s method.

Purpose: This exercise introduces inheritance, one of the four pillars of OOP. Inheritance lets a child class automatically receive all attributes and methods from its parent, promoting code reuse and expressing natural “is-a” relationships. A Bus is a Vehicle, so it makes sense for it to share the same interface.

Given Input: bus1 = Bus("School Bus", 120)

Expected Output: Vehicle: School Bus, Max Speed: 120 km/h

Refer: Inheritance in Python

▼ Hint
  • To create a child class, pass the parent class as an argument in the class definition: class Bus(Vehicle):.
  • If the child class adds nothing new, use pass in its body.
  • Create a Bus instance using the same arguments as Vehicle and call display() to confirm inheritance is working.
▼ Solution & Explanation
class Vehicle:
    def __init__(self, name, max_speed):
        self.name = name
        self.max_speed = max_speed

    def display(self):
        print(f"Vehicle: {self.name}, Max Speed: {self.max_speed} km/h")

class Bus(Vehicle):
    pass

bus1 = Bus("School Bus", 120)
bus1.display()Code language: Python (python)

Explanation:

  • class Bus(Vehicle):: The parentheses indicate that Bus inherits from Vehicle. Python sets up the inheritance chain automatically, meaning Bus gets all of Vehicle‘s attributes and methods for free.
  • pass: Since Bus adds no new behavior at this stage, pass is used as a placeholder. The class is still fully functional because everything it needs comes from Vehicle.
  • bus1.display(): Python first looks for display on the Bus instance, then on the Bus class, and finally on Vehicle, where it finds and executes the method. This lookup process is known as the Method Resolution Order (MRO).

Exercise 14: verride Parent Method Using super()

Problem Statement: Write a Python program where a Vehicle parent class has a seating_capacity() method that accepts a capacity argument. Create a Bus child class that overrides this method to provide a default seating capacity of 50, using super() to call the parent’s version internally.

Purpose: This exercise covers method overriding and the use of super(), two key tools in OOP inheritance. Overriding lets a child class customize or extend a parent method’s behavior without rewriting it from scratch. super() delegates part of the work back to the parent, keeping the code DRY and maintaining the original logic as a foundation.

Given Input: bus = Bus("School Bus", 120)

Expected Output: School Bus seating capacity is: 50

▼ Hint
  • Define seating_capacity(self, capacity) in the Vehicle class and have it print a message using the capacity argument.
  • In the Bus class, define a method with the same name but override it to call super().seating_capacity(50), passing the default value of 50 directly.
  • Call bus.seating_capacity() on a Bus instance with no arguments to confirm that the default kicks in.
▼ Solution & Explanation
class Vehicle:
    def __init__(self, name, max_speed):
        self.name = name
        self.max_speed = max_speed

    def seating_capacity(self, capacity):
        print(f"{self.name} seating capacity is: {capacity}")

class Bus(Vehicle):
    def seating_capacity(self):
        super().seating_capacity(50)

bus = Bus("School Bus", 120)
bus.seating_capacity()Code language: Python (python)

Explanation:

  • def seating_capacity(self, capacity) in Vehicle: The parent defines the method to accept a flexible capacity value, keeping it general enough to work for any vehicle type.
  • def seating_capacity(self) in Bus: The child overrides the method with a version that takes no capacity argument. This is the override: when seating_capacity() is called on a Bus instance, Python runs this version instead of the parent’s.
  • super().seating_capacity(50): super() returns a proxy to the parent class, allowing the child to call the parent’s method directly. The hardcoded 50 is the bus-specific default, passed up to the parent’s implementation so the print logic stays in one place.
  • bus.seating_capacity(): Called with no arguments on the Bus instance. The override intercepts the call, supplies the default of 50, and delegates the actual output to the parent, combining both classes’ behavior cleanly.

Exercise 15: Add Maintenance Fee in Child Class via super()

Problem Statement: Write a Python program that creates a Vehicle parent class with a base fare, then extends a Taxi child class that adds a 10% maintenance fee on top of the base fare using super().

Purpose: This exercise teaches you how to use super() to call the parent class constructor, extend child class behaviour by building on inherited attributes, and model real-world pricing logic using inheritance.

Given Input: base_fare = 500

Expected Output: Total fare with maintenance fee: 550.0

▼ Hint
  • Define a Vehicle class with an __init__ that accepts base_fare and stores it as an instance attribute.
  • Create a Taxi class that inherits from Vehicle.
  • In Taxi.__init__, call super().__init__(base_fare) to initialise the parent, then compute the maintenance fee as base_fare * 0.10.
  • Add a method total_fare() that returns self.base_fare + self.maintenance_fee.
▼ Solution & Explanation
class Vehicle:
    def __init__(self, base_fare):
        self.base_fare = base_fare

class Taxi(Vehicle):
    def __init__(self, base_fare):
        super().__init__(base_fare)
        self.maintenance_fee = base_fare * 0.10

    def total_fare(self):
        return self.base_fare + self.maintenance_fee

taxi = Taxi(500)
print("Total fare with maintenance fee:", taxi.total_fare())Code language: Python (python)

Explanation:

  • class Vehicle: Defines the parent class that accepts and stores base_fare in its constructor.
  • super().__init__(base_fare): Calls the parent constructor from inside the child class, ensuring self.base_fare is properly set before the child adds its own logic.
  • self.maintenance_fee = base_fare * 0.10: Computes the 10% maintenance fee and stores it as a separate child-only attribute.
  • total_fare(): Returns the sum of the base fare and the maintenance fee, demonstrating how child classes can extend parent behaviour without modifying the parent.

Exercise 16: Polymorphism with Dog & Cat speak()

Problem Statement: Write a Python program that defines an Animal base class with a speak() method, then overrides it in Dog and Cat subclasses to return their respective sounds.

Purpose: This exercise introduces method overriding, one of the core pillars of polymorphism in OOP. It shows how different subclasses can share the same interface but provide their own specific behaviour.

Given Input: Objects of Dog and Cat classes

Expected Output:

Dog says: Woof!
Cat says: Meow!

Refer: Polymorphism in Python

▼ Hint
  • Define an Animal class with a speak() method that returns a generic string like "Some sound".
  • Create Dog and Cat classes that inherit from Animal.
  • Override speak() in each subclass to return the appropriate sound string.
  • Instantiate both classes and call speak() on each object to verify the output.
▼ Solution & Explanation
class Animal:
    def speak(self):
        return "Some sound"

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

dog = Dog()
cat = Cat()

print("Dog says:", dog.speak())
print("Cat says:", cat.speak())Code language: Python (python)

Explanation:

  • class Animal: Acts as the base class with a default speak() method, establishing a common interface for all subclasses.
  • class Dog(Animal): Inherits from Animal and overrides speak() to return "Woof!", replacing the parent’s generic implementation.
  • class Cat(Animal): Similarly overrides speak() to return "Meow!".
  • Method overriding: When speak() is called on a Dog or Cat object, Python uses the subclass version, not the parent’s. This is the foundation of polymorphism.

Exercise 17: Full-Time vs Part-Time Employee Pay Logic

Problem Statement: Write a Python program that defines an Employee base class, then creates FullTimeEmployee and PartTimeEmployee subclasses, each implementing different pay calculation logic.

Purpose: This exercise models a common HR scenario and teaches you how to use inheritance to share common attributes while allowing each subclass to define its own business logic for calculating pay.

Given Input: FullTimeEmployee("Alice", 60000) and PartTimeEmployee("Bob", 500, 20)

Expected Output:

Alice's monthly pay: 5000.0
Bob's monthly pay: 10000
▼ Hint
  • Define an Employee base class with __init__ accepting name and a calculate_pay() method that can be left as a placeholder.
  • In FullTimeEmployee, store the annual salary and compute monthly pay as salary / 12.
  • In PartTimeEmployee, store hourly_rate and hours_worked, then compute pay as their product.
  • Override calculate_pay() in each subclass with the appropriate formula.
▼ Solution & Explanation
class Employee:
    def __init__(self, name):
        self.name = name

    def calculate_pay(self):
        return 0

class FullTimeEmployee(Employee):
    def __init__(self, name, annual_salary):
        super().__init__(name)
        self.annual_salary = annual_salary

    def calculate_pay(self):
        return self.annual_salary / 12

class PartTimeEmployee(Employee):
    def __init__(self, name, hourly_rate, hours_worked):
        super().__init__(name)
        self.hourly_rate = hourly_rate
        self.hours_worked = hours_worked

    def calculate_pay(self):
        return self.hourly_rate * self.hours_worked

ft = FullTimeEmployee("Alice", 60000)
pt = PartTimeEmployee("Bob", 500, 20)

print(f"{ft.name}'s monthly pay: {ft.calculate_pay()}")
print(f"{pt.name}'s monthly pay: {pt.calculate_pay()}")Code language: Python (python)

Explanation:

  • class Employee: Serves as the base class holding the shared name attribute and a default calculate_pay() that returns 0.
  • FullTimeEmployee.calculate_pay(): Divides the annual salary by 12 to get monthly pay, using Python 3’s / operator which returns a float.
  • PartTimeEmployee.calculate_pay(): Multiplies hourly_rate by hours_worked, modelling a pay-per-hour contract.
  • super().__init__(name): Used in both subclasses to delegate the name assignment to the parent, avoiding code duplication.

Exercise 18: Shape Subclasses with Custom area() Methods

Problem Statement: Write a Python program that defines a Shape base class with an area() method, then implements it in Circle, Square, and Triangle subclasses using the appropriate geometric formulas.

Purpose: This exercise is a classic demonstration of polymorphism. Each shape shares the same area() interface but provides a completely different calculation, showing how OOP handles real-world variation cleanly.

Given Input: Circle(7), Square(4), Triangle(6, 8)

Expected Output:

Circle area: 153.94
Square area: 16
Triangle area: 24.0
▼ Hint
  • Define a Shape base class with an area() method that returns 0 as a placeholder.
  • For Circle, use the formula 3.14159 * radius ** 2.
  • For Square, use side ** 2.
  • For Triangle, use 0.5 * base * height.
▼ Solution & Explanation
class Shape:
    def area(self):
        return 0

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return round(3.14159 * self.radius ** 2, 2)

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2

class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

shapes = [Circle(7), Square(4), Triangle(6, 8)]
for shape in shapes:
    print(f"{type(shape).__name__} area: {shape.area()}")Code language: Python (python)

Explanation:

  • class Shape: Provides a common interface with a default area() that returns 0. All subclasses are expected to override this method.
  • Circle.area(): Applies the formula pi * r^2. The result is rounded to 2 decimal places using round() for clean output.
  • Square.area(): Returns side ** 2, the simplest area formula.
  • type(shape).__name__: Dynamically retrieves the class name of each object at runtime, making the print loop reusable without hardcoding names.

Exercise 19: Media Subclasses with Type-Specific Attributes

Problem Statement: Write a Python program that defines a Media base class, then creates Book, Magazine, and DVD subclasses, each with type-specific attributes and a describe() method.

Purpose: This exercise shows how inheritance can model a taxonomy of related objects. Each media type shares a common identity (title, price) but carries unique attributes specific to its format, reflecting real-world library or inventory systems.

Given Input: Book("Clean Code", 499, "Robert C. Martin"), Magazine("Wired", 150, "Monthly"), DVD("Inception", 299, 148)

Expected Output:

Book: Clean Code by Robert C. Martin - Rs.499
Magazine: Wired (Monthly) - Rs.150
DVD: Inception, 148 mins - Rs.299
▼ Hint
  • Define a Media base class with title and price in __init__.
  • Each subclass should call super().__init__(title, price) and then add its own unique attribute: author for Book, frequency for Magazine, and duration for DVD.
  • Override a describe() method in each subclass to print a formatted string using the shared and unique attributes.
▼ Solution & Explanation
class Media:
    def __init__(self, title, price):
        self.title = title
        self.price = price

    def describe(self):
        return f"{self.title} - Rs.{self.price}"

class Book(Media):
    def __init__(self, title, price, author):
        super().__init__(title, price)
        self.author = author

    def describe(self):
        return f"Book: {self.title} by {self.author} - Rs.{self.price}"

class Magazine(Media):
    def __init__(self, title, price, frequency):
        super().__init__(title, price)
        self.frequency = frequency

    def describe(self):
        return f"Magazine: {self.title} ({self.frequency}) - Rs.{self.price}"

class DVD(Media):
    def __init__(self, title, price, duration):
        super().__init__(title, price)
        self.duration = duration

    def describe(self):
        return f"DVD: {self.title}, {self.duration} mins - Rs.{self.price}"

items = [
    Book("Clean Code", 499, "Robert C. Martin"),
    Magazine("Wired", 150, "Monthly"),
    DVD("Inception", 299, 148)
]

for item in items:
    print(item.describe())Code language: Python (python)

Explanation:

  • class Media: Stores the shared title and price attributes common to all media types, and provides a generic describe() fallback.
  • super().__init__(title, price): Used in every subclass to avoid repeating the assignment of shared attributes, keeping the code DRY (Don’t Repeat Yourself).
  • Unique attributes: author, frequency, and duration are subclass-specific and would not belong in the base class, since not all media types share them.
  • Iterating with describe(): Calling the same method on different objects and getting different output is polymorphism in practice.

Exercise 20: Discounted Order Subclass with 10% Off

Problem Statement: Write a Python program that creates an Order class with a total amount, then creates a DiscountedOrder subclass that applies a 10% discount to the total.

Purpose: This exercise models a common e-commerce pattern and shows how a child class can extend a parent’s behaviour by modifying a calculated value, without changing the parent class at all.

Given Input: DiscountedOrder("ORD001", 1200)

Expected Output:

Order ID: ORD001
Original Total: 1200
Discounted Total: 1080.0
▼ Hint
  • Define an Order class with order_id and total attributes and a get_total() method that returns self.total.
  • In DiscountedOrder, call super().__init__(order_id, total) and override get_total() to return self.total * 0.90.
  • Print both the original self.total and the discounted result from get_total() to show the difference.
▼ Solution & Explanation
class Order:
    def __init__(self, order_id, total):
        self.order_id = order_id
        self.total = total

    def get_total(self):
        return self.total

class DiscountedOrder(Order):
    def __init__(self, order_id, total):
        super().__init__(order_id, total)

    def get_total(self):
        return self.total * 0.90

order = DiscountedOrder("ORD001", 1200)
print("Order ID:", order.order_id)
print("Original Total:", order.total)
print("Discounted Total:", order.get_total())Code language: Python (python)

Explanation:

  • class Order: The parent class stores order_id and total, and get_total() simply returns the full amount with no modification.
  • DiscountedOrder.get_total(): Overrides the parent method to apply a 10% reduction by multiplying self.total by 0.90. The parent class is never modified.
  • order.total vs order.get_total(): Accessing self.total directly still returns the original value, while get_total() returns the discounted one. This distinction is important for keeping the original data intact.
  • Open/Closed principle: The Order class is closed for modification but open for extension. DiscountedOrder extends it without touching the original code.

Exercise 21: Vehicle Class Hierarchy with Bike, Truck & Bus

Problem Statement: Write a Python program that defines a Vehicle base class and creates Bike, Truck, and Bus subclasses, each defining a unique max_speed attribute and a describe() method.

Purpose: This exercise reinforces the concept of class hierarchies and shows how subclasses can specialise a shared blueprint with their own attribute values, reflecting how real-world transport systems are categorised.

Given Input: Objects of Bike, Truck, and Bus classes

Expected Output:

Bike max speed: 120 km/h
Truck max speed: 90 km/h
Bus max speed: 100 km/h
▼ Hint
  • Define a Vehicle base class with a max_speed attribute set to 0 and a describe() method that prints it.
  • Create Bike, Truck, and Bus subclasses, each setting their own max_speed in __init__.
  • Override describe() in each subclass, or rely on the parent’s method if the output format is the same.
▼ Solution & Explanation
class Vehicle:
    def __init__(self, max_speed):
        self.max_speed = max_speed

    def describe(self):
        print(f"{type(self).__name__} max speed: {self.max_speed} km/h")

class Bike(Vehicle):
    def __init__(self):
        super().__init__(120)

class Truck(Vehicle):
    def __init__(self):
        super().__init__(90)

class Bus(Vehicle):
    def __init__(self):
        super().__init__(100)

vehicles = [Bike(), Truck(), Bus()]
for v in vehicles:
    v.describe()Code language: Python (python)

Explanation:

  • class Vehicle: Accepts max_speed as a parameter and provides a shared describe() method used by all subclasses.
  • Subclass constructors: Each subclass hardcodes its own max_speed value and passes it up to the parent via super().__init__(). No extra attributes are needed in the subclasses.
  • type(self).__name__: Inside the base class describe(), this retrieves the actual runtime class name (Bike, Truck, or Bus), making the method reusable without overriding it in each subclass.
  • Looping over a mixed list: All three objects are stored in a single list and iterated uniformly, a direct example of polymorphic behaviour.

Exercise 22: Identify Object’s Class Using type()

Problem Statement: Write a Python program that creates objects from multiple classes and uses the built-in type() function to identify which class each object belongs to.

Purpose: This exercise teaches you how Python tracks the type of every object at runtime. Understanding type() is essential for debugging, dynamic dispatch, and writing flexible code that reacts differently based on the type of object it receives.

Given Input: Objects from Dog, Cat, and Vehicle classes

Expected Output:

d is of type: Dog
c is of type: Cat
v is of type: Vehicle
▼ Hint
  • Define a few simple classes (they can have empty bodies using pass).
  • Create one object from each class.
  • Use type(obj).__name__ to get the class name as a string, or compare type(obj) directly to the class itself (e.g., type(obj) == Dog).
▼ Solution & Explanation
class Dog:
    pass

class Cat:
    pass

class Vehicle:
    pass

d = Dog()
c = Cat()
v = Vehicle()

objects = {"d": d, "c": c, "v": v}

for name, obj in objects.items():
    print(f"{name} is of type: {type(obj).__name__}")Code language: Python (python)

Explanation:

  • class Dog: pass: The pass keyword creates a valid but empty class body. This is useful when the structure of the class is not relevant to the exercise goal.
  • type(obj): Returns the class (type object) that obj was created from. For example, type(d) returns <class '__main__.Dog'>.
  • type(obj).__name__: The .__name__ attribute on the returned type object gives just the plain string name of the class, such as "Dog", without the module prefix.
  • Alternative – isinstance(): While type() checks the exact class, isinstance(d, Dog) also returns True for subclasses. Use type() when you need an exact match, and isinstance() when inheritance should be considered.

Exercise 23: Type Checking with isinstance() & issubclass()

Problem Statement: Write a Python program that uses isinstance() to check whether an object is an instance of a given class, and issubclass() to check whether one class is a subclass of another.

Purpose: This exercise teaches you two of Python’s most important type-inspection tools. Unlike type(), both functions are inheritance-aware, making them essential for writing flexible, safe code that handles mixed object types gracefully.

Given Input: A Dog class inheriting from Animal, and an instance d = Dog()

Expected Output:

Is d an instance of Dog? True
Is d an instance of Animal? True
Is Dog a subclass of Animal? True
Is Animal a subclass of Dog? False
▼ Hint
  • Define an Animal base class and a Dog subclass that inherits from it.
  • Use isinstance(obj, ClassName) to check if an object is an instance of a class or any of its parent classes.
  • Use issubclass(ChildClass, ParentClass) to check if one class inherits from another.
  • Note that isinstance(d, Animal) returns True even though d was created from Dog, because Dog inherits from Animal.
▼ Solution & Explanation
class Animal:
    pass

class Dog(Animal):
    pass

d = Dog()

print("Is d an instance of Dog?", isinstance(d, Dog))
print("Is d an instance of Animal?", isinstance(d, Animal))
print("Is Dog a subclass of Animal?", issubclass(Dog, Animal))
print("Is Animal a subclass of Dog?", issubclass(Animal, Dog))Code language: Python (python)

Explanation:

  • isinstance(d, Dog): Returns True because d was directly created from the Dog class.
  • isinstance(d, Animal): Also returns True because Dog inherits from Animal. This inheritance-awareness is what makes isinstance() more useful than a direct type() comparison in most real-world scenarios.
  • issubclass(Dog, Animal): Returns True because Dog is defined with Animal as its parent. This works on classes themselves, not instances.
  • issubclass(Animal, Dog): Returns False because the relationship is one-directional. The parent does not inherit from the child.

Exercise 24: Vector Addition Using add Overloading

Problem Statement: Write a Python program that creates a Vector class representing a 2D vector, and implements the __add__ dunder method so that two Vector objects can be added using the + operator.

Purpose: This exercise introduces operator overloading, a powerful OOP feature that lets your custom classes behave like built-in types. Implementing __add__ makes your objects integrate naturally with Python’s syntax.

Given Input: v1 = Vector(2, 3) and v2 = Vector(4, 1)

Expected Output: Vector(6, 4)

▼ Hint
  • Define a Vector class with x and y attributes in __init__.
  • Implement __add__(self, other) to return a new Vector whose x is self.x + other.x and y is self.y + other.y.
  • Implement __repr__ or __str__ to control how the object is printed.
  • Test it by writing v1 + v2 and printing the result.
▼ Solution & Explanation
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(4, 1)

result = v1 + v2
print(result)Code language: Python (python)

Explanation:

  • __add__(self, other): Python calls this method automatically when the + operator is used between two Vector objects. self is the left operand and other is the right.
  • Returns a new Vector: Rather than modifying self in place, the method creates and returns a fresh Vector object. This keeps objects immutable under addition, which is the expected behaviour for mathematical types.
  • __repr__: Defines the string representation of the object used by print() and the interactive shell. Without it, print(result) would display something like <__main__.Vector object at 0x...>.
  • Operator overloading: The same pattern applies to other operators such as __sub__ for -, __mul__ for *, and __eq__ for ==, making custom classes feel like native Python types.

Exercise 25: Cart Length Using len Overloading

Problem Statement: Write a Python program that creates a Cart class that stores a list of items, and implements __len__ so that calling len(cart) returns the number of items currently in the cart.

Purpose: This exercise introduces the __len__ dunder method, which lets your custom class integrate with Python’s built-in len() function. This is part of Python’s data model and makes your objects behave like native sequences or containers.

Given Input: A cart with items ["apple", "banana", "mango"]

Expected Output: Number of items in cart: 3

▼ Hint
  • Define a Cart class with an __init__ that initialises an empty list self.items = [].
  • Add an add_item(item) method that appends to self.items.
  • Implement __len__(self) to return len(self.items).
  • Once __len__ is defined, Python will use it whenever you call len(cart) on your object.
▼ Solution & Explanation
class Cart:
    def __init__(self):
        self.items = []

    def add_item(self, item):
        self.items.append(item)

    def __len__(self):
        return len(self.items)

cart = Cart()
cart.add_item("apple")
cart.add_item("banana")
cart.add_item("mango")

print("Number of items in cart:", len(cart))Code language: Python (python)

Explanation:

  • self.items = []: Initialises the internal list that stores cart contents. Each instance gets its own independent list.
  • __len__(self): Python calls this automatically when len(cart) is used. It delegates to the built-in len() on the internal list, which already knows how to count its elements.
  • Protocol-based design: By implementing __len__, your Cart object now participates in Python’s sequence protocol. This also enables truthiness checks: an object with __len__ returning 0 is treated as False in a boolean context.
  • Related dunder methods: You can extend this pattern with __getitem__ to support indexing (e.g., cart[0]) and __iter__ to support looping over the cart directly.

Exercise 26: Private Balance with Property Getter & Setter

Problem Statement: Write a Python program that creates a BankAccount class where the balance is stored as a private attribute __balance, and exposed safely through a @property getter and a setter that validates the value before updating it.

Purpose: This exercise demonstrates encapsulation, one of the four pillars of OOP. Using name-mangled private attributes alongside @property lets you control how external code reads and modifies internal state, preventing invalid data from being assigned.

Given Input: BankAccount(1000), then deposit 500, then attempt to set balance to -200

Expected Output:

Current balance: 1000
Current balance: 1500
Invalid balance. Must be non-negative.
▼ Hint
  • Store the balance as self.__balance in __init__. The double underscore triggers Python’s name mangling, making it harder to access directly from outside the class.
  • Define a @property method named balance that returns self.__balance.
  • Define a @balance.setter that checks if the new value is non-negative before assigning it, and prints an error message if not.
  • Add a deposit(amount) method that updates the balance using the setter: self.balance = self.__balance + amount.
▼ Solution & Explanation
class BankAccount:
    def __init__(self, initial_balance):
        self.__balance = initial_balance

    @property
    def balance(self):
        return self.__balance

    @balance.setter
    def balance(self, amount):
        if amount < 0:
            print("Invalid balance. Must be non-negative.")
        else:
            self.__balance = amount

    def deposit(self, amount):
        self.balance = self.__balance + amount

account = BankAccount(1000)
print("Current balance:", account.balance)

account.deposit(500)
print("Current balance:", account.balance)

account.balance = -200Code language: Python (python)

Explanation:

  • self.__balance: The double underscore prefix triggers name mangling. Python internally renames this to _BankAccount__balance, making it inaccessible as account.__balance from outside the class.
  • @property: Turns the balance method into a read-only attribute-style accessor. Callers write account.balance instead of account.balance().
  • @balance.setter: Intercepts any assignment to account.balance = value and runs the validation logic before updating __balance. This is where business rules are enforced.
  • deposit() uses the setter: By writing self.balance = ... inside the method instead of self.__balance = ..., the deposit operation still passes through the validation logic, keeping the rules consistent.

Exercise 27: Callable Object Class Using call

Problem Statement: Write a Python program that creates a Multiplier class which stores a factor, and implements __call__ so that an instance of the class can be invoked directly like a function to multiply a given number by that factor.

Purpose: This exercise introduces the __call__ dunder method, which makes any object callable. This pattern is commonly used in machine learning (layer objects), decorators, and anywhere you need a stateful function-like object.

Given Input: Multiplier(3) called with 10, and Multiplier(5) called with 7

Expected Output:

30
35
▼ Hint
  • Define a Multiplier class with an __init__ that stores the factor as self.factor.
  • Implement __call__(self, value) to return self.factor * value.
  • Create an instance with triple = Multiplier(3), then call it like a function: triple(10).
▼ Solution & Explanation
class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, value):
        return self.factor * value

triple = Multiplier(3)
penta = Multiplier(5)

print(triple(10))
print(penta(7))Code language: Python (python)

Explanation:

  • self.factor: Stores the multiplier value at construction time. This is the state that distinguishes callable objects from plain functions: the object remembers its configuration between calls.
  • __call__(self, value): Python invokes this method whenever the object is called using parentheses, such as triple(10). Without this method, doing so would raise a TypeError: 'Multiplier' object is not callable.
  • Callable objects vs. plain functions: A regular function cannot carry persistent state between calls without using global variables or closures. A callable class instance stores that state cleanly in its attributes.
  • Real-world use: This pattern is used extensively in frameworks like PyTorch, where neural network layers are callable objects that store weights as instance attributes and process input data through __call__.

Exercise 28: Flight Class with Passenger Capacity Check

Problem Statement: Write a Python program that creates a Passenger class and a Flight class. The Flight class should manage a list of Passenger objects and block further bookings when the seat capacity is reached.

Purpose: This exercise models a real-world object composition scenario where one class owns and manages a collection of another class. It also teaches boundary enforcement, where business rules (capacity limits) are built directly into the class methods.

Given Input: A Flight with capacity 2, then three booking attempts

Expected Output:

Alice booked on Flight AI202.
Bob booked on Flight AI202.
Sorry, Flight AI202 is fully booked.
▼ Hint
  • Define a Passenger class with a name attribute.
  • Define a Flight class with flight_number, capacity, and an empty passengers list in __init__.
  • Add a book(passenger) method that checks len(self.passengers) < self.capacity before appending.
  • Print a confirmation message on success, or a “fully booked” message when capacity is exceeded.
▼ Solution & Explanation
class Passenger:
    def __init__(self, name):
        self.name = name

class Flight:
    def __init__(self, flight_number, capacity):
        self.flight_number = flight_number
        self.capacity = capacity
        self.passengers = []

    def book(self, passenger):
        if len(self.passengers) < self.capacity:
            self.passengers.append(passenger)
            print(f"{passenger.name} booked on Flight {self.flight_number}.")
        else:
            print(f"Sorry, Flight {self.flight_number} is fully booked.")

flight = Flight("AI202", 2)
flight.book(Passenger("Alice"))
flight.book(Passenger("Bob"))
flight.book(Passenger("Charlie"))Code language: Python (python)

Explanation:

  • class Passenger: A simple data class that holds a passenger’s name. In a larger system, it might also store a passport number, seat preference, or booking reference.
  • self.passengers = []: Each Flight instance maintains its own list. This is object composition: the Flight owns a collection of Passenger objects.
  • Capacity check in book(): The guard condition len(self.passengers) < self.capacity enforces the business rule at the point of data entry, preventing the list from growing beyond its limit.
  • Passing objects as arguments: flight.book(Passenger("Alice")) creates a Passenger object inline and passes it directly. The Flight method then works with the object’s attributes, demonstrating inter-object communication.

Exercise 29: Zoo Class that Feeds All Animals

Problem Statement: Write a Python program that defines an Animal base class with an eat() method, creates a few subclasses with their own eat() implementations, and builds a Zoo class that holds a list of animals and calls eat() on all of them via a feed_all() method.

Purpose: This exercise combines composition with polymorphism. The Zoo class does not need to know the specific type of each animal; it simply calls the shared eat() interface on every object in its collection, and each animal responds in its own way.

Given Input: A zoo containing a Lion, a Elephant, and a Parrot

Expected Output:

Lion eats meat.
Elephant eats grass.
Parrot eats seeds.
▼ Hint
  • Define an Animal base class with an eat() method that returns a generic string.
  • Create Lion, Elephant, and Parrot subclasses, each overriding eat() with a specific message.
  • Define a Zoo class with an add_animal(animal) method and a feed_all() method that loops through all stored animals and calls eat() on each.
▼ Solution & Explanation
class Animal:
    def eat(self):
        return "eating."

class Lion(Animal):
    def eat(self):
        return "Lion eats meat."

class Elephant(Animal):
    def eat(self):
        return "Elephant eats grass."

class Parrot(Animal):
    def eat(self):
        return "Parrot eats seeds."

class Zoo:
    def __init__(self):
        self.animals = []

    def add_animal(self, animal):
        self.animals.append(animal)

    def feed_all(self):
        for animal in self.animals:
            print(animal.eat())

zoo = Zoo()
zoo.add_animal(Lion())
zoo.add_animal(Elephant())
zoo.add_animal(Parrot())

zoo.feed_all()Code language: Python (python)

Explanation:

  • class Zoo: Acts as a container class that manages a heterogeneous collection of Animal objects. It does not care about the specific subtype of each animal it holds.
  • feed_all(): Iterates over self.animals and calls eat() on each. Because every animal overrides eat(), Python dispatches to the correct subclass method automatically. This is runtime polymorphism.
  • Composition over inheritance: Zoo does not inherit from Animal. Instead, it contains animals. This is a “has-a” relationship, as opposed to the “is-a” relationship of inheritance.
  • Extensibility: Adding a new animal type (e.g., Penguin) requires only creating a new subclass with its own eat(). The Zoo and feed_all() code need no changes.

Exercise 30: Character Class with Auto Level-Up Logic

Problem Statement: Write a Python program that creates a Character class with health, exp, and level attributes. The character should automatically level up and reset exp whenever accumulated experience reaches or exceeds 100.

Purpose: This exercise shows how to embed game logic directly into a class using a method that manages state transitions. It also demonstrates how to handle overflow (excess exp after levelling up) and keep multiple related attributes in sync.

Given Input: Character("Aria", health=100), then gain_exp(60) twice

Expected Output:

Aria gained 60 exp. (Total: 60)
Aria gained 60 exp. Level up! Now Level 2. (Remaining exp: 20)
▼ Hint
  • Define Character.__init__ with name, health, and set self.exp = 0 and self.level = 1 as defaults.
  • In gain_exp(amount), add amount to self.exp, then check if self.exp >= 100.
  • If a level-up occurs, increment self.level, subtract 100 from self.exp to carry over the remainder, and print the level-up message.
  • If no level-up occurs, print a simpler message showing the current total exp.
▼ Solution & Explanation
class Character:
    def __init__(self, name, health):
        self.name = name
        self.health = health
        self.exp = 0
        self.level = 1

    def gain_exp(self, amount):
        self.exp += amount
        if self.exp >= 100:
            self.exp -= 100
            self.level += 1
            print(f"{self.name} gained {amount} exp. Level up! Now Level {self.level}. (Remaining exp: {self.exp})")
        else:
            print(f"{self.name} gained {amount} exp. (Total: {self.exp})")

hero = Character("Aria", health=100)
hero.gain_exp(60)
hero.gain_exp(60)Code language: Python (python)

Explanation:

  • self.exp = 0 and self.level = 1: These default values are set in __init__ rather than passed as parameters, since every new character reasonably starts at level 1 with zero experience.
  • self.exp += amount: Accumulates experience over multiple calls. The running total is checked after every gain, not just once.
  • self.exp -= 100: Subtracts exactly 100 instead of resetting to zero, so any exp earned beyond the threshold carries over to the next level. For example, gaining 60 exp on top of 60 gives 120 total; after levelling up, 20 exp remains.
  • Extending the logic: This pattern supports multiple level-ups in one call (e.g., gaining 250 exp at once) by changing the if to a while self.exp >= 100 loop, making the system robust for large exp rewards.

Exercise 31: Playlist Class with Add, Remove & Shuffle

Problem Statement: Write a Python program that defines a Song class and a Playlist class. The Playlist should support adding songs, removing songs by title, and shuffling the order of the playlist randomly.

Purpose: This exercise reinforces object composition, list manipulation, and the use of the standard library. Managing a collection of objects with add, remove, and reorder operations is a pattern found in media players, task managers, and many real-world applications.

Given Input: A playlist with three songs, then a removal and a shuffle

Expected Output:

Playlist: Blinding Lights, Levitating, Peaches
Removed: Levitating
After shuffle: (order will vary)
▼ Hint
  • Define a Song class with title and artist attributes.
  • Define a Playlist class with an internal self.songs = [] list and an add_song(song) method that appends to it.
  • In remove_song(title), use a list comprehension to filter out the song whose title matches the given string.
  • In shuffle(), use random.shuffle(self.songs) from the random module to randomise the order in place.
▼ Solution & Explanation
import random

class Song:
    def __init__(self, title, artist):
        self.title = title
        self.artist = artist

class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []

    def add_song(self, song):
        self.songs.append(song)

    def remove_song(self, title):
        original_count = len(self.songs)
        self.songs = [s for s in self.songs if s.title != title]
        if len(self.songs) < original_count:
            print(f"Removed: {title}")
        else:
            print(f"Song '{title}' not found in playlist.")

    def shuffle(self):
        random.shuffle(self.songs)

    def display(self):
        titles = [s.title for s in self.songs]
        print(f"Playlist: {', '.join(titles)}")

playlist = Playlist("My Mix")
playlist.add_song(Song("Blinding Lights", "The Weeknd"))
playlist.add_song(Song("Levitating", "Dua Lipa"))
playlist.add_song(Song("Peaches", "Justin Bieber"))

playlist.display()
playlist.remove_song("Levitating")
playlist.shuffle()
print("After shuffle:", end=" ")
playlist.display()Code language: Python (python)

Explanation:

  • class Song: A lightweight data object holding a song’s title and artist. The Playlist stores these objects rather than plain strings, making it easy to display or filter by either attribute.
  • remove_song() with list comprehension: [s for s in self.songs if s.title != title] builds a new list that excludes the matching song and reassigns it to self.songs. Comparing the before and after count confirms whether a removal actually occurred.
  • random.shuffle(self.songs): Shuffles the list in place, meaning no new list is created. The order of self.songs is randomised directly, and the output will differ on every run.
  • display(): Uses a list comprehension to extract just the titles and joins them with ', '.join(), producing a clean single-line summary of the playlist’s current state.

Filed Under: Python, Python Exercises, Python Object-Oriented Programming (OOP)

Did you find this page helpful? Let others know about it. Sharing helps me continue to create free Python resources.

TweetF  sharein  shareP  Pin

About Vishal

I’m Vishal Hule, the Founder of PYnative.com. As a Python developer, I enjoy assisting students, developers, and learners. Follow me on Twitter.

Related Tutorial Topics:

Python Python Exercises Python Object-Oriented Programming (OOP)

All Coding Exercises:

C Exercises
C++ Exercises
Python Exercises

Python Exercises and Quizzes

Free coding exercises and quizzes cover Python basics, data structure, data analytics, and more.

  • 15+ Topic-specific Exercises and Quizzes
  • Each Exercise contains 25+ questions
  • Each Quiz contains 25 MCQ
Exercises
Quizzes

Loading comments... Please wait.

In: Python Python Exercises Python Object-Oriented Programming (OOP)
TweetF  sharein  shareP  Pin

  Python Exercises

  • All Python Exercises
  • Basic Exercise for Beginners
  • Intermediate Python Exercises
  • Input and Output Exercise
  • Loop Exercise
  • Functions Exercise
  • String Exercise
  • Data Structure Exercise
  • List Exercise
  • Dictionary Exercise
  • Set Exercise
  • Tuple Exercise
  • Date and Time Exercise
  • OOP Exercise
  • File Handling Exercise
  • Python JSON Exercise
  • Random Data Generation Exercise
  • NumPy Exercise
  • Pandas Exercise
  • Matplotlib Exercise
  • Python Database Exercise

 Explore Python

  • Python Tutorials
  • Python Exercises
  • Python Quizzes
  • Python Interview Q&A
  • Python Programs

All Python Topics

Python Basics Python Exercises Python Quizzes Python Interview Python File Handling Python OOP Python Date and Time Python Random Python Regex Python Pandas Python Databases Python MySQL Python PostgreSQL Python SQLite Python JSON

About PYnative

PYnative.com is for Python lovers. Here, You can get Tutorials, Exercises, and Quizzes to practice and improve your Python skills.

Follow Us

To get New Python Tutorials, Exercises, and Quizzes

  • Twitter
  • Facebook
  • Sitemap

Explore Python

  • Learn Python
  • Python Basics
  • Python Databases
  • Python Exercises
  • Python Quizzes
  • Online Python Code Editor
  • Python Tricks

Coding Exercises

  • C Exercises
  • C++ Exercises
  • Python Exercises

Legal Stuff

  • About Us
  • Contact Us

We use cookies to improve your experience. While using PYnative, you agree to have read and accepted our:

  • Terms Of Use
  • Privacy Policy
  • Cookie Policy

Copyright © 2018–2026 pynative.com