Chapter 21 Advanced 65 Questions

Practice Questions — Object-Oriented Programming in Python

← Back to Notes
7 Easy
11 Medium
11 Hard

Topic-Specific Questions

Question 1
Easy
What is the output of the following code?
class Dog:
    def __init__(self, name):
        self.name = name

d = Dog("Rex")
print(d.name)
The __init__ method sets self.name to the given argument.
Rex
Question 2
Easy
What is the output?
class Cat:
    def __init__(self, name, age):
        self.name = name
        self.age = age

c1 = Cat("Whiskers", 3)
c2 = Cat("Luna", 5)
print(c1.name, c2.age)
Each object has its own name and age.
Whiskers 5
Question 3
Easy
What is the output?
class Greeter:
    def __init__(self, name):
        self.name = name
    
    def greet(self):
        return f"Hello, {self.name}!"

g = Greeter("Aarav")
print(g.greet())
The greet method uses self.name to build the greeting.
Hello, Aarav!
Question 4
Easy
What is the output?
class Car:
    wheels = 4
    
    def __init__(self, brand):
        self.brand = brand

c1 = Car("Toyota")
c2 = Car("Honda")
print(c1.wheels)
print(c2.wheels)
print(Car.wheels)
wheels is a class variable shared by all instances.
4
4
4
Question 5
Easy
What is the output?
class Box:
    def __init__(self, length, width):
        self.length = length
        self.width = width
    
    def area(self):
        return self.length * self.width

b = Box(5, 3)
print(b.area())
print(type(b))
area() returns length * width. type() gives the class.
15
<class '__main__.Box'>
Question 6
Medium
What is the output?
class Counter:
    count = 0
    
    def __init__(self):
        Counter.count += 1
    
    def get_count(self):
        return Counter.count

a = Counter()
b = Counter()
c = Counter()
print(a.get_count())
print(Counter.count)
Each __init__ call increments the class variable Counter.count.
3
3
Question 7
Medium
What is the output?
class Item:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    
    def __str__(self):
        return f"{self.name}: Rs.{self.price}"
    
    def __repr__(self):
        return f"Item('{self.name}', {self.price})"

i = Item("Book", 250)
print(i)
print(repr(i))
print() uses __str__. repr() uses __repr__.
Book: Rs.250
Item('Book', 250)
Question 8
Medium
What is the output?
class MyClass:
    x = 10

a = MyClass()
b = MyClass()
a.x = 20

print(a.x)
print(b.x)
print(MyClass.x)
Assigning to a.x creates an instance variable on a, not modifying the class variable.
20
10
10
Question 9
Medium
What is the output?
class Stack:
    def __init__(self):
        self.items = []
    
    def push(self, item):
        self.items.append(item)
        return self
    
    def pop(self):
        return self.items.pop()
    
    def size(self):
        return len(self.items)

s = Stack()
s.push(1).push(2).push(3)
print(s.size())
print(s.pop())
print(s.size())
push() returns self, enabling method chaining.
3
3
2
Question 10
Medium
What is the output?
class Person:
    def __init__(self, name):
        self.__name = name
    
    def get_name(self):
        return self.__name

p = Person("Priya")
print(p.get_name())
try:
    print(p.__name)
except AttributeError:
    print("Cannot access")
Double underscore triggers name mangling.
Priya
Cannot access
Question 11
Medium
What is the output?
class Circle:
    pi = 3.14159
    
    def __init__(self, radius):
        self.radius = radius
    
    @property
    def area(self):
        return Circle.pi * self.radius ** 2

c = Circle(5)
print(round(c.area, 2))
c.radius = 10
print(round(c.area, 2))
@property makes area behave like an attribute, but it is computed each time.
78.54
314.16
Question 12
Hard
What is the output?
class A:
    data = []
    
    def add(self, item):
        self.data.append(item)

