Skip to content
C

Object-Oriented Programming

Classes and objects, the constructor and self, instance vs class variables, instance/class/static methods, encapsulation, all four types of inheritance, polymorphism and method overriding, abstraction with ABC, and magic methods.


Everything you've learned so far — variables, functions, data structures — works great for small programs. But as applications grow larger (think: a banking system, an e-commerce platform, a hospital management system), you need a way to model real-world things — like a "Student," a "Bank Account," or a "Product" — as organized units that bundle together their data and behavior. That's exactly what Object-Oriented Programming (OOP) is for.

This is one of the most important topics in this entire course — take your time with it.


1. What is Object-Oriented Programming?

What is it?

OOP is a programming style built around objects — units that combine data (attributes) and behavior (methods) together, modeled after real-world things.

Definition: Object-Oriented Programming is a programming paradigm that organizes code around objects, which bundle data (attributes) and behavior (methods) together.

Why do we use it?

  • Real-world modeling — a "Student" object naturally has a name, age, and marks (data), and behaviors like calculate_average() (methods).
  • Reusability — define a "template" (class) once, create as many objects from it as needed.
  • Organization — large applications become far easier to manage when related data and logic are grouped together.
  • Scalability — most professional, large-scale software (web frameworks, games, enterprise systems) is built using OOP principles.

How does it work?

OOP revolves around two core ideas:

  • A class is a blueprint/template that defines what data and behavior objects of that type will have.
  • An object is a specific instance created from that class, with its own actual data.

Think of a class like a cookie cutter, and objects like the individual cookies made using it — same shape (structure), different actual cookies (data).


2. Classes and Objects

Syntax

python
class ClassName: def __init__(self, parameters): self.attribute = value def method_name(self): # method body

Simple Example — A Student Class

python
class Student: def __init__(self, name, age, course): self.name = name self.age = age self.course = course def display_info(self): print(f"{self.name}, Age {self.age}, studying {self.course}") # Creating objects (instances) from the class student1 = Student("Aditi", 21, "Computer Science") student2 = Student("Rohan", 22, "Data Science") student1.display_info() student2.display_info()

Output:

Aditi, Age 21, studying Computer Science
Rohan, Age 22, studying Data Science

Explanation of the Code

  • class Student: defines the blueprint.
  • __init__ is a special method (called the constructor) that runs automatically whenever a new object is created — it sets up the object's initial data.
  • self refers to the specific object being created or used — it's how Python knows which student's name/age/course you're talking about.
  • student1 and student2 are two separate objects, each with their own independent data, even though both come from the same Student class.

Real-World Example

An e-commerce app might have a Product class, with each individual product (a specific shirt, a specific phone) as an object created from that class, each holding its own name, price, and stock quantity.

Common Mistakes

  • Forgetting self as the first parameter in every method inside a class.
  • Forgetting parentheses when creating an object: student1 = Student (this just refers to the class itself, not a new object).
  • Confusing the class (blueprint) with an object (an actual instance made from it).

Important Points

  • A class is a blueprint; an object is an actual instance created from it.
  • You can create as many objects as you want from a single class, each with independent data.
  • Class names conventionally use PascalCase (e.g., Student, BankAccount), while objects/variables use snake_case.

Practice

  1. Create a Book class with title, author, and price attributes, and a method to display the book's details. Create two book objects and display both.

3. The Constructor (__init__) and self

What is it?

__init__ is a special method that runs automatically the moment a new object is created — it's where you set up the object's initial attribute values. self represents the specific object currently being worked with.

Why do we use it?

Without a constructor, you'd have to manually set every attribute after creating each object, one line at a time. The constructor does this setup automatically and consistently, every time.

Simple Example

python
class Employee: def __init__(self, name, salary): self.name = name self.salary = salary emp1 = Employee("Zara", 50000) print(emp1.name, emp1.salary) # Zara 50000

