Complete Python Object-Oriented
Programming Guide
Table of Contents
1. Classes and Objects
2. Instance vs Class Variables
3. Inheritance
4. Abstract Classes and Interfaces
5. Polymorphism
6. Access Modifiers
1. Classes and Objects
What is a Class?
A class is like a blueprint or template for creating objects. Think of it as a cookie cutter - it
defines the shape and structure, but it's not the actual cookie.
What is an Object?
An object is an actual instance created from a class. Using our cookie analogy, if the class is the
cookie cutter, the object is the actual cookie made from that cutter.
Creating Your First Class
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def display_info(self):
print(f"{self.year} {self.make} {self.model}")
Key Components Explained:
• class Car: - Defines a new class named Car
• __init__ - Special method called a constructor that runs when creating an object
• self - Refers to the specific instance of the class being created
• self.make = make - Creates an attribute (variable) for this specific object
Creating Objects (Instances)
# Creating objects of the Car class
car1 = Car("Toyota", "Camry", 2022)
car2 = Car("Honda", "Civic", 2023)
# Accessing attributes
print(car1.make) # Output: Toyota
print(car2.year) # Output: 2023
# Calling methods
car1.display_info() # Output: 2022 Toyota Camry
car2.display_info() # Output: 2023 Honda Civic
The Constructor (__init__ method)
The __init__ method is automatically called when you create a new object. It's used to initialize
the object's attributes.
def __init__(self, make, model, year):
self.make = make # Instance variable
self.model = model # Instance variable
self.year = year # Instance variable
The self Parameter
• self is a reference to the current instance of the class
• It's used to access variables and methods belonging to that specific object
• It's automatically passed when you call a method, but you must include it in the method
definition
2. Instance vs Class Variables
Instance Variables
Instance variables are unique to each object. Each object has its own copy of these variables.
class Student:
def __init__(self, name, age):
self.name = name # Instance variable
self.age = age # Instance variable
student1 = Student("Alice", 20)
student2 = Student("Bob", 22)
print(student1.name) # Output: Alice
print(student2.name) # Output: Bob
Class Variables
Class variables are shared among all instances of a class. They belong to the class itself, not to
any specific object.
class BankAccount:
interest_rate = 0.03 # Class variable (shared by all instances)
def __init__(self, balance):
self.balance = balance # Instance variable (unique to each instance)
account1 = BankAccount(1000)
account2 = BankAccount(1500)
# Both accounts share the same interest rate
print(account1.interest_rate) # Output: 0.03
print(account2.interest_rate) # Output: 0.03
# But they have different balances
print(account1.balance) # Output: 1000
print(account2.balance) # Output: 1500
Accessing Class Variables
# Through class name (recommended)
print(BankAccount.interest_rate)
# Through instance name (works but not recommended)
print(account1.interest_rate)
When to Use Each:
• Instance Variables: When you need data specific to each object
• Class Variables: When you want to share data among all objects of the class
3. Inheritance
What is Inheritance?
Inheritance allows a new class (child/subclass) to inherit attributes and methods from an existing
class (parent/superclass). It promotes code reuse and establishes relationships between classes.
Basic Inheritance Example
class Animal: # Parent class (superclass)
def __init__(self, name, species):
self.name = name
self.species = species
def make_sound(self):
print("Generic animal sound")
def show_info(self):
print(f"Name: {self.name}, Species: {self.species}")
class Dog(Animal): # Child class (subclass)
def __init__(self, name, breed):
super().__init__(name, species="Dog") # Call parent constructor
self.breed = breed
def make_sound(self): # Method overriding
super().make_sound() # Call parent method
print("Woof!")
def show_info(self): # Method overriding
super().show_info() # Call parent method
print(f"Breed: {self.breed}")
Using the Classes
# Creating instances
generic_animal = Animal(name="Generic Animal", species="Unknown")
my_dog = Dog(name="Buddy", breed="Golden Retriever")
# Using methods
generic_animal.show_info()
# Output: Name: Generic Animal, Species: Unknown
my_dog.show_info()
# Output: Name: Buddy, Species: Dog
# Breed: Golden Retriever
my_dog.make_sound()
# Output: Generic animal sound
# Woof!
The super() Function
• super() gives you access to methods in the parent class
• super().__init__() calls the parent class constructor
• super().method_name() calls the parent class method
Multi-level Inheritance
# Grandparent class
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclasses must implement this method")
# Parent class inheriting from Animal
class Mammal(Animal):
def give_birth(self):
return f"{self.name} is giving birth to live young."
# Child class inheriting from Mammal
class Dog(Mammal):
def speak(self):
return f"{self.name} says Woof!"
# Grandchild class inheriting from Dog
class Poodle(Dog):
def fetch(self):
return f"{self.name} is fetching the ball."
# Usage
my_dog = Poodle(name="Buddy")
print(my_dog.speak()) # Output: Buddy says Woof!
print(my_dog.give_birth()) # Output: Buddy is giving birth to live young.
print(my_dog.fetch()) # Output: Buddy is fetching the ball.
Multiple Inheritance
A class can inherit from multiple parent classes:
class Parent1:
def greet(self):
print("Hello from Parent1!")
class Parent2:
def greet(self):
print("Hello from Parent2!")
class Child(Parent1, Parent2):
pass
c = Child()
c.greet() # Output: Hello from Parent1! (first parent takes precedence)
The Diamond Problem and Method Resolution Order (MRO)
class A:
def say(self):
print("Hello from A")
class B(A):
def say(self):
print("Hello from B")
class C(A):
def say(self):
print("Hello from C")
class D(B, C):
pass
d = D()
d.say() # Output: Hello from B
# Check the Method Resolution Order
print(D.__mro__)
# Output: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>,
<class '__main__.A'>, <class 'object'>)
Why avoid multiple inheritance?
• Can cause confusion about which method gets called
• Makes code harder to understand and debug
• Use composition over inheritance when possible
4. Abstract Classes and Interfaces
What is an Abstract Class?
An abstract class is a class that cannot be instantiated directly. It serves as a blueprint for other
classes and may contain both abstract methods (must be implemented by subclasses) and
concrete methods (already implemented).
Abstract Class vs Interface
• Abstract Class: Can have both abstract and concrete methods, allows state (member
variables)
• Interface: Defines only abstract methods, usually doesn't have state
Creating Abstract Classes with ABC
from abc import ABC, abstractmethod
# Define an abstract class
class Animal(ABC):
@abstractmethod
def sound(self):
pass # Abstract method - must be implemented in subclasses
def sleep(self): # Concrete method - has implementation
print("This animal is sleeping.")
# Define subclasses
class Dog(Animal):
def sound(self): # Must implement the abstract method
print("Woof woof!")
class Cat(Animal):
def sound(self): # Must implement the abstract method
print("Meow!")
# Usage
dog = Dog()
dog.sound() # Output: Woof woof!
dog.sleep() # Output: This animal is sleeping.
cat = Cat()
cat.sound() # Output: Meow!
cat.sleep() # Output: This animal is sleeping.
# This would cause an error:
# animal = Animal() # TypeError: Can't instantiate abstract class
Interface-like Behavior (All Abstract Methods)
from abc import ABC, abstractmethod
class Shape(ABC): # Acts like an interface
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
# Implementing the interface
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
def perimeter(self):
return 2 * 3.14 * self.radius
# Usage
rect = Rectangle(4, 5)
print("Rectangle Area:", rect.area()) # Output: Rectangle Area: 20
print("Rectangle Perimeter:", rect.perimeter()) # Output: Rectangle
Perimeter: 18
circle = Circle(3)
print("Circle Area:", circle.area()) # Output: Circle Area: 28.26
print("Circle Perimeter:", circle.perimeter()) # Output: Circle Perimeter:
18.84
5. Polymorphism
What is Polymorphism?
Polymorphism means "many forms." It allows objects of different classes to be treated as objects
of a common base class, enabling a single interface to be used for different types of objects.
Method Overriding (Runtime Polymorphism)
class Animal:
def speak(self):
return "Animal speaks"
class Dog(Animal):
def speak(self): # Overriding the parent method
return "Dog barks"
class Cat(Animal):
def speak(self): # Overriding the parent method
return "Cat meows"
# Using polymorphism
def animal_sound(animal):
return animal.speak()
# Creating instances of different classes
animal = Animal()
dog = Dog()
cat = Cat()
# Same function works with different objects
print(animal_sound(animal)) # Output: Animal speaks
print(animal_sound(dog)) # Output: Dog barks
print(animal_sound(cat)) # Output: Cat meows
Duck Typing
"If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck."
class Duck:
def quack(self):
return "Duck quacks"
class Person:
def quack(self): # Same method name, different class
return "Person imitates a duck"
# Using duck typing
def duck_sound(entity):
return entity.quack() # Doesn't care about the type, just that it has
quack()
# Creating instances
duck = Duck()
person = Person()
# Both work because they have the same method
print(duck_sound(duck)) # Output: Duck quacks
print(duck_sound(person)) # Output: Person imitates a duck
Method Overloading (Not Traditional in Python)
Python doesn't support traditional method overloading, but you can achieve similar results:
Using Default Parameters
class Example:
def add(self, x, y=0):
return x + y
obj = Example()
print(obj.add(2, 3)) # Output: 5
print(obj.add(2)) # Output: 2 (y defaults to 0)
Using *args
class Example:
def add(self, *args):
return sum(args)
obj = Example()
print(obj.add(2, 3, 5)) # Output: 10
print(obj.add(1, 2, 3, 4)) # Output: 10
6. Access Modifiers
Python doesn't have strict access modifiers like other languages, but uses naming conventions to
indicate visibility:
Public (Default)
By default, all attributes and methods are public:
class MyClass:
def __init__(self):
self.public_variable = 42
def public_method(self):
return "This is a public method"
obj = MyClass()
print(obj.public_variable) # Accessible
print(obj.public_method()) # Accessible
Protected (Single Underscore _)
Conventionally indicates "protected" - shouldn't be accessed outside the class:
class MyClass:
def __init__(self):
self._protected_variable = 42
def _protected_method(self):
return "This is a protected method"
obj = MyClass()
print(obj._protected_variable) # Works, but not recommended
print(obj._protected_method()) # Works, but not recommended
Private (Double Underscore __)
Conventionally indicates "private" - Python applies name mangling:
class MyClass:
def __init__(self):
self.__private_variable = 42
def __private_method(self):
return "This is a private method"
def get_private_variable(self):
return self.__private_variable
obj = MyClass()
# This won't work:
# print(obj.__private_variable) # AttributeError
# But this will work:
print(obj.get_private_variable()) # Output: 42
# You can still access it with name mangling (not recommended):
print(obj._MyClass__private_variable) # Output: 42
Complete Example
class ExampleClass:
def __init__(self):
self.public_variable = "I am public"
self._protected_variable = "I am protected"
self.__private_variable = "I am private"
def get_public_variable(self):
return self.public_variable
def get_protected_variable(self):
return self._protected_variable
def get_private_variable(self):
return self.__private_variable
obj = ExampleClass()
# Public access
print(obj.public_variable) # Works fine
print(obj.get_public_variable()) # Works fine
# Protected access
print(obj._protected_variable) # Works but not recommended
print(obj.get_protected_variable()) # Better way
# Private access
# print(obj.__private_variable) # Would cause AttributeError
print(obj.get_private_variable()) # Correct way
print(obj._ExampleClass__private_variable) # Works but violates convention
Summary
Classes and Objects: Classes are blueprints, objects are instances created from classes.
Variables: Instance variables are unique to each object, class variables are shared by all objects.
Inheritance: Allows classes to inherit properties and methods from other classes. Use super()
to access parent class methods.
Abstract Classes: Cannot be instantiated directly, define contracts for subclasses using the abc
module.
Polymorphism: Allows different objects to be treated uniformly through method overriding and
duck typing.
Access Modifiers: Python uses naming conventions (public, _protected, __private) rather
than strict enforcement.
The key principle in Python OOP is "We are all consenting adults here" - the language trusts
developers to follow conventions rather than strictly enforcing them.
7. Practice Questions and Exercises
Section A: Classes and Objects - Basic Questions
Question 1: Create a Book class with attributes title, author, and pages. Include a method
get_info() that returns a formatted string with all book details.
Question 2: What will be the output of this code? Explain why.
class Counter:
def __init__(self, start=0):
self.value = start
def increment(self):
self.value += 1
return self.value
c1 = Counter()
c2 = Counter(10)
print(c1.increment())
print(c2.increment())
print(c1.value)
Question 3: Fix the error in this code:
class Student:
def __init__(self, name, age):
name = name
age = age
def display(self):
print(f"Name: {self.name}, Age: {self.age}")
s = Student("Alice", 20)
s.display()
Section B: Instance vs Class Variables - Intermediate Questions
Question 4: What will be the output? Explain the difference.
class BankAccount:
bank_name = "Global Bank"
total_accounts = 0
def __init__(self, account_holder):
self.account_holder = account_holder
BankAccount.total_accounts += 1
acc1 = BankAccount("Alice")
acc2 = BankAccount("Bob")
print(acc1.bank_name)
print(acc2.bank_name)
print(acc1.total_accounts)
print(BankAccount.total_accounts)
acc1.bank_name = "Local Bank"
print(acc1.bank_name)
print(acc2.bank_name)
print(BankAccount.bank_name)
Question 5: Create a Car class where:
• All cars share the same fuel_type = "Petrol"
• Each car has its own brand, model, and mileage
• Include a class method to change the fuel type for all cars
• Include a method to display car details
Question 6 (Tricky): What's wrong with this code and how would you fix it?
class Employee:
benefits = []
def __init__(self, name):
self.name = name
def add_benefit(self, benefit):
self.benefits.append(benefit)
emp1 = Employee("Alice")
emp2 = Employee("Bob")
emp1.add_benefit("Health Insurance")
emp2.add_benefit("Dental Insurance")
print(f"Alice's benefits: {emp1.benefits}")
print(f"Bob's benefits: {emp2.benefits}")
Section C: Inheritance - Advanced Questions
Question 7: Complete the following inheritance hierarchy:
class Vehicle:
def __init__(self, brand, year):
# Your code here
pass
def start_engine(self):
# Your code here
pass
class Car(Vehicle):
def __init__(self, brand, year, doors):
# Your code here
pass
def start_engine(self):
# Call parent method and add car-specific behavior
pass
class ElectricCar(Car):
def __init__(self, brand, year, doors, battery_capacity):
# Your code here
pass
def charge_battery(self):
# Your code here
pass
Question 8: What will be the output of this code?
class A:
def method(self):
print("A's method")
class B(A):
def method(self):
print("B's method")
super().method()
class C(A):
def method(self):
print("C's method")
super().method()
class D(B, C):
def method(self):
print("D's method")
super().method()
d = D()
d.method()
print("MRO:", D.__mro__)
Question 9 (Very Tricky): Predict the output and explain the MRO:
class X:
def show(self):
print("X")
class Y(X):
def show(self):
print("Y")
super().show()
class Z(X):
def show(self):
print("Z")
super().show()
class W(Y, Z):
pass
w = W()
w.show()
Question 10: Create a multi-level inheritance scenario:
• Animal class with name and speak() method
• Mammal class inheriting from Animal with give_birth() method
• Dog class inheriting from Mammal with bark() method
• ServiceDog class inheriting from Dog with assist_human() method Each class should
properly call parent constructors and methods.
Section D: Abstract Classes and Interfaces - Expert Questions
Question 11: Complete this abstract class implementation:
from abc import ABC, abstractmethod
class Shape(ABC):
def __init__(self, color):
self.color = color
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
def display_color(self):
print(f"This shape is {self.color}")
# Create Rectangle and Circle classes that inherit from Shape
# Rectangle should have width and height
# Circle should have radius
Question 12: What's wrong with this code?
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def sound(self):
pass
def sleep(self):
print("Sleeping...")
class Dog(Animal):
def bark(self):
print("Woof!")
dog = Dog()
dog.bark()
Question 13 (Advanced): Create an interface-like abstract class for a PaymentProcessor:
• Must have process_payment(amount) method
• Must have verify_payment() method
• Can have log_transaction() concrete method
• Create CreditCardProcessor and PayPalProcessor implementations
Section E: Polymorphism - Complex Questions
Question 14: What will be the output?
class Animal:
def speak(self):
return "Some sound"
class Dog(Animal):
def speak(self):
return "Woof"
class Cat(Animal):
def speak(self):
return "Meow"
def make_animals_speak(animals):
for animal in animals:
print(animal.speak())
animals = [Animal(), Dog(), Cat(), Dog()]
make_animals_speak(animals)
Question 15: Implement duck typing with this scenario:
# Create classes: Duck, Robot, Person
# All should have a walk() method with different implementations
# Create a function that makes any object walk, regardless of its type
Question 16 (Tricky): What's the issue with this method overloading attempt?
class Calculator:
def add(self, a, b):
return a + b
def add(self, a, b, c):
return a + b + c
calc = Calculator()
print(calc.add(1, 2))
print(calc.add(1, 2, 3))
Question 17: Fix the above calculator to work with variable arguments using proper Python
techniques.
Section F: Access Modifiers - Tricky Questions
Question 18: Predict the output and explain what happens:
class SecretClass:
def __init__(self):
self.public = "Everyone can see this"
self._protected = "Don't access directly"
self.__private = "Very secret"
def reveal_secrets(self):
return f"Public: {self.public}, Protected: {self._protected},
Private: {self.__private}"
obj = SecretClass()
print(obj.public)
print(obj._protected)
# print(obj.__private) # What happens here?
print(obj.reveal_secrets())
print(obj._SecretClass__private) # What about this?
Question 19: What's the difference between these two classes?
class ClassA:
def __init__(self):
self._value = 10
class ClassB:
def __init__(self):
self.__value = 10
def get_value(self):
return self.__value
# Try to access _value and __value from outside
Question 20: Create a class with proper encapsulation:
• Private attribute __balance
• Public method deposit(amount)
• Public method withdraw(amount) that checks for sufficient funds
• Public method get_balance() to view balance
• Ensure balance cannot be directly modified from outside
Section G: Integration Questions (Multiple Concepts)
Question 21: Design a complete system:
# Create an abstract class Media with:
# - title (protected)
# - duration (private)
# - play() abstract method
# - get_info() concrete method
# Create subclasses:
# - Movie (inherits Media, has genre)
# - Song (inherits Media, has artist)
# - Podcast (inherits Media, has host)
# Create a MediaPlayer class that can play any media type (polymorphism)
Question 22 (Very Advanced): What will be the final output?
class Counter:
count = 0
def __init__(self):
Counter.count += 1
self.__id = Counter.count
def get_id(self):
return self.__id
@classmethod
def get_total_count(cls):
return cls.count
class SpecialCounter(Counter):
special_count = 0
def __init__(self):
super().__init__()
SpecialCounter.special_count += 1
def get_id(self):
return f"Special-{super().get_id()}"
c1 = Counter()
c2 = Counter()
s1 = SpecialCounter()
s2 = SpecialCounter()
print(f"Total counters: {Counter.get_total_count()}")
print(f"Special counters: {SpecialCounter.special_count}")
print(f"C1 ID: {c1.get_id()}")
print(f"S1 ID: {s1.get_id()}")
Question 23: Create a university management system:
• Abstract class Person with name, age, and abstract method get_role()
• Student class inheriting from Person with student_id and grades list
• Professor class inheriting from Person with employee_id and subjects list
• University class that can store both students and professors
• Implement proper encapsulation and polymorphism
Section H: Debugging and Error Analysis
Question 24: Find and fix all errors:
from abc import ABC, abstractmethod
class Animal(ABC)
def __init__(self, name):
self.name = name
@abstractmethod
def sound():
pass
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
dog = Dog("Buddy", "Golden Retriever")
print(dog.sound())
Question 25: What's wrong with this inheritance?
class Parent:
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hello, I'm {self.name}")
class Child(Parent):
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
super().greet()
print(f"I'm {self.age} years old")
child = Child("Alice", 10)
child.greet()
Answer Key and Explanations
Answers for Section A (Classes and Objects):
Answer 1:
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def get_info(self):
return f"'{self.title}' by {self.author}, {self.pages} pages"
book = Book("1984", "George Orwell", 328)
print(book.get_info())
Answer 2: Output:
1
11
1
Explanation: c1 starts at 0, increments to 1. c2 starts at 10, increments to 11. c1.value remains
1.
Answer 3: Error: Missing self. prefix. Fixed code:
class Student:
def __init__(self, name, age):
self.name = name # Fixed: added self.
self.age = age # Fixed: added self.
def display(self):
print(f"Name: {self.name}, Age: {self.age}")
Answers for Section B (Instance vs Class Variables):
Answer 4: Output:
Global Bank
Global Bank
2
2
Local Bank
Global Bank
Global Bank
Explanation: When you assign to acc1.bank_name, you create an instance variable that shadows
the class variable for that instance only.
Answer 5:
class Car:
fuel_type = "Petrol" # Class variable
def __init__(self, brand, model, mileage):
self.brand = brand # Instance variables
self.model = model
self.mileage = mileage
@classmethod
def change_fuel_type(cls, new_fuel_type):
cls.fuel_type = new_fuel_type
def display_details(self):
print(f"{self.brand} {self.model}, Mileage: {self.mileage}, Fuel:
{Car.fuel_type}")
Answer 6: Problem: benefits is a class variable (list), so all instances share the same list. Fix:
class Employee:
def __init__(self, name):
self.name = name
self.benefits = [] # Make it an instance variable
def add_benefit(self, benefit):
self.benefits.append(benefit)
Answers for Section C (Inheritance):
Answer 7:
class Vehicle:
def __init__(self, brand, year):
self.brand = brand
self.year = year
def start_engine(self):
print(f"{self.brand} engine starting...")
class Car(Vehicle):
def __init__(self, brand, year, doors):
super().__init__(brand, year)
self.doors = doors
def start_engine(self):
super().start_engine()
print(f"Car with {self.doors} doors ready to drive!")
class ElectricCar(Car):
def __init__(self, brand, year, doors, battery_capacity):
super().__init__(brand, year, doors)
self.battery_capacity = battery_capacity
def charge_battery(self):
print(f"Charging {self.battery_capacity}kWh battery...")
Answer 8: Output:
D's method
B's method
C's method
A's method
MRO: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>,
<class '__main__.A'>, <class 'object'>)
Answer 9: Output:
Y
Z
X
Explanation: Due to C3 linearization, the MRO is W → Y → Z → X → object.
Key Study Tips for Your Exam:
1. Understand self and super(): These are fundamental and appear in many questions
2. Master MRO: Practice predicting method resolution order in multiple inheritance
3. Know the difference: Instance vs class variables is a common exam topic
4. Abstract classes: Remember you need ABC and @abstractmethod
5. Access modifiers: Understand it's convention-based, not enforcement
6. Polymorphism: Focus on method overriding and duck typing
7. Practice debugging: Many exam questions involve finding and fixing errors
Remember: The best way to prepare is to actually code these examples and experiment with
variations!