a1 = A()
a2 = A()
a1.add(1)
a2.add(2)
print(a1.data)
print(a2.data)
print(a1.data is a2.data)
data is a mutable class variable. All instances share the same list.
[1, 2]
[1, 2]
True
Question 13
Hard
What is the output?
class Tracker:
    def __init__(self):
        self.history = []
    
    def record(self, value):
        self.history.append(value)
        return self

t1 = Tracker()
t2 = Tracker()
t1.record("a").record("b")
t2.record("x")
print(t1.history)
print(t2.history)
print(t1.history is t2.history)
history is created in __init__ as an instance variable. Each tracker has its own list.
['a', 'b']
['x']
False
Question 14
Hard
What is the output?
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"({self.x}, {self.y})"

p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

print(p1 == p2)
print(p1 == p3)
print(p1 is p2)
points = [p1, p2, p3]
print(p2 in points)
__eq__ compares values. 'is' compares identity. 'in' uses __eq__.
True
False
False
True
Question 15
Hard
What is the output?
class Config:
    _instance = None
    
    def __init__(self, value):
        self.value = value
    
    @classmethod
    def get_instance(cls, value):
        if cls._instance is None:
            cls._instance = cls(value)
        return cls._instance

c1 = Config.get_instance("first")
c2 = Config.get_instance("second")
print(c1.value)
print(c2.value)
print(c1 is c2)
The class method creates the instance only once (singleton pattern).
first
first
True
Question 16
Hard
What is the output?
class Number:
    def __init__(self, value):
        self._value = value
    
    @property
    def value(self):
        print("Getting value")
        return self._value
    
    @value.setter
    def value(self, new_value):
        print("Setting value")
        self._value = new_value

n = Number(10)
print(n.value)
n.value = 20
print(n.value)
The property getter and setter print messages when accessed.
Getting value
10
Setting value
Getting value
20
Question 17
Medium
What is the difference between a class variable and an instance variable?
Think about where they are defined and who shares them.
A class variable is defined in the class body (outside any method) and is shared by all instances. A instance variable is defined using self.variable (usually in __init__) and belongs to one specific object. Changing a class variable through the class name affects all instances. Assigning through an instance creates a new instance variable that shadows the class variable.
Question 18
Hard
Why does assigning self.count += 1 (where count is a class variable) create an instance variable instead of modifying the class variable?
Expand self.count += 1 into its equivalent form.
self.count += 1 is equivalent to self.count = self.count + 1. On the right side, self.count reads the class variable (since no instance variable exists yet). Adding 1 produces a new value. On the left side, self.count = ... is an assignment through an instance, which creates a new instance variable. The class variable is never modified.

Mixed & Application Questions

Question 1
Easy
What is the output?
class Rectangle:
    def __init__(self, w, h):
        self.w = w
        self.h = h
    
    def area(self):
        return self.w * self.h
    
    def perimeter(self):
        return 2 * (self.w + self.h)

r = Rectangle(4, 6)
print(r.area())
print(r.perimeter())
Area = width * height. Perimeter = 2 * (width + height).
24
20
Question 2
Easy
What is the output?
class Pair:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    
    def sum(self):
        return self.a + self.b

p = Pair("Hello", " World")
print(p.sum())
print(Pair(3, 7).sum())
The + operator works for both strings and numbers.
Hello World
10
Question 3
Medium
What is the output?
class Student:
    def __init__(self, name, marks):
        self.name = name
        self.marks = marks
    
    def average(self):
        return sum(self.marks) / len(self.marks)

students = [
    Student("Aarav", [90, 85, 92]),
    Student("Priya", [88, 95, 82]),
    Student("Rohan", [75, 80, 70]),
]

for s in students:
    print(f"{s.name}: {s.average():.1f}")
Calculate the average for each student's marks.
Aarav: 89.0
Priya: 88.3
Rohan: 75.0
Question 4
Medium
What is the output?
class Multiplier:
    def __init__(self, factor):
        self.factor = factor
    
    def multiply(self, value):
        return self.factor * value

double = Multiplier(2)
triple = Multiplier(3)