Explanation

  • The moment Employee("Zara", 50000) runs, Python automatically calls __init__(self, "Zara", 50000) behind the scenes.
  • self.name = name stores the passed-in value as an attribute on that specific object — this is why emp1.name gives back "Zara".

Why self Matters

python
emp1 = Employee("Zara", 50000) emp2 = Employee("Karan", 60000) print(emp1.name) # Zara print(emp2.name) # Karan

self ensures that even though both objects were created from the same Employee class, each keeps its own independent data — changing emp1.salary never affects emp2.salary.

Common Mistakes

  • Forgetting to include self as the first parameter of __init__ (and every instance method) — this causes a TypeError.
  • Forgetting the self. prefix when setting an attribute, e.g. writing name = name instead of self.name = name — this creates a local variable that disappears immediately instead of actually storing data on the object.

Important Points

  • __init__ runs automatically when an object is created — you never call it directly.
  • self must be the first parameter of every regular (instance) method, though Python passes it automatically — you never pass it explicitly when calling the method.

4. Instance Variables vs Class Variables

What is it?

  • An instance variable belongs to a specific object — each object has its own separate copy.
  • A class variable is shared by all objects created from that class — there's only one copy, shared across every instance.

Simple Example

python
class Employee: company_name = "TechCorp" # class variable - shared by ALL employees def __init__(self, name, salary): self.name = name # instance variable - unique per employee self.salary = salary # instance variable - unique per employee emp1 = Employee("Zara", 50000) emp2 = Employee("Karan", 60000) print(emp1.company_name) # TechCorp print(emp2.company_name) # TechCorp print(emp1.name, emp2.name) # Zara Karan (different, as expected)

Explanation

  • company_name is defined directly inside the class (not inside __init__), so it belongs to the class itself, shared identically by every employee object.
  • name and salary are set inside __init__ using self., making them unique per object.

Real-World Example

A company name, tax rate, or interest rate might reasonably be a class variable (the same for every object of that type), while a specific employee's name or a specific account's balance must be an instance variable (unique per object).

Common Mistakes

  • Modifying a class variable through an instance in a way that accidentally creates a new instance variable instead of updating the shared value: emp1.company_name = "NewCorp" creates an instance-level override just for emp1, it does not change the class variable for everyone.

Important Points

  • Class variables are defined directly inside the class body, outside any method.
  • Instance variables are typically defined inside __init__ using self..
  • Changing a class variable properly should be done via ClassName.variable = new_value, not through an individual instance.

Practice

  1. Create a Car class with a class variable wheels = 4 and instance variables brand and model. Create two car objects and print both their shared wheel count and their individual brands.

5. Instance Methods, Class Methods, and Static Methods

What is it?

Python supports three kinds of methods inside a class, each with a different purpose:

  • Instance methods — the normal kind, operate on a specific object's data (use self).
  • Class methods — operate on the class itself, not a specific object (use cls, marked with @classmethod).
  • Static methods — don't need access to the object or the class at all; they're just related, grouped logically inside the class (marked with @staticmethod).

Simple Example

python
class Employee: company_name = "TechCorp" def __init__(self, name, salary): self.name = name self.salary = salary def give_raise(self, amount): # instance method self.salary += amount @classmethod def change_company_name(cls, new_name): # class method cls.company_name = new_name @staticmethod def is_valid_salary(salary): # static method return salary > 0 emp1 = Employee("Zara", 50000) emp1.give_raise(5000) print(emp1.salary) # 55000 Employee.change_company_name("NewTechCorp") print(emp1.company_name) # NewTechCorp print(Employee.is_valid_salary(-500)) # False

Explanation of the Code

  • give_raise() is an instance method — it modifies self.salary, meaning it works on one specific employee.
  • change_company_name() is a class method — it modifies cls.company_name, affecting the shared class variable for all employees at once.
  • is_valid_salary() is a static method — it doesn't need self or cls at all; it's just a related utility function that logically belongs with the Employee class.

Comparison Table — Instance vs Class vs Static Methods

