0% found this document useful (0 votes)
14 views22 pages

Complete Python CT2 Prep

This document is a comprehensive guide to Python Object-Oriented Programming (OOP), covering key concepts such as classes, objects, inheritance, polymorphism, abstract classes, and access modifiers. It explains the differences between instance and class variables, demonstrates method overriding and duck typing, and provides examples of how to implement these concepts in Python. Additionally, it includes practice questions to reinforce understanding of the material.

Uploaded by

u1804127
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
14 views22 pages

Complete Python CT2 Prep

This document is a comprehensive guide to Python Object-Oriented Programming (OOP), covering key concepts such as classes, objects, inheritance, polymorphism, abstract classes, and access modifiers. It explains the differences between instance and class variables, demonstrates method overriding and duck typing, and provides examples of how to implement these concepts in Python. Additionally, it includes practice questions to reinforce understanding of the material.

Uploaded by

u1804127
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 22

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!

You might also like