results = [double.multiply(x) for x in range(1, 5)]
print(results)
print(triple.multiply(10))
double.factor = 2, triple.factor = 3.
[2, 4, 6, 8]
30
Question 5
Medium
What is the output?
class Account:
    def __init__(self, balance=0):
        self.balance = balance
        self.transactions = []
    
    def deposit(self, amount):
        self.balance += amount
        self.transactions.append(f"+{amount}")
    
    def withdraw(self, amount):
        self.balance -= amount
        self.transactions.append(f"-{amount}")

a = Account(100)
a.deposit(50)
a.withdraw(30)
a.deposit(20)
print(a.balance)
print(a.transactions)
Track the balance: 100 + 50 - 30 + 20.
140
['+50', '-30', '+20']
Question 6
Hard
What is the output?
class Node:
    def __init__(self, value, next_node=None):
        self.value = value
        self.next = next_node
    
    def __str__(self):
        result = str(self.value)
        current = self.next
        while current:
            result += f" -> {current.value}"
            current = current.next
        return result

c = Node(3)
b = Node(2, c)
a = Node(1, b)
print(a)
print(b)
Each node points to the next. __str__ follows the chain.
1 -> 2 -> 3
2 -> 3
Question 7
Hard
What is the output?
class Matrix:
    def __init__(self, rows):
        self.rows = rows
    
    def __eq__(self, other):
        return self.rows == other.rows
    
    def transpose(self):
        t = list(zip(*self.rows))
        return Matrix([list(row) for row in t])

m1 = Matrix([[1, 2], [3, 4]])
m2 = m1.transpose()
print(m2.rows)
print(m1 == m1.transpose().transpose())
Transposing swaps rows and columns. Transposing twice gives the original.
[[1, 3], [2, 4]]
True
Question 8
Hard
What is the output?
class Logger:
    _messages = []
    
    @classmethod
    def log(cls, message):
        cls._messages.append(message)
    
    @classmethod
    def get_logs(cls):
        return cls._messages.copy()

Logger.log("start")
Logger.log("process")

l1 = Logger()
l2 = Logger()
l1.log("end")

print(Logger.get_logs())
print(len(l2.get_logs()))
All log calls modify the same class-level list.
['start', 'process', 'end']
3
Question 9
Hard
What is the output?
class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
    
    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32
    
    @staticmethod
    def from_fahrenheit(f):
        return Temperature((f - 32) * 5/9)

t1 = Temperature(100)
print(t1.fahrenheit)

t2 = Temperature.from_fahrenheit(32)
print(t2._celsius)
print(t2.fahrenheit)
100C = 212F. 32F = 0C.
212.0
0.0
32.0
Question 10
Medium
What is the difference between @classmethod and @staticmethod?
What does each method receive as its first parameter?
@classmethod receives the class (cls) as its first parameter and can access/modify class state. @staticmethod receives neither self nor cls and is essentially a regular function scoped inside the class. Use classmethod for alternative constructors or operations on class data. Use staticmethod for utility functions logically grouped with the class.
Question 11
Hard
What is name mangling in Python? Why does Python use it instead of truly private attributes?
Double underscore attributes get renamed. Think about Python's philosophy.
Name mangling transforms self.__attr to self._ClassName__attr. It is not true privacy -- the attribute is still accessible if you know the mangled name. Python uses this approach because of its philosophy: 'We are all consenting adults here.' Python trusts developers to follow conventions rather than enforcing strict access control. The mangling prevents accidental name collisions in subclasses, which is its primary purpose.

Multiple Choice Questions

MCQ 1
What keyword is used to define a class in Python?
  • A. define
  • B. class
  • C. struct
  • D. object
Answer: B
B is correct. The class keyword defines a class. Option A is not a Python keyword. Options C and D are not used for class definitions.
MCQ 2
What is __init__() in a Python class?
  • A. A destructor that cleans up objects
  • B. A constructor that initializes object attributes
  • C. A method that prints the object
  • D. A class variable
