OOP Concepts – In-depth Theory
Concept Description
1. Class & Object Blueprint and instance of the blueprint
2. Encapsulation Binding data and methods together, hiding internal details
3. Inheritance Derive new classes from existing ones
4. Polymorphism One interface, many implementations
5. Abstraction Hiding complex implementation, exposing only essential features
✅ 1. Class and Object
🔹 What is a Class?
A class is a blueprint or template for creating objects. It defines the structure and behavior (i.e., data
and methods) that the objects created from the class will have.
Think of a class like a blueprint for a house – it defines the design but isn't a real house itself.
🔹 What is an Object?
An object is an instance of a class – a real-world entity created using that blueprint. It contains:
Attributes (data) like variables.
Methods (functions) that perform operations on data.
🔹 Why use them?
They help model real-world problems as code, encourage reusability, and organize logic more
cleanly.
The class header contains the keyword class followed by an identifier used to name the
class. The body of the class definition contains one or more method definitions, all of which must
be indented to same indentation level. Suppose we want to define a class to represent a point or
coordinate in the two-dimensional Cartesian coordinate system. The objects will need to store two
values, one for the x-coordinate and one for the y-coordinate. We will also have to decide what
operations we want to be able to perform on the objects created from the new class. We can begin
with a framework for the new class:
class Point :
# The methods are defined in the body one after the other.
[Link] 1/29
📌 New in Python 3.x: All classes are automatically derived from the object base class even if it’s
not explicitly stated in the header definition.
✅ 2. Encapsulation
🔹 What is Encapsulation?
Encapsulation is the concept of bundling data and the methods that operate on that data into a
single unit (class). It also involves restricting access to internal details of how an object works.
You use encapsulation to:
Hide internal object data (mark them private)
Expose only what is necessary using getters and setters
🔹 How to implement in Python?
Use __ before variable/method names → to make them private
Use methods to access/modify private data
🔒 Encapsulation = Protect data + Control access.
🔹 What is Method?
A method is a service or operation that can be performed on an object created from the given
class. A method is very similar to a function with several exceptions:
(1) a method is defined as part of a class definition;
(2) a method can only be used with an instance of the class in which it is defined; and
(3) each method header must include a parameter named self, which must be listed first.
class Point :
# ...
def shift( self, xInc, yInc ):
[Link] += xInc
[Link] += yInc
[Link] 2/29
🔹 What is Constructor ?
All classes should define a special method known as the constructor , which defines and
initializes the data to be contained in the object. The constructor is automatically called when an
instance of the class is created. In Python, the constructor is named init and is usually listed first in
the class definition:
class Point:
def __init__( self, x, y ):
[Link] = x
[Link] = y
The self Reference
As indicated earlier, self is a special parameter that must be included in each method
definition and it must be listed first. When a method is called, this parameter is automatically filled
with a reference to the object on which the method was invoked.
What is Destructor ?
A destructor is a special method that gets called automatically when an object is about to be
destroyed — i.e., when it goes out of scope or is explicitly deleted.
When to Define a Destructor Explicitly?
You should define a destructor:
When your class holds external resources (e.g., file handles, sockets, or database
connections).
When you need to log or debug object lifecycle (e.g., "Object deleted" message).
To clean up temporary data, such as files or memory buffers.
class FileManager:
#constructor
def __init__(self, filename):
[Link] = filename
[Link] = open(filename, 'w')
print(f"File '{[Link]}' opened.")
#class method
def write_data(self, data):
[Link](data)
#destructor
def __del__(self):
[Link]()
print(f"File '{[Link]}' closed and object destroyed.")
# Create an object
f = FileManager("[Link]")
f.write_data("Hello, world!")
# Deleting the object explicitly (optional)
[Link] 3/29
del f
Copy Constructor (Shallow vs Deep Copy)
In Python, a copy constructor creates a new object by copying the attributes of an existing
object. Unlike languages like C++ or Java, Python doesn't have an explicit copy constructor.
However, its behavior can be achieved using the __copy__ or __deepcopy__ methods, or the
copy module.
import copy
class MyClass:
def __init__(self, data):
[Link] = data
def __copy__(self):
return MyClass([Link]) # Shallow copy
def __deepcopy__(self, memo):
return MyClass([Link]([Link], memo)) # Deep copy
obj1 = MyClass([1, 2, 3])
obj2 = [Link](obj1) # Shallow copy
obj3 = [Link](obj1) # Deep copy
[Link][0] = 99
print([Link]) # [99, 2, 3] (shallow copy affected)
print([Link]) # [1, 2, 3] (deep copy unaffected)
Implementing Copy Behavior
• __copy__ method (Shallow Copy):
• The __copy__ method returns a shallow copy of the object. A shallow copy creates
a new object and then inserts references to the objects found in the original. This
means if the original object contains mutable objects (like lists or dictionaries), the
copy will share these objects. Changes to these shared objects will affect both the
original and the copy.
import copy
class MyClass:
def __init__(self, data):
[Link] = data
def __copy__(self):
return MyClass([Link])
original_obj = MyClass([1, 2, 3])
copied_obj = [Link](original_obj)
[Link] 4/29
copied_obj.[Link](4)
print(original_obj.data) # Output: [1, 2, 3, 4]
print(copied_obj.data) # Output: [1, 2, 3, 4]
__deepcopy__ method (Deep Copy):
• The __deepcopy__ method returns a deep copy of the object. A deep copy creates a new
object and recursively inserts copies of the objects found in the original. This means that all
objects within the original are duplicated, and changes to the copy will not affect the
original.
class MyClass:
def __init__(self, data):
[Link] = data
def __deepcopy__(self, memo):
return MyClass([Link]([Link], memo))
import copy
original_obj = MyClass([1, 2, 3])
copied_obj = [Link](original_obj)
copied_obj.[Link](4)
print(original_obj.data) # Output: [1, 2, 3]
print(copied_obj.data) # Output: [1, 2, 3, 4]
When to Use
• Use a shallow copy when you want a quick copy and don't need to modify the internal
objects independently.
• Use a deep copy when you need a completely independent copy of an object and its internal
objects. This is crucial when modifying the copy should not affect the original.
[Link] 5/29
✅ 3. Inheritance
🔹 What is Inheritance?
Inheritance allows a class (child) to inherit features (methods and properties) from another class
(parent).
It helps with:
Code reuse (don't rewrite common code)
Extensibility (override/extend behavior)
🔹 Types of Inheritance in Python:
Single Inheritance: One child, one parent
Multilevel Inheritance: Parent → Child → Subchild
Multiple Inheritance: One child class inherits from multiple parents
🔹 Why use it?
When you have general-to-specific relationships. Example:
Animal → Dog, Employee → Manager
Python, like all object-oriented languages, supports class
inheritance. Instead of creating a new class from scratch, we can
derive a new class from an existing one. The new class
automatically inherits all data attributes and methods of the
existing class without having to explicitly redefine the code.
This leads to a hierarchical structure in which the newly derived
class becomes the child of the original or parent class.
Single Inheritance Multilevel Inheritance Multiple Inheritance
Parent class Parent class Parent 1 class Parent 2 class
Child class
Child class Child class
Grandchild class
Single Inheritance
[Link] 6/29
class Parent:
def feature_1(self):
print('feature_1 from Parent is running...')
def feature_2(self):
print('feature_2 from Parent is running...')
class Child(Parent):
def feature_3(self):
print('feature_3 from Child is running...')
Multilevel Inheritance
class Parent:
def feature_1(self):
print('feature_1 from Parent is running...')
def feature_2(self):
print('feature_2 from Parent is running...')
class Child(Parent):
def feature_3(self):
print('feature_3 from Child is running...')
class GrandChild(Child):
def feature_3(self):
print('feature_3 from GrandChild is running...')
Multiple Inheritance
class Parent_1:
def feature_1(self):
print('feature_1 from Parent_1 is running...')
def feature_2(self):
print('feature_2 from Parent_1 is running...')
class Parent_2:
def feature_3(self):
print('feature_3 from Parent_2 is running...')
class Child(Parent_1, Parent_2 ):
def feature_3(self):
print('feature_3 from Child is running...')
[Link] 7/29
✅ 4. Polymorphism
🔹 What is Polymorphism?
Polymorphism means "many forms". In OOP, it allows different classes to implement methods
with the same name, and the correct one will be used based on the object.
🔹 Forms of Polymorphism:
Method overriding (in child classes)
Duck typing: "If it walks like a duck and quacks like a duck, it’s a duck"
(Python checks if an object behaves correctly, not its type explicitly.)
🔹 Why use it?
Enables flexibility and extensibility in your code
Helps implement dynamic behavior
Method Overriding and Method Overloading (Python-style, since it doesn't
support traditional overloading like Java/C++)
Inheritance + Overriding + Overloading
📌 Scenario: Vehicles → Car
class Vehicle:
def start(self):
print("Vehicle started.")
def speed(self, value):
print(f"Vehicle speed: {value} km/h")
class Car(Vehicle):
def start(self): # Method overriding
print("Car engine started with keyless ignition.")
def speed(self, value=None): # Method overloading (Python-style)
if value is None:
print("Speed not specified.")
else:
print(f"Car speed: {value} km/h")
# Usage
v = Vehicle()
[Link]()
[Link](60)
print("---")
c = Car()
[Link]() # Overridden method
[Link]() # Overloaded method with no value
[Link](100) # Overloaded method with value
[Link] 8/29
Output:
Vehicle started.
Vehicle speed: 60 km/h
---
Car engine started with keyless ignition.
Speed not specified.
Car speed: 100 km/h
💡 Explanation
Inheritance: Car inherits from Vehicle.
Method Overriding: Car provides its own version of start().
Method Overloading:
Python does not support traditional overloading (same method, different signatures).
Instead, you handle overloading using default arguments, *args, or **kwargs.
Magic/Dunder Methods
Magic methods, also known as dunder methods (short for "double underscore" methods), are
special methods in Python that begin and end with double underscores. They are used to implement
operator overloading and customize class behavior. These methods are not typically called
directly but are invoked implicitly by Python when certain operations are performed on
objects of the class
Common Magic Methods
• __init__(self, ...): Constructor; initializes object attributes upon creation.
• __new__(cls, ...): Method called before __init__() when creating a class
instance.
• __str__(self): Returns a string representation of the object, useful for print() and
str().
• __repr__(self): Returns a string representation of the object, aimed at developers for
debugging.
• __len__(self): Returns the length of the object, used by len().
• __add__(self, other): Implements addition (+).
• __sub__(self, other): Implements subtraction (-).
• __mul__(self, other): Implements multiplication (*).
• __truediv__(self, other): Implements division (/).
• __eq__(self, other): Implements equality (==).
[Link] 9/29
• __lt__(self, other): Implements less than (<).
• __gt__(self, other): Implements greater than (>).
• __le__(self, other): Implements less than or equal to (<=).
• __del__(self): Destructor, called when an object is garbage collected.
• __call__(self, ...): Allows instances of a class to be called like functions.
• __iter__(self): Returns an iterator for the class.
• __getitem__(self, key): Implements access to items using the index or key.
• __setitem__(self, key, value): Implements assignment to items using the index
or key.
• __contains__(self, item): Implements the in operator for membership testing.
Simple example of operator ‘+’ overloading
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __str__(self):
return f"({self.x}, {self.y})"
p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2 # Calls __add__
print(p3) # Calls __str__
Encapsulation using Magic method
class Person:
def __init__(self, name):
self.__name = name # Encapsulated (name mangling)
def __getattr__(self, attr):
return f"{attr} not found!"
p = Person("Alice")
print(p.__dict__) # Internal object state
print([Link]) # Triggers __getattr__
[Link] 10/29
Simulate a Bank Account system where:
• The balance is private.
• Only controlled access is allowed via __getattr__, __setattr__, and private
variable encapsulation.
• We'll hide sensitive data using Python’s encapsulation mechanisms.
✅ Real-Life Example: Bank Account System
class BankAccount:
def __init__(self, name, balance):
[Link] = name
self.__balance = balance # Private variable via name mangling
def deposit(self, amount):
if amount > 0:
self.__balance += amount
else:
raise ValueError("Deposit must be positive")
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
else:
raise ValueError("Insufficient funds or invalid amount")
def get_balance(self):
return self.__balance
def __getattr__(self, attr):
# Provide a generic response to unauthorized attribute access
return f"'{attr}' is not accessible or does not exist."
def __setattr__(self, name, value):
# Prevent direct setting of __balance outside
constructor/deposit/withdraw
if name == "_BankAccount__balance":
if hasattr(self, name):
raise AttributeError("Direct modification of balance not
allowed!")
super().__setattr__(name, value)
🔍 Explanation:
• __balance is a private variable (Python uses name mangling → becomes
_BankAccount__balance)
• __setattr__ prevents direct balance assignment after object creation.
[Link] 11/29
• __getattr__ intercepts calls to undefined or inaccessible attributes, improving error
messaging and control.
🔧 Usage:
account = BankAccount("Alice", 1000)
print(account.get_balance()) # ✅ 1000
[Link](200)
print(account.get_balance()) # ✅ 1200
[Link](500)
print(account.get_balance()) # ✅ 700
# Try accessing private attribute
print(account.__balance) # ⚠️ Triggers __getattr__ → Not accessible
print([Link]) # ⚠️ Triggers __getattr__ → Not accessible
# Try changing balance directly (blocked)
account.__balance = 9999 # This creates a new attribute, does not modify
real balance
print(account.get_balance()) # Still 700
# Try hacking through name mangling
# account._BankAccount__balance = 9999 # ⚠️ Will raise error due to __setattr__
🔐 Output:
1000
1200
700
'__balance' is not accessible or does not exist.
'balance' is not accessible or does not exist.
700
🧠 Why This Is Encapsulation?
1. Encapsulation = hiding internal data and exposing only what’s needed.
2. By using:
• __getattr__() → customized access to hidden attributes.
• __setattr__() → controlled mutation rules.
• __balance → private naming (name mangling).
We hide the internal balance and only allow safe, validated access through deposit(),
withdraw(), and get_balance() methods.
[Link] 12/29
Class Methods and Static Methods
In Python, class methods and static methods are special types of methods defined within a class, but
they behave differently from regular instance methods.
Class Methods
Class methods are bound to the class and not the instance of the class. They receive the class itself
as the first argument, conventionally named cls. Class methods are defined using the
@classmethod decorator. They can access and modify class-level attributes and call other
class methods
class MyClass:
count = 0
def __init__(self):
[Link] += 1
@classmethod
def get_count(cls):
return [Link]
print(MyClass.get_count()) # Output: 0
obj1 = MyClass()
print(MyClass.get_count()) # Output: 1
obj2 = MyClass()
print(MyClass.get_count()) # Output: 2
Another example
class Employee:
company = "OpenAI" # Class variable
def __init__(self, name):
[Link] = name
@classmethod
def change_company(cls, new_name):
[Link] = new_name
@classmethod
def from_string(cls, emp_str):
name = emp_str.split("-")[0]
return cls(name)
# Factory method usage
e1 = Employee.from_string("Alice-Engineer")
print([Link]) # Alice
print([Link]) # OpenAI
Employee.change_company("DeepMind")
print([Link]) # DeepMind
[Link] 13/29
🔧 Factory Pattern using @classmethod
from datetime import datetime
class Logger:
def __init__(self, log_level):
self.log_level = log_level
@classmethod
def with_timestamp(cls):
time = [Link]().strftime("%H:%M:%S")
return cls(f"INFO@{time}")
logger = Logger.with_timestamp()
print(logger.log_level)
Static Methods
Static methods are not bound to either the class or the instance. They are essentially
regular functions that are placed within the class namespace. Static methods do not receive any
implicit first argument. They are defined using the @staticmethod decorator. Static methods are
often used for utility functions that are related to the class but do not need access to class or instance
data.
class MathUtils:
@staticmethod
def add(x, y):
return x + y
@staticmethod
def multiply(x, y):
return x * y
print([Link](5, 3)) # Output: 8
print([Link](2, 7)) # Output: 14
Another example
class MathOperations:
@staticmethod
def add(x, y):
return x + y
@staticmethod
def is_even(x):
return x % 2 == 0
print([Link](5, 3)) # 8
print(MathOperations.is_even(10)) # True
[Link] 14/29
Key Differences
• Binding:
Class methods are bound to the class, while static methods are not bound to either the class
or the instance.
• First Argument:
Class methods receive the class as the first argument (cls), while static methods do not
receive any implicit first argument.
• Access to Class/Instance Data:
Class methods can access and modify class-level attributes, while static methods cannot
access class or instance data directly.
• Usage:
Class methods are often used for factory methods or when you need to work with class-level
attributes, while static methods are used for utility functions that are related to the class but
do not need access to its data
[Link] 15/29
✅ 5. Abstraction
🔹 What is Abstraction?
Abstraction means hiding internal implementation details and only exposing essential features.
For example, when you drive a car, you don't know how the engine works internally. You just use
the steering and brakes.
Any class that contains abstract method(s) is called an abstract class. Abstract methods do not
include any implementations – they are always defined and implemented as part of the methods of
the sub-classes inherited from the abstract class. Look at the sample syntax below for an abstract
class:
🔹 In Python:
Use the abc (Abstract Base Classes) module
Define abstract classes and methods using @abstractmethod
🔹 Why use it?
Define standard interfaces that all child classes must implement
Achieve enforced design for plugin-like architecture
Advanced Python: Abstract Base Classes (ABCs) and Abstract
Methods
Table of Contents
1. Introduction to ABCs
2. Creating ABCs with abc
3. Advanced ABC Features
4. Virtual Subclasses
5. ABCMeta Metaclass
6. ABCs in Collections
7. Plugin System Example
8. ABCs vs Protocols
9. Best Practices
10. Real-World Example
[Link] 16/29
1. Introduction to Abstract Base Classes (ABCs)
Purpose:
- Define interfaces that subclasses must implement.
- Prevent instantiation of incomplete classes.
- Enforce method implementation.
Key Terms:
Term Description
Abstract Method Declared but not implemented (must be overridden)
Concrete Class Implements all abstract methods.
Interface Contract defining required methods.
2. Creating ABCs with the abc Module
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
"""Calculate area; must be implemented by subclasses."""
pass
@abstractmethod
def perimeter(self) -> float:
pass
Key Points:
- Inherit from ABC.
- Decorate abstract methods with @abstractmethod.
3. Advanced ABC Features
3.1 Abstract Properties
from abc import abstractproperty
class DatabaseConnection(ABC):
@abstractproperty
def connection_string(self) -> str:
pass
[Link] 17/29
3.2 Abstract Class/Static Methods
class Serializer(ABC):
@classmethod
@abstractmethod
def serialize(cls, obj) -> bytes:
pass
@staticmethod
@abstractmethod
def deserialize(data: bytes):
pass
3.3 Abstract Class Variables (Python 3.6+)
class Plugin(ABC):
version: str # Abstract class variable
author: str = "Unknown"
4. Registering Virtual Subclasses
class Animal(ABC):
@abstractmethod
def speak(self) -> str:
pass
class Dog: # No inheritance
def speak(self) -> str:
return "Woof!"
[Link](Dog) # Now `isinstance(Dog(), Animal) == True`
5. The ABCMeta Metaclass
For low-level control:
from abc import ABCMeta, abstractmethod
class MyABC(metaclass=ABCMeta):
@abstractmethod
def must_implement(self):
pass
[Link] 18/29
6. Abstract Base Classes in Collections
from [Link] import MutableSequence
class CustomList(MutableSequence):
def __init__(self, data):
[Link] = list(data)
def __getitem__(self, index):
return [Link][index]
def __len__(self):
return len([Link])
# Must implement __setitem__, __delitem__, insert
7. Advanced Example: Plugin System
from abc import ABC, abstractmethod
class Plugin(ABC):
@abstractmethod
def execute(self, data):
pass
class UppercasePlugin(Plugin):
def execute(self, data):
return [Link]()
# Usage
plugin = UppercasePlugin()
print([Link]("hello")) # "HELLO"
8. ABCs vs Protocols (Python 3.8+)
Use Protocol for structural subtyping:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self):
print("Drawing circle")
# No inheritance needed
def render(d: Drawable):
[Link]()
[Link] 19/29
9. Best Practices for ABCs
1 Use sparingly when interfaces must be enforced.
2 Document abstract methods clearly.
3 Prefer Protocols for duck typing.
4 Keep hierarchies shallow.
10. Real-World Example: Database Adapter
class DatabaseAdapter(ABC):
@abstractmethod
def connect(self, connection_str: str):
pass
@abstractmethod
def query(self, sql: str) -> list:
pass
class PostgreSQLAdapter(DatabaseAdapter):
def connect(self, connection_str):
import psycopg2
[Link] = [Link](connection_str)
def query(self, sql):
with [Link]() as cur:
[Link](sql)
return [Link]()
Appendix: Common Pitfalls
❌ Forgetting to implement abstract methods.
❌ Overusing ABCs where duck typing suffices.
❌ Complex inheritance hierarchies.
[Link] 20/29
🧠 Summary Mnemonic: "A PIE Class"
Concept Mnemonic
Abstraction Hides complexity
Polymorphism Same name, different behavior
Inheritance Share and extend behavior
Encapsulation Protect internal data
Class/Object Foundation of OOP
🔷 Python Code Examples for Each OOP
Concept
✅ 1. Class and Object
class Car:
def __init__(self, brand, model):
[Link] = brand
[Link] = model
def start(self):
print(f"{[Link]} {[Link]} is starting...")
# Create object
my_car = Car("Toyota", "Corolla")
my_car.start()
🧠 Explanation:
Car is the class (blueprint)
my_car is an object (instance)
__init__ is the constructor
self refers to the current object
✅ 2. Encapsulation
class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private variable
def deposit(self, amount):
self.__balance += amount
def get_balance(self):
return self.__balance
[Link] 21/29
# Usage
acc = BankAccount(1000)
[Link](500)
print(acc.get_balance())
# print(acc.__balance) # This will raise an AttributeError
🧠 Explanation:
__balance is private
Data access is controlled via methods → this is encapsulation
✅ 3. Inheritance
class Animal:
def speak(self):
print("Animal speaks")
class Dog(Animal):
def speak(self):
print("Dog barks")
# Usage
d = Dog()
[Link]() # Dog barks
🧠 Explanation:
Dog inherits from Animal
Overrides speak() method
✅ 4. Polymorphism
class Bird:
def fly(self):
print("Some birds fly")
class Eagle(Bird):
def fly(self):
print("Eagle soars high")
class Penguin(Bird):
def fly(self):
print("Penguins can't fly")
# Polymorphism
def bird_fly_test(bird: Bird):
[Link]()
bird_fly_test(Eagle())
bird_fly_test(Penguin())
[Link] 22/29
🧠 Explanation:
fly() behaves differently based on the object
Same method name → different behaviors
✅ 5. Abstraction (Using ABC module)
from abc import ABC, abstractmethod
class Vehicle(ABC):
@abstractmethod
def start_engine(self):
pass
class Car(Vehicle):
def start_engine(self):
print("Car engine started")
# v = Vehicle() # Cannot instantiate abstract class
c = Car()
c.start_engine()
🧠 Explanation:
Vehicle is abstract class
start_engine is abstract method
Subclass must implement it
[Link] 23/29
10 Advanced Python Class Exercises with Solutions
These exercises cover advanced Python class concepts including:
• - Abstract Base Classes (ABCs)
• - Magic methods (`__str__`, `__add__`, etc.)
• - Class decorators and descriptors
• - Inheritance and polymorphism
• - Metaclasses
• - Custom exceptions
Exercises
1. Abstract Shape Class
Create an abstract Shape class with abstract methods area() and perimeter(). Implement
concrete classes Circle and Rectangle.
2. Custom Sequence Class
Create a class MySequence that mimics Python’s list but only allows integers. Implement
__getitem__, __len__, and __setitem__.
3. Singleton Class
Implement a singleton class Database that ensures only one instance exists.
4. Class Decorator for Validation
Create a decorator @validate_input that checks if method arguments are positive numbers.
5. Polymorphic Payment System
Create an abstract PaymentMethod class with subclasses CreditCard, PayPal, and
BankTransfer. Each should implement a process_payment(amount) method.
6. Custom Context Manager
Implement a class Timer that measures code execution time using __enter__ and __exit__.
7. Class with Read-Only Properties
Create a class Person with read-only properties name and age (set once at initialization).
8. Metaclass for Method Registration
Create a metaclass RegistryMeta that auto-registers classes in a dictionary when defined.
9. Custom Exception Hierarchy
Implement a hierarchy: BaseError → NetworkError → TimeoutError with proper
inheritance.
[Link] 24/29
10. Descriptor for Unit Conversion
Create a descriptor Temperature that converts between Celsius and Fahrenheit when accessed.
Solutions
1. Abstract Shape Class
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
[Link] = radius
def area(self):
return [Link] * [Link] ** 2
def perimeter(self):
return 2 * [Link] * [Link]
class Rectangle(Shape):
def __init__(self, length, width):
[Link] = length
[Link] = width
def area(self):
return [Link] * [Link]
def perimeter(self):
return 2 * ([Link] + [Link])
2. Custom Sequence Class
class MySequence:
def __init__(self, items):
[Link] = []
for item in items:
if not isinstance(item, int):
raise TypeError("Only integers allowed")
[Link] 25/29
[Link](item)
def __getitem__(self, index):
return [Link][index]
def __len__(self):
return len([Link])
def __setitem__(self, index, value):
if not isinstance(value, int):
raise TypeError("Only integers allowed")
[Link][index] = value
3. Singleton Class
class Database:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
db1 = Database()
db2 = Database()
print(db1 is db2) # True
4. Class Decorator for Validation
def validate_input(func):
def wrapper(self, *args):
for arg in args:
if not isinstance(arg, (int, float)) or arg <= 0:
raise ValueError("Input must be positive number")
return func(self, *args)
return wrapper
class Calculator:
@validate_input
def multiply(self, x, y):
return x * y
5. Polymorphic Payment System
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
[Link] 26/29
def process_payment(self, amount):
pass
class CreditCard(PaymentMethod):
def process_payment(self, amount):
print(f"Processing ${amount} via Credit Card")
class PayPal(PaymentMethod):
def process_payment(self, amount):
print(f"Processing ${amount} via PayPal")
def process_payment(method: PaymentMethod, amount):
method.process_payment(amount)
6. Custom Context Manager (custom “try exception” operation )
import time
class Timer:
def __enter__(self):
[Link] = [Link]()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
[Link] = [Link]()
print(f"Elapsed time: {[Link] - [Link]:.2f}s")
with Timer():
[Link](1) # Simulate work
7. Class with Read-Only Properties
class Person:
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
return self._name
@property
def age(self):
return self._age
p = Person("Alice", 30)
print([Link]) # Alice
[Link] = "Bob" # AttributeError
[Link] 27/29
8. Metaclass for Method Registration
class RegistryMeta(type):
registry = {}
def __new__(cls, name, bases, attrs):
new_class = super().__new__(cls, name, bases, attrs)
[Link][name] = new_class
return new_class
class Animal(metaclass=RegistryMeta):
pass
class Dog(Animal):
pass
print([Link]) # {'Animal': <class '__main__.Animal'>,
'Dog': <class '__main__.Dog'>}
9. Custom Exception Hierarchy
class BaseError(Exception):
"""Base error class"""
pass
class NetworkError(BaseError):
"""Network-related error"""
pass
class TimeoutError(NetworkError):
"""Connection timeout error"""
pass
try:
raise TimeoutError("Timeout occurred")
except BaseError as e:
print(f"Caught error: {e.__class__.__name__}")
10. Descriptor for Unit Conversion
class Temperature:
def __init__(self, celsius=0):
[Link] = celsius
def __get__(self, obj, objtype):
return [Link]
def __set__(self, obj, value):
if not isinstance(value, (int, float)):
raise ValueError("Must be a number")
[Link] 28/29
[Link] = value
@property
def fahrenheit(self):
return ([Link] * 9/5) + 32
@[Link]
def fahrenheit(self, value):
[Link] = (value - 32) * 5/9
class Weather:
temp = Temperature()
w = Weather()
[Link] = 25 # Celsius
print([Link]) # 77.0
[Link] 29/29