Instance MethodClass MethodStatic Method
First parameterselfclsnone (no automatic first param)
Decorator(none)@classmethod@staticmethod
Operates onA specific object's dataThe class itself (shared data)Neither — standalone logic
Called viaobject.method()Class.method() or object.method()Class.method() or object.method()

Common Mistakes

  • Forgetting the @classmethod or @staticmethod decorator, which changes how Python passes arguments to the method — a very common source of confusing errors.
  • Using self inside a method meant to be a class method or static method.

Important Points

  • Use instance methods for anything that needs a specific object's data.
  • Use class methods when you need to affect or read data shared by the whole class.
  • Use static methods for utility/helper functions that logically belong to the class but don't need object or class data.

Practice

  1. Add a class method to the Employee example that creates a new employee from a string like "Zara-50000" (splitting on the hyphen).

6. Encapsulation

What is it?

Encapsulation means bundling data and the methods that operate on it together, while restricting direct outside access to some of that data — protecting an object's internal state from accidental or unauthorized changes.

Definition: Encapsulation is the practice of keeping an object's internal data protected, and only allowing controlled access through methods.

Access Levels in Python (By Convention)

Python doesn't enforce strict access control like some languages, but follows naming conventions:

PrefixMeaningExample
No underscorePublic — accessible from anywhereself.name
Single underscore _Protected — internal use, but still accessible (a convention, not enforced)self._balance
Double underscore __Private — Python "name-mangles" it to discourage direct outside accessself.__pin

Simple Example

python
class BankAccount: def __init__(self, owner, balance): self.owner = owner # public self.__balance = balance # private def deposit(self, amount): if amount > 0: self.__balance += amount def get_balance(self): return self.__balance account = BankAccount("Aditi", 1000) account.deposit(500) print(account.get_balance()) # 1500 # print(account.__balance) # AttributeError - can't access directly from outside

Explanation of the Code

  • self.__balance is a private attribute — Python internally renames it (_BankAccount__balance) to discourage direct outside access.
  • Instead of letting outside code set balance directly (which could allow invalid negative values), the class only allows controlled access through deposit() and get_balance().

Real-World Example

A bank account should never let external code set balance = -5000 directly — all changes must go through controlled methods like deposit() and withdraw(), which can enforce rules (e.g., "amount must be positive").

Common Mistakes

  • Assuming double-underscore attributes are truly impossible to access from outside — they're just strongly discouraged and renamed, not fully unbreakable.
  • Making everything private "for safety," even data that genuinely needs to be freely accessible — encapsulation should be applied thoughtfully, not everywhere.

Important Points

  • Encapsulation protects internal data from unintended, invalid changes.
  • Python relies on naming conventions (_, __) rather than strict enforcement.
  • The property decorator (introduced later in Intermediate Python) offers an even cleaner way to control attribute access.

Practice

  1. Create a BankAccount class with a private __balance attribute, and methods deposit(), withdraw() (which should refuse to withdraw more than the balance), and get_balance().

7. Inheritance

What is it?

Inheritance lets one class (the child/subclass) reuse and extend the attributes and methods of another class (the parent/superclass) — modeling natural "is-a" relationships, like "a Car is a Vehicle."

Definition: Inheritance allows a class to acquire the properties and methods of another class, promoting code reuse.

7.1 Single Inheritance

python
class Vehicle: def __init__(self, brand): self.brand = brand def start(self): print(f"{self.brand} vehicle is starting") class Car(Vehicle): # Car inherits from Vehicle def __init__(self, brand, model): super().__init__(brand) # calls Vehicle's constructor self.model = model def display_info(self): print(f"{self.brand} {self.model}") car = Car("Toyota", "Corolla") car.start() # inherited from Vehicle car.display_info() # defined in Car

Output:

Toyota vehicle is starting
Toyota Corolla