Answer: B
B is correct. __init__ is the constructor/initializer. It runs automatically when an object is created and sets up its initial state.
MCQ 3
What does 'self' refer to in a class method?
  • A. The class itself
  • B. The current instance (object) calling the method
  • C. The parent class
  • D. The module the class is in
Answer: B
B is correct. self refers to the specific instance that the method is being called on. When obj.method() is called, self is obj.
MCQ 4
What is an instance of a class called?
  • A. A variable
  • B. An object
  • C. A function
  • D. A module
Answer: B
B is correct. An instance of a class is called an object. A class is the blueprint; an object is a specific entity created from that blueprint.
MCQ 5
How do you create an object from a class named Dog?
  • A. Dog.new()
  • B. new Dog()
  • C. Dog()
  • D. create Dog()
Answer: C
C is correct. In Python, you create objects by calling the class name as if it were a function: Dog(). This is unlike Java/C++ which use the new keyword.
MCQ 6
What is the difference between self.name and name inside __init__(self, name)?
  • A. They are the same thing
  • B. self.name is an instance variable stored on the object; name is a parameter that exists only during the method call
  • C. name is the class variable; self.name is the local variable
  • D. self.name is read-only; name is writable
Answer: B
B is correct. name is a parameter (local variable) that disappears when __init__ finishes. self.name is an instance variable that persists as an attribute of the object.
MCQ 7
What happens when you assign to a class variable through an instance?
  • A. The class variable is modified for all instances
  • B. A new instance variable is created that shadows the class variable
  • C. A TypeError is raised
  • D. The assignment is ignored
Answer: B
B is correct. Assigning through an instance (obj.x = 5) always creates or modifies an instance variable, never a class variable. The class variable remains unchanged, but the new instance variable shadows it for that specific object.
MCQ 8
What does __str__() do?
  • A. Converts the object to an integer
  • B. Returns a human-readable string representation used by print()
  • C. Creates a string variable in the class
  • D. Checks if the object is a string
Answer: B
B is correct. __str__ defines the string representation used by print() and str(). Without it, printing an object shows something like <__main__.MyClass object at 0x...>.
MCQ 9
What is the naming convention for Python class names?
  • A. snake_case (my_class)
  • B. camelCase (myClass)
  • C. PascalCase (MyClass)
  • D. ALL_CAPS (MY_CLASS)
Answer: C
C is correct. Python class names use PascalCase (also called CamelCase with initial capital): Student, BankAccount, FileHandler. Methods and variables use snake_case.
MCQ 10
What does @property do?
  • A. Makes a variable immutable
  • B. Turns a method into an attribute-like accessor
  • C. Creates a class variable
  • D. Makes the method static
Answer: B
B is correct. @property turns a method into something that can be accessed like an attribute (without parentheses). It allows you to add getter/setter logic while keeping the clean obj.attr syntax.
MCQ 11
What is the purpose of __repr__() vs __str__()?
  • A. They do the same thing
  • B. __str__ is for humans (print), __repr__ is for developers (debugging/console)
  • C. __repr__ is for humans, __str__ is for developers
  • D. __repr__ converts to a number, __str__ converts to a string
Answer: B
B is correct. __str__ provides a human-friendly string (used by print). __repr__ provides an unambiguous developer-friendly string (used in the console, repr(), and when objects are in containers like lists).
MCQ 12
What does Python's name mangling do with self.__attr?
  • A. Makes the attribute truly private and inaccessible
  • B. Renames it to self._ClassName__attr
  • C. Deletes the attribute after __init__
  • D. Encrypts the attribute value
Answer: B
B is correct. Python renames __attr to _ClassName__attr. It is not true privacy -- the attribute can still be accessed with the mangled name. The primary purpose is preventing accidental name collisions in inheritance.
MCQ 13
What is the first parameter of a @classmethod?
  • A. self (the instance)
  • B. cls (the class)
  • C. args (the arguments)
  • D. It has no required first parameter
Answer: B
B is correct. A @classmethod receives the class itself as the first parameter, conventionally named cls. This allows it to create instances (cls(args)) and access class variables.
MCQ 14
If only __repr__ is defined (no __str__), what does print(obj) use?
  • A. It prints nothing
  • B. It raises an error
  • C. It uses __repr__ as a fallback
  • D. It prints the object's memory address
Answer: C
C is correct. If __str__ is not defined, Python falls back to __repr__ for print() and str(). If neither is defined, it uses the default representation showing the class name and memory address.
MCQ 15
What is the purpose of __eq__(self, other)?
  • A. It assigns a value to the object
  • B. It defines how == works for objects of this class
  • C. It checks if the object exists
  • D. It compares the memory addresses of two objects
Answer: B
B is correct. __eq__ defines the behavior of the == operator. Without it, == checks identity (same object in memory), not value equality.
MCQ 16
Is 'self' a keyword in Python?
  • A. Yes, it is a reserved keyword
  • B. No, it is a convention; any name could be used
  • C. Yes, but only inside __init__
  • D. No, it is automatically created by Python
Answer: B
B is correct. self is not a keyword -- it is a strong convention. You could technically use this or me, but doing so would confuse every Python developer who reads your code.
MCQ 17
What happens if you define a class variable as a list and append to it through an instance?
  • A. A new instance variable is created
  • B. The class variable list is modified for ALL instances
  • C. An error is raised
  • D. Only that instance's copy is modified
Answer: B
B is correct. self.data.append(x) does not create a new instance variable because it is not an assignment. It modifies the existing list object in place. Since the list is a class variable, the modification is visible to all instances.
MCQ 18
What is returned by type(obj) if obj is an instance of class Student?
  • A. 'Student'
  • B. <class 'Student'>
  • C. object
  • D. Student()
Answer: B
B is correct. type(obj) returns the class of the object, which displays as <class '__main__.Student'> (or <class 'Student'> in simplified form).
MCQ 19
Can a class have multiple __init__ methods (method overloading)?
  • A. Yes, like Java
  • B. No, the last __init__ defined replaces all previous ones
  • C. Yes, but only with different numbers of parameters
  • D. No, Python does not support __init__
Answer: B
B is correct. Python does not support method overloading. If you define __init__ twice, the second definition replaces the first. Use default parameters, *args, or @classmethod alternative constructors instead.
MCQ 20
What does returning NotImplemented from __eq__ do?
  • A. Raises a NotImplementedError
  • B. Returns False
  • C. Tells Python to try the other operand's __eq__ method
  • D. Makes the comparison always True
Answer: C
C is correct. Returning NotImplemented (not raising NotImplementedError) is a signal to Python that this comparison is not supported. Python will then try the other operand's __eq__ method. If that also returns NotImplemented, the comparison falls back to identity comparison.

Coding Challenges

Challenge 1: Student Gradebook

Easy
Create a Student class with name and marks (list). Add methods: add_mark(mark), average(), highest(), lowest(), and __str__. Test with two students.
Sample Input
(No input required)
Sample Output
Aarav - Avg: 88.7, Highest: 95, Lowest: 82 Priya - Avg: 91.0, Highest: 97, Lowest: 85
Use instance variables for name and marks. marks should be a list.
class Student:
    def __init__(self, name, marks=None):
        self.name = name
        self.marks = marks if marks else []
    
    def add_mark(self, mark):
        self.marks.append(mark)
    
    def average(self):
        return sum(self.marks) / len(self.marks) if self.marks else 0
    
    def highest(self):
        return max(self.marks) if self.marks else 0
    
    def lowest(self):
        return min(self.marks) if self.marks else 0
    
    def __str__(self):
        return f"{self.name} - Avg: {self.average():.1f}, Highest: {self.highest()}, Lowest: {self.lowest()}"

s1 = Student("Aarav", [88, 92, 82, 95, 86])
s2 = Student("Priya", [91, 97, 85, 88, 94])
print(s1)
print(s2)

Challenge 2: Bank Account with Transaction History