Explanation of the Code

  • class Car(Vehicle): means Car inherits everything from Vehicle.
  • super().__init__(brand) calls the parent class's constructor, so Car doesn't need to repeat the logic for setting self.brand — it reuses Vehicle's existing setup.
  • car.start() works even though start() is only defined in Vehicle — this is inheritance in action.

7.2 Multilevel Inheritance

A chain: grandparent → parent → child.

python
class Vehicle: def start(self): print("Vehicle starting") class Car(Vehicle): def drive(self): print("Car is driving") class SportsCar(Car): def turbo_boost(self): print("Turbo boost activated!") my_car = SportsCar() my_car.start() # from Vehicle my_car.drive() # from Car my_car.turbo_boost() # from SportsCar

7.3 Hierarchical Inheritance

Multiple child classes inherit from the same parent class.

python
class Vehicle: def start(self): print("Vehicle starting") class Car(Vehicle): pass class Motorcycle(Vehicle): pass car = Car() bike = Motorcycle() car.start() # Vehicle starting bike.start() # Vehicle starting

7.4 Multiple Inheritance

A single child class inherits from more than one parent class.

python
class Flyable: def fly(self): print("Flying") class Swimmable: def swim(self): print("Swimming") class FlyingBoat(Flyable, Swimmable): pass vehicle = FlyingBoat() vehicle.fly() # Flying vehicle.swim() # Swimming

Comparison Table — Types of Inheritance

TypeStructureExample
SingleOne parent, one childVehicleCar
MultilevelChain of inheritanceVehicleCarSportsCar
HierarchicalOne parent, multiple childrenVehicleCar, VehicleMotorcycle
MultipleOne child, multiple parentsFlyingBoatFlyable, Swimmable

Common Mistakes

  • Forgetting to call super().__init__(...) in the child's constructor, which means the parent's setup logic never runs.
  • Overusing multiple inheritance, which can make code harder to trace — Python resolves conflicts using something called the Method Resolution Order (MRO), which can get confusing if overused.

Important Points

  • super() gives access to the parent class's methods, most commonly its constructor.
  • A child class automatically has access to everything in its parent, and can also override or add new behavior.

Practice

  1. Create a Person class with name and age, then a Student class that inherits from it and adds a course attribute.
  2. Create a Bird class with a fly() method, and two child classes Sparrow and Penguin — override fly() in Penguin to print "Penguins can't fly" instead.

8. Polymorphism and Method Overriding

What is it?

Polymorphism means "many forms" — the same method name can behave differently depending on the object calling it. Method overriding is the most common way this happens: a child class redefines a method it inherited from its parent.

Definition: Polymorphism allows objects of different classes to be treated through a common interface, with each class providing its own specific behavior for shared method names.

Simple Example — Method Overriding

python
class Employee: def calculate_bonus(self): return 1000 class Manager(Employee): def calculate_bonus(self): # overrides the parent's version return 5000 employees = [Employee(), Manager(), Employee()] for emp in employees: print(emp.calculate_bonus())

Output:

1000
5000
1000

Explanation of the Code

  • Both Employee and Manager have a method called calculate_bonus(), but Manager provides its own version, which overrides the parent's.
  • The for loop calls .calculate_bonus() on each object without needing to know or check what specific type each one is — Python automatically runs the correct version for each object. This is polymorphism in action.

Real-World Example

An e-commerce app might have a base Product class with a calculate_shipping_cost() method, overridden differently by DigitalProduct (free/no shipping) versus PhysicalProduct (calculated based on weight).