Easy
Create a BankAccount class with owner, balance, and a transactions list. Implement deposit(amount), withdraw(amount) with validation, and show_history(). The class variable should track total accounts created.
Sample Input
(No input required)
Sample Output
Aarav's balance: 1300 Transactions: ['+500', '-200', '+1000'] Total accounts: 2
Withdraw should fail if insufficient funds. Use a class variable for account count.
class BankAccount:
    total_accounts = 0
    
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
        self.transactions = []
        BankAccount.total_accounts += 1
    
    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
            self.transactions.append(f'+{amount}')
    
    def withdraw(self, amount):
        if amount > self.balance:
            print(f"Insufficient funds for {self.owner}")
            return
        if amount > 0:
            self.balance -= amount
            self.transactions.append(f'-{amount}')
    
    def show_history(self):
        return self.transactions

a1 = BankAccount("Aarav", 0)
a1.deposit(500)
a1.withdraw(200)
a1.deposit(1000)

a2 = BankAccount("Priya", 500)

print(f"{a1.owner}'s balance: {a1.balance}")
print(f"Transactions: {a1.show_history()}")
print(f"Total accounts: {BankAccount.total_accounts}")

Challenge 3: Todo List Manager

Easy
Create a TodoList class with methods: add(task), complete(index), remove(index), pending() (returns incomplete tasks), and __str__. Each task should track its completion status.
Sample Input
(No input required)
Sample Output
[x] Buy groceries [ ] Study Python [ ] Exercise Pending: 2 tasks
Store tasks as a list of dictionaries with 'task' and 'done' keys.
class TodoList:
    def __init__(self):
        self.tasks = []
    
    def add(self, task):
        self.tasks.append({'task': task, 'done': False})
    
    def complete(self, index):
        if 0 <= index < len(self.tasks):
            self.tasks[index]['done'] = True
    
    def remove(self, index):
        if 0 <= index < len(self.tasks):
            self.tasks.pop(index)
    
    def pending(self):
        return [t for t in self.tasks if not t['done']]
    
    def __str__(self):
        lines = []
        for t in self.tasks:
            status = 'x' if t['done'] else ' '
            lines.append(f"[{status}] {t['task']}")
        return '\n'.join(lines)

todo = TodoList()
todo.add("Buy groceries")
todo.add("Study Python")
todo.add("Exercise")
todo.complete(0)
print(todo)
print(f"Pending: {len(todo.pending())} tasks")

Challenge 4: Fraction Class with Arithmetic

Medium
Create a Fraction class that stores numerator and denominator. Implement add(other) and multiply(other) methods that return new Fraction objects. Include a simplify() method using GCD, __str__, and __eq__.
Sample Input
(No input required)
Sample Output
1/2 + 1/3 = 5/6 2/3 * 3/4 = 1/2 2/4 == 1/2: True
Always simplify fractions. Handle negative fractions. Do not use the fractions module.
def gcd(a, b):
    while b:
        a, b = b, a % b
    return a

class Fraction:
    def __init__(self, num, den):
        if den == 0:
            raise ValueError("Denominator cannot be zero")
        if den < 0:
            num, den = -num, -den
        common = gcd(abs(num), abs(den))
        self.num = num // common
        self.den = den // common
    
    def add(self, other):
        new_num = self.num * other.den + other.num * self.den
        new_den = self.den * other.den
        return Fraction(new_num, new_den)
    
    def multiply(self, other):
        return Fraction(self.num * other.num, self.den * other.den)
    
    def __eq__(self, other):
        return self.num == other.num and self.den == other.den
    
    def __str__(self):
        return f"{self.num}/{self.den}"

a = Fraction(1, 2)
b = Fraction(1, 3)
print(f"{a} + {b} = {a.add(b)}")

c = Fraction(2, 3)
d = Fraction(3, 4)
print(f"{c} * {d} = {c.multiply(d)}")

print(f"2/4 == 1/2: {Fraction(2, 4) == Fraction(1, 2)}")

Challenge 5: Inventory Management System

Medium
Create a Product class (name, price, quantity) and an Inventory class that manages a list of products. Inventory should have: add_product(product), remove_product(name), find_product(name), total_value() (sum of price*quantity), and low_stock(threshold) that returns products below the threshold.
Sample Input
(No input required)
Sample Output
Total value: Rs.18750 Low stock (< 10): ['Mouse'] Found: Keyboard - Rs.500 x 15
Use two classes. Product should have __str__. Inventory should manage a list.
class Product:
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity
    
    def __str__(self):
        return f"{self.name} - Rs.{self.price} x {self.quantity}"

class Inventory:
    def __init__(self):
        self.products = []
    
    def add_product(self, product):
        self.products.append(product)
    
    def remove_product(self, name):
        self.products = [p for p in self.products if p.name != name]
    
    def find_product(self, name):
        for p in self.products:
            if p.name == name:
                return p
        return None
    
    def total_value(self):
        return sum(p.price * p.quantity for p in self.products)
    
    def low_stock(self, threshold=10):
        return [p.name for p in self.products if p.quantity < threshold]

inv = Inventory()
inv.add_product(Product("Keyboard", 500, 15))
inv.add_product(Product("Mouse", 250, 5))
inv.add_product(Product("Monitor", 8000, 1))

print(f"Total value: Rs.{inv.total_value()}")
print(f"Low stock (< 10): {inv.low_stock(10)}")
print(f"Found: {inv.find_product('Keyboard')}")

Challenge 6: Playlist Manager

Medium
Create a Song class (title, artist, duration_seconds) and a Playlist class. Playlist methods: add(song), remove(title), total_duration() (formatted as MM:SS), shuffle() (return shuffled copy), and __str__ (numbered list of songs).
Sample Input
(No input required)
Sample Output
1. Shape of You - Ed Sheeran (3:53) 2. Believer - Imagine Dragons (3:24) 3. Blinding Lights - The Weeknd (3:20) Total duration: 10:37
Song duration stored in seconds. Display as M:SS.
class Song:
    def __init__(self, title, artist, duration_seconds):
        self.title = title
        self.artist = artist
        self.duration = duration_seconds
    
    def formatted_duration(self):
        minutes = self.duration // 60
        seconds = self.duration % 60
        return f"{minutes}:{seconds:02d}"
    
    def __str__(self):
        return f"{self.title} - {self.artist} ({self.formatted_duration()})"

class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add(self, song):
        self.songs.append(song)
    
    def remove(self, title):
        self.songs = [s for s in self.songs if s.title != title]
    
    def total_duration(self):
        total = sum(s.duration for s in self.songs)
        minutes = total // 60
        seconds = total % 60
        return f"{minutes}:{seconds:02d}"
    
    def __str__(self):
        lines = [f"{i+1}. {song}" for i, song in enumerate(self.songs)]
        return '\n'.join(lines)

pl = Playlist("Favorites")
pl.add(Song("Shape of You", "Ed Sheeran", 233))
pl.add(Song("Believer", "Imagine Dragons", 204))
pl.add(Song("Blinding Lights", "The Weeknd", 200))
print(pl)
print(f"Total duration: {pl.total_duration()}")

Challenge 7: Vector Class with Operator Methods