Common Mistakes

  • Confusing method overriding (redefining an inherited method) with overloading (defining a method multiple times with different parameters — which Python doesn't directly support the way some other languages do).

Important Points

  • Method overriding lets a child class provide its own version of an inherited method.
  • Polymorphism lets you write code that works generically across different object types, as long as they share the relevant method name.

Practice

  1. Create a base class Shape with a method area() returning 0, and child classes Circle and Rectangle that override area() with the correct formulas.

9. Abstraction and Abstract Classes

What is it?

Abstraction means hiding complex internal implementation details and only exposing what's necessary — focusing on what something does, not how. In Python, this is often achieved using abstract classes, which define methods that must be implemented by any child class, without providing the actual implementation themselves.

Simple Example — Using the abc Module

python
from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass # no implementation here - child classes MUST provide one class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14159 * self.radius ** 2 class Rectangle(Shape): def __init__(self, length, width): self.length = length self.width = width def area(self): return self.length * self.width circle = Circle(5) rectangle = Rectangle(4, 6) print(circle.area()) # 78.53975 print(rectangle.area()) # 24

Explanation of the Code

  • Shape(ABC) marks Shape as an abstract base class — it cannot be instantiated directly: Shape() would raise a TypeError.
  • @abstractmethod forces any child class (Circle, Rectangle) to provide its own area() method — if a child class forgets to implement it, Python raises an error when trying to create an object from that child class.
  • This guarantees a consistent interface: every Shape subclass is guaranteed to have a working area() method, even though each calculates it completely differently.

Real-World Example

A payment processing system might define an abstract PaymentMethod class with an abstract process_payment() method, ensuring every specific payment type (CreditCard, PayPal, BankTransfer) implements its own version, while the rest of the application can call .process_payment() generically on any of them.

Common Mistakes

  • Trying to create an object directly from an abstract class: Shape()TypeError: Can't instantiate abstract class.
  • Forgetting to implement an abstract method in a child class, which also raises an error at instantiation time.

Important Points

  • Abstract classes cannot be instantiated directly — they exist to be inherited from.
  • @abstractmethod forces child classes to implement specific methods, guaranteeing a consistent interface across all subclasses.
  • Requires importing ABC and abstractmethod from the built-in abc module.

Practice

  1. Create an abstract class PaymentMethod with an abstract method process_payment(amount), then implement CreditCardPayment and UPIPayment subclasses.

10. Magic Methods (Dunder Methods)

What is it?

Magic methods (also called "dunder" methods, short for "double underscore") are special methods surrounded by double underscores (like __init__) that let your custom classes work naturally with Python's built-in syntax — like print(), +, ==, and len().

Common Magic Methods

MethodPurposeTriggered By
__init__ConstructorCreating a new object
__str__Human-readable string representationprint(obj), str(obj)
__repr__Developer-facing representationTyping the object name in a shell, debugging
__len__Defines behavior for len()len(obj)
__eq__Defines behavior for ==obj1 == obj2
__add__Defines behavior for +obj1 + obj2

Simple Example

python
class Product: def __init__(self, name, price): self.name = name self.price = price def __str__(self): return f"{self.name} - Rs.{self.price}" def __eq__(self, other): return self.price == other.price def __add__(self, other): return self.price + other.price item1 = Product("Shirt", 500) item2 = Product("Cap", 200) print(item1) # Shirt - Rs.500 (uses __str__ automatically) print(item1 == item2) # False (uses __eq__) print(item1 + item2) # 700 (uses __add__)

Explanation of the Code

  • Without __str__, print(item1) would show something unhelpful like <__main__.Product object at 0x...>. Defining __str__ makes it print something meaningful instead.
  • __eq__ lets you customize what == actually means for your objects — here, two products are "equal" if their prices match, regardless of name.
  • __add__ lets the + operator work between two Product objects, even though Python has no built-in idea of what "adding two products" should mean — you define it yourself.

Real-World Example

Nearly every well-designed class in real applications defines at least __str__ (or __repr__) so that printing or debugging objects gives useful, readable output instead of a generic memory address.

Common Mistakes

  • Forgetting that without __str__/__repr__, printing an object gives an unhelpful default representation.
  • Defining __eq__ without considering what happens if other isn't even the same type — real-world code often adds type checks inside __eq__ for safety.

Important Points

  • Magic methods let custom objects integrate naturally with Python's built-in operators and functions.
  • __str__ is for readable, user-facing output; __repr__ is more for developers/debugging.

Practice

  1. Add a __len__ method to a Cart class (holding a list of items) so that len(cart) returns the number of items in it.

Common Beginner Mistakes — Summary for This Section

  • Forgetting self as the first parameter of instance methods.
  • Confusing instance variables with class variables.
  • Forgetting super().__init__() in a child class's constructor.
  • Trying to instantiate an abstract class directly.
  • Forgetting to define __str__, resulting in unhelpful printed output for custom objects.

Cheat Sheet — OOP

python
class Animal: # class definition species_count = 0 # class variable def __init__(self, name): # constructor self.name = name # instance variable Animal.species_count += 1 def speak(self): # instance method print(f"{self.name} makes a sound") @classmethod def get_count(cls): # class method return cls.species_count @staticmethod def is_valid_name(name): # static method return len(name) > 0 class Dog(Animal): # inheritance def speak(self): # method overriding (polymorphism) print(f"{self.name} barks") from abc import ABC, abstractmethod class Shape(ABC): # abstraction @abstractmethod def area(self): pass

Mini Project: Bank Account Management System

Objective

Build a simple banking system using classes, demonstrating encapsulation, inheritance, and method overriding — a savings account and a current account, each with slightly different rules.

Requirements

  • A base Account class with deposit, withdraw, and balance-check functionality.
  • Balance must be kept private (encapsulated).
  • A SavingsAccount subclass that adds interest.
  • A CurrentAccount subclass that allows a small overdraft.

Concepts Used

Classes, objects, constructors, encapsulation, inheritance, method overriding, magic methods.

Complete Code

python
class Account: def __init__(self, owner, balance=0): self.owner = owner self._balance = balance # protected: subclasses can still access it def deposit(self, amount): if amount <= 0: print("Deposit amount must be positive.") return self._balance += amount print(f"Deposited Rs.{amount}. New balance: Rs.{self._balance}") def withdraw(self, amount): if amount > self._balance: print("Insufficient balance.") return self._balance -= amount print(f"Withdrew Rs.{amount}. New balance: Rs.{self._balance}") def get_balance(self): return self._balance def __str__(self): return f"{self.owner}'s Account - Balance: Rs.{self._balance}" class SavingsAccount(Account): def __init__(self, owner, balance=0, interest_rate=0.05): super().__init__(owner, balance) self.interest_rate = interest_rate def add_interest(self): interest = self._balance * self.interest_rate self._balance += interest print(f"Interest added: Rs.{interest:.2f}. New balance: Rs.{self._balance:.2f}") class CurrentAccount(Account): def __init__(self, owner, balance=0, overdraft_limit=5000): super().__init__(owner, balance) self.overdraft_limit = overdraft_limit def withdraw(self, amount): # overriding to allow overdraft if amount > self._balance + self.overdraft_limit: print("Withdrawal exceeds overdraft limit.") return self._balance -= amount print(f"Withdrew Rs.{amount}. New balance: Rs.{self._balance}") savings = SavingsAccount("Aditi", 10000) savings.add_interest() savings.withdraw(2000) current = CurrentAccount("Rohan", 1000, overdraft_limit=3000) current.withdraw(3500) # allowed due to overdraft print(current)

Code Explanation

  • Account handles the shared logic — deposit, basic withdrawal, and balance checking — while keeping _balance protected.
  • SavingsAccount extends Account with an add_interest() method, unique to savings accounts.
  • CurrentAccount overrides withdraw() entirely, since its withdrawal rule (allowing overdraft) is fundamentally different from the base class's rule.
  • __str__ gives every account type (inherited from Account) a clean, readable printed format automatically.

Sample Output

Interest added: Rs.500.00. New balance: Rs.10500.00
Withdrew Rs.2000. New balance: Rs.8500.00
Withdrew Rs.3500. New balance: Rs.-2500
Rohan's Account - Balance: Rs.-2500

Possible Improvements

  • Add a transaction history list that records every deposit/withdrawal with a timestamp.
  • Add a custom exception (from the Exception Handling file) instead of just printing error messages.
  • Add a transfer() method that moves money between two account objects.

Challenge Task

Add a FixedDepositAccount subclass that doesn't allow withdrawals at all until a certain "maturity" condition (e.g., a boolean flag) is met.


Interview Questions

Q1. What is the difference between a class and an object? Answer: A class is a blueprint defining attributes and behavior; an object is a specific instance created from that class, holding its own actual data.

Q2. What is the purpose of `self` in Python classes? Answer: self refers to the specific object a method is being called on, allowing the method to access and modify that particular object's own attributes.

Q3. What is the difference between instance variables and class variables? Answer: Instance variables are unique to each object (usually set in __init__ using self.); class variables are shared by every object of that class, defined directly in the class body.

Q4. What is the difference between a class method and a static method? Answer: A class method (marked @classmethod) receives the class itself (cls) and can access/modify class-level data. A static method (marked @staticmethod) receives neither self nor cls — it's just a related utility function grouped inside the class.

Q5. What is encapsulation, and how does Python implement it? Answer: Encapsulation is bundling data with the methods that operate on it, while restricting direct outside access. Python uses naming conventions — a single underscore (_var) signals "protected" (internal use), and a double underscore (__var) triggers name-mangling to discourage direct outside access.

Q6. What is the difference between method overriding and polymorphism? Answer: Method overriding is when a child class redefines a method inherited from its parent. Polymorphism is the broader concept — being able to call the same method name on different objects and get behavior appropriate to each object's actual class.

Q7. What is an abstract class, and why would you use one? Answer: An abstract class (built using ABC and @abstractmethod) cannot be instantiated directly and forces any subclass to implement specific methods, guaranteeing a consistent interface across all subclasses.

Q8. What is `__init__` vs `__str__`? Answer: __init__ is the constructor, called automatically when creating a new object, used to set initial attribute values. __str__ defines what print() or str() shows for that object — a human-readable representation.

Q9. What is the difference between single, multiple, multilevel, and hierarchical inheritance? Answer: Single: one parent, one child. Multiple: one child inherits from more than one parent. Multilevel: a chain (parent → child → grandchild). Hierarchical: multiple children inherit from the same single parent.


Practice Questions

Beginner

  1. Create a Person class with name and age attributes and a method that prints a greeting.
  2. Create a Rectangle class with length and width, and a method to calculate its area.
  3. Create two objects from a Car class and print both their brand and model.
  4. Add a class variable bank_name = "ABC Bank" to an Account class and access it from two different objects.
  5. Add a __str__ method to a Book class so printing a book object shows its title and author neatly.

Intermediate

  1. Create a Vehicle base class and a Car subclass that inherits from it and adds its own attribute.
  2. Create an Employee class and a Manager subclass that overrides a calculate_bonus() method.
  3. Create a BankAccount class with a private __balance attribute and controlled deposit()/withdraw() methods.
  4. Create an abstract Shape class with an abstract area() method, and implement it in Circle and Square subclasses.
  5. Add __eq__ to a Product class so two products are considered equal if their names and prices both match.

Challenge

  1. Design a small Library system with a Book class and a Member class, where a Member can borrow and return books (tracked as a list of borrowed books on the member object).
  2. Build a Shape hierarchy (ShapeCircle, Rectangle, Triangle) with an abstract area() and perimeter() method, and write a function that accepts any shape and prints both values (demonstrating polymorphism).
  3. Extend the Bank Account Management mini project to add a TransactionHistory class that logs every deposit/withdrawal across all account types, and can print a full statement for any account.

Mock Test

  • Object-Oriented Programming - Quick Test

    10 questions covering classes, objects, encapsulation, inheritance, polymorphism, abstraction and magic methods.

    10 questions · 10 min · Easy
    Start Mock Test

Coding Problems