Hard
Create a Vector2D class with x and y. Implement __add__ (vector addition), __sub__ (vector subtraction), __mul__ (scalar multiplication), __eq__ (equality), __abs__ (magnitude), and __str__. Test thoroughly.
Sample Input
(No input required)
Sample Output
v1 + v2 = (4, 6) v1 - v2 = (-2, -2) v1 * 3 = (3, 6) |v2| = 5.0 v1 == Vector2D(1, 2): True
Return new Vector2D objects from operations (do not modify originals).
class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector2D(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        return Vector2D(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        return Vector2D(self.x * scalar, self.y * scalar)
    
    def __abs__(self):
        return (self.x**2 + self.y**2) ** 0.5
    
    def __eq__(self, other):
        if not isinstance(other, Vector2D):
            return NotImplemented
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"({self.x}, {self.y})"
    
    def __repr__(self):
        return f"Vector2D({self.x}, {self.y})"

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

print(f"v1 + v2 = {v1 + v2}")
print(f"v1 - v2 = {v1 - v2}")
print(f"v1 * 3 = {v1 * 3}")
print(f"|v2| = {abs(v2)}")
print(f"v1 == Vector2D(1, 2): {v1 == Vector2D(1, 2)}")

Challenge 8: Linked List Implementation

Hard
Implement a singly linked list using Node and LinkedList classes. LinkedList should support: append(value), prepend(value), delete(value), search(value) -> bool, length(), and __str__ (display as 'a -> b -> c -> None').
Sample Input
(No input required)
Sample Output
1 -> 2 -> 3 -> 4 -> None Length: 4 Search 3: True After delete 2: 1 -> 3 -> 4 -> None
Do not use Python lists. Build the data structure from scratch using Node objects.
class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None
    
    def append(self, value):
        new_node = Node(value)
        if not self.head:
            self.head = new_node
            return
        current = self.head
        while current.next:
            current = current.next
        current.next = new_node
    
    def prepend(self, value):
        new_node = Node(value)
        new_node.next = self.head
        self.head = new_node
    
    def delete(self, value):
        if not self.head:
            return
        if self.head.value == value:
            self.head = self.head.next
            return
        current = self.head
        while current.next:
            if current.next.value == value:
                current.next = current.next.next
                return
            current = current.next
    
    def search(self, value):
        current = self.head
        while current:
            if current.value == value:
                return True
            current = current.next
        return False
    
    def length(self):
        count = 0
        current = self.head
        while current:
            count += 1
            current = current.next
        return count
    
    def __str__(self):
        parts = []
        current = self.head
        while current:
            parts.append(str(current.value))
            current = current.next
        parts.append("None")
        return " -> ".join(parts)

ll = LinkedList()
for v in [1, 2, 3, 4]:
    ll.append(v)
print(ll)
print(f"Length: {ll.length()}")
print(f"Search 3: {ll.search(3)}")
ll.delete(2)
print(f"After delete 2: {ll}")

Challenge 9: Class Registry with @classmethod

Hard
Create a Shape class with a class-level registry. Each shape subclass (Circle, Rectangle) registers itself. Implement a from_dict(data) classmethod that creates the right shape based on a 'type' key. Include area() and __str__ for each shape.
Sample Input
(No input required)
Sample Output
Circle(radius=5) area: 78.54 Rectangle(4x6) area: 24 Registered shapes: ['circle', 'rectangle']
Use a class variable dictionary for the registry. Use @classmethod for the factory.
class Shape:
    _registry = {}
    
    @classmethod
    def register(cls, shape_type, shape_class):
        cls._registry[shape_type] = shape_class
    
    @classmethod
    def from_dict(cls, data):
        shape_type = data.get('type')
        shape_class = cls._registry.get(shape_type)
        if not shape_class:
            raise ValueError(f"Unknown shape: {shape_type}")
        return shape_class(**{k: v for k, v in data.items() if k != 'type'})
    
    @classmethod
    def registered_shapes(cls):
        return list(cls._registry.keys())

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return round(3.14159 * self.radius ** 2, 2)
    
    def __str__(self):
        return f"Circle(radius={self.radius})"

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def __str__(self):
        return f"Rectangle({self.width}x{self.height})"

Shape.register('circle', Circle)
Shape.register('rectangle', Rectangle)

c = Shape.from_dict({'type': 'circle', 'radius': 5})
r = Shape.from_dict({'type': 'rectangle', 'width': 4, 'height': 6})
print(f"{c} area: {c.area()}")
print(f"{r} area: {r.area()}")
print(f"Registered shapes: {Shape.registered_shapes()}")

Need to Review the Concepts?

Go back to the detailed notes for this chapter.

Read Chapter Notes

Want to learn Python with a live mentor?

Explore our Python course