Skip to main content

Object-Oriented Programming

Classes and Objects

Class Definition and Instantiation

# Basic class definition
class Person:
def __init__(self, name, age):
self.name = name # Instance variable
self.age = age

def greet(self):
return f"Hello, I'm {self.name}"

# Creating objects (instantiation)
person1 = Person("Alice", 30)
person2 = Person("Bob", 25)

# Accessing attributes and methods
print(person1.name) # "Alice"
print(person1.greet()) # "Hello, I'm Alice"

Class vs Instance Attributes

class Dog:
species = "Canis lupus" # Class variable (shared by all instances)

def __init__(self, name, breed):
self.name = name # Instance variable (unique to each instance)
self.breed = breed

def bark(self):
return f"{self.name} says woof!"

# Usage
dog1 = Dog("Buddy", "Golden Retriever")
dog2 = Dog("Max", "German Shepherd")

print(dog1.species) # "Canis lupus" (same for all dogs)
print(dog1.name) # "Buddy" (unique to dog1)
print(Dog.species) # Access class variable via class name

Constructor and Self

class BankAccount:
def __init__(self, account_number, initial_balance=0):
self.account_number = account_number
self.balance = initial_balance
self.transactions = []

def deposit(self, amount):
self.balance += amount
self.transactions.append(f"Deposited ${amount}")

def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
self.transactions.append(f"Withdrew ${amount}")
else:
raise ValueError("Insufficient funds")

def get_balance(self):
return self.balance

# Usage
account = BankAccount("123456789", 1000)
account.deposit(500)
account.withdraw(200)
print(account.get_balance()) # 1300

Instance Variables and Methods

Instance Variables

class Student:
def __init__(self, name, student_id):
self.name = name
self.student_id = student_id
self.grades = []
self.enrolled_courses = []

def add_grade(self, subject, grade):
self.grades.append((subject, grade))

def enroll_course(self, course):
self.enrolled_courses.append(course)

def get_average_grade(self):
if not self.grades:
return 0
return sum(grade for _, grade in self.grades) / len(self.grades)

# Each instance has its own variables
student1 = Student("Alice", "S001")
student2 = Student("Bob", "S002")

student1.add_grade("Math", 85)
student1.add_grade("Physics", 92)
student2.add_grade("Math", 78)

print(student1.get_average_grade()) # 88.5
print(student2.get_average_grade()) # 78.0

Instance Methods

class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height

def area(self):
"""Calculate area - instance method"""
return self.width * self.height

def perimeter(self):
"""Calculate perimeter - instance method"""
return 2 * (self.width + self.height)

def scale(self, factor):
"""Scale the rectangle - modifies instance"""
self.width *= factor
self.height *= factor

def is_square(self):
"""Check if rectangle is a square"""
return self.width == self.height

# Usage
rect = Rectangle(4, 5)
print(rect.area()) # 20
print(rect.perimeter()) # 18
print(rect.is_square()) # False

rect.scale(2)
print(rect.area()) # 80

Class Variables and Methods

Class Variables

class Employee:
company_name = "Tech Corp" # Class variable
employee_count = 0 # Class variable
salary_multiplier = 1.0 # Class variable

def __init__(self, name, salary):
self.name = name # Instance variable
self.salary = salary # Instance variable
Employee.employee_count += 1 # Modify class variable

def get_annual_salary(self):
return self.salary * Employee.salary_multiplier * 12

@classmethod
def get_employee_count(cls):
return cls.employee_count

@classmethod
def set_salary_multiplier(cls, multiplier):
cls.salary_multiplier = multiplier

# Usage
emp1 = Employee("Alice", 5000)
emp2 = Employee("Bob", 6000)

print(Employee.get_employee_count()) # 2
print(emp1.company_name) # "Tech Corp"

Employee.set_salary_multiplier(1.1)
print(emp1.get_annual_salary()) # 66000.0

Class Methods

class Person:
def __init__(self, name, age):
self.name = name
self.age = age

@classmethod
def from_string(cls, person_str):
"""Alternative constructor - class method"""
name, age = person_str.split('-')
return cls(name, int(age))

@classmethod
def from_dict(cls, data):
"""Another alternative constructor"""
return cls(data['name'], data['age'])

@classmethod
def get_species(cls):
"""Class method returning class information"""
return "Homo sapiens"

# Usage - different ways to create objects
person1 = Person("Alice", 30) # Regular constructor
person2 = Person.from_string("Bob-25") # Class method
person3 = Person.from_dict({"name": "Charlie", "age": 35}) # Class method

print(Person.get_species()) # "Homo sapiens"

Static Methods

Static Method Definition

class MathUtils:
@staticmethod
def add(a, b):
"""Static method - doesn't need class or instance"""
return a + b

@staticmethod
def is_prime(n):
"""Check if number is prime"""
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True

@staticmethod
def factorial(n):
"""Calculate factorial"""
if n <= 1:
return 1
return n * MathUtils.factorial(n - 1)

# Usage - can call without creating instance
print(MathUtils.add(5, 3)) # 8
print(MathUtils.is_prime(17)) # True
print(MathUtils.factorial(5)) # 120

# Or with instance (but not recommended)
utils = MathUtils()
print(utils.add(2, 3)) # 5

Static vs Class vs Instance Methods

class Example:
class_var = "class variable"

def __init__(self, value):
self.instance_var = value

def instance_method(self):
"""Has access to self and class"""
return f"Instance: {self.instance_var}, Class: {self.class_var}"

@classmethod
def class_method(cls):
"""Has access to class but not instance"""
return f"Class: {cls.class_var}"

@staticmethod
def static_method():
"""No access to class or instance"""
return "Static method - no access to class or instance"

# Usage
obj = Example("instance value")
print(obj.instance_method()) # Instance: instance value, Class: class variable
print(Example.class_method()) # Class: class variable
print(Example.static_method()) # Static method - no access to class or instance

Inheritance

Single Inheritance

class Animal:
def __init__(self, name, species):
self.name = name
self.species = species

def speak(self):
return f"{self.name} makes a sound"

def move(self):
return f"{self.name} moves"

class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name, "Canis lupus") # Call parent constructor
self.breed = breed

def speak(self):
return f"{self.name} barks"

def fetch(self):
return f"{self.name} fetches the ball"

class Cat(Animal):
def __init__(self, name, color):
super().__init__(name, "Felis catus")
self.color = color

def speak(self):
return f"{self.name} meows"

def climb(self):
return f"{self.name} climbs the tree"

# Usage
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Gray")

print(dog.speak()) # "Buddy barks"
print(cat.speak()) # "Whiskers meows"
print(dog.move()) # "Buddy moves" (inherited)
print(dog.fetch()) # "Buddy fetches the ball"

Multiple Inheritance

class Flyable:
def fly(self):
return "Flying through the air"

def land(self):
return "Landing safely"

class Swimmable:
def swim(self):
return "Swimming in water"

def dive(self):
return "Diving underwater"

class Bird(Animal, Flyable):
def __init__(self, name, wingspan):
super().__init__(name, "Aves")
self.wingspan = wingspan

class Duck(Bird, Swimmable):
def __init__(self, name, wingspan):
super().__init__(name, wingspan)

def speak(self):
return f"{self.name} quacks"

# Usage
duck = Duck("Donald", 24)
print(duck.speak()) # "Donald quacks"
print(duck.fly()) # "Flying through the air"
print(duck.swim()) # "Swimming in water"
print(duck.move()) # "Donald moves" (from Animal)

# Method Resolution Order (MRO)
print(Duck.__mro__) # Shows the order of method resolution

Inheritance with Complex Hierarchies

class Vehicle:
def __init__(self, brand, model, year):
self.brand = brand
self.model = model
self.year = year

def start(self):
return f"{self.brand} {self.model} is starting"

def stop(self):
return f"{self.brand} {self.model} is stopping"

class Car(Vehicle):
def __init__(self, brand, model, year, doors):
super().__init__(brand, model, year)
self.doors = doors

def honk(self):
return "Beep beep!"

class ElectricCar(Car):
def __init__(self, brand, model, year, doors, battery_capacity):
super().__init__(brand, model, year, doors)
self.battery_capacity = battery_capacity

def charge(self):
return f"Charging {self.brand} {self.model}"

def start(self):
return f"{self.brand} {self.model} is starting silently"

# Usage
tesla = ElectricCar("Tesla", "Model 3", 2023, 4, 75)
print(tesla.start()) # "Tesla Model 3 is starting silently"
print(tesla.charge()) # "Charging Tesla Model 3"
print(tesla.honk()) # "Beep beep!" (inherited from Car)

Method Overriding and super()

Method Overriding

class Shape:
def __init__(self, color):
self.color = color

def area(self):
raise NotImplementedError("Subclass must implement area method")

def describe(self):
return f"A {self.color} shape with area {self.area()}"

class Circle(Shape):
def __init__(self, color, radius):
super().__init__(color)
self.radius = radius

def area(self):
return 3.14159 * self.radius ** 2

class Rectangle(Shape):
def __init__(self, color, width, height):
super().__init__(color)
self.width = width
self.height = height

def area(self):
return self.width * self.height

def describe(self):
# Override parent method with additional info
base_description = super().describe()
return f"{base_description} (Rectangle: {self.width}x{self.height})"

# Usage
circle = Circle("red", 5)
rectangle = Rectangle("blue", 4, 6)

print(circle.describe()) # "A red shape with area 78.53975"
print(rectangle.describe()) # "A blue shape with area 24 (Rectangle: 4x6)"

Advanced super() Usage

class A:
def __init__(self):
print("A.__init__")

def method(self):
print("A.method")

class B(A):
def __init__(self):
print("B.__init__")
super().__init__()

def method(self):
print("B.method")
super().method()

class C(A):
def __init__(self):
print("C.__init__")
super().__init__()

def method(self):
print("C.method")
super().method()

class D(B, C):
def __init__(self):
print("D.__init__")
super().__init__()

def method(self):
print("D.method")
super().method()

# Usage
d = D()
# Output:
# D.__init__
# B.__init__
# C.__init__
# A.__init__

d.method()
# Output:
# D.method
# B.method
# C.method
# A.method

Magic Methods (Dunder Methods)

Common Magic Methods

class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages

def __str__(self):
"""String representation for humans"""
return f"'{self.title}' by {self.author}"

def __repr__(self):
"""String representation for developers"""
return f"Book('{self.title}', '{self.author}', {self.pages})"

def __len__(self):
"""Length of the book"""
return self.pages

def __eq__(self, other):
"""Equality comparison"""
if not isinstance(other, Book):
return False
return (self.title == other.title and
self.author == other.author)

def __lt__(self, other):
"""Less than comparison (for sorting)"""
return self.pages < other.pages

def __add__(self, other):
"""Add two books (combine pages)"""
if isinstance(other, Book):
return self.pages + other.pages
return NotImplemented

# Usage
book1 = Book("1984", "George Orwell", 328)
book2 = Book("Animal Farm", "George Orwell", 112)

print(str(book1)) # "'1984' by George Orwell"
print(repr(book1)) # "Book('1984', 'George Orwell', 328)"
print(len(book1)) # 328
print(book1 == book2) # False
print(book1 < book2) # False
print(book1 + book2) # 440

Arithmetic Magic Methods

class Vector:
def __init__(self, x, y):
self.x = x
self.y = y

def __str__(self):
return f"Vector({self.x}, {self.y})"

def __add__(self, other):
"""Vector addition"""
return Vector(self.x + other.x, self.y + other.y)

def __sub__(self, other):
"""Vector subtraction"""
return Vector(self.x - other.x, self.y - other.y)

def __mul__(self, scalar):
"""Scalar multiplication"""
return Vector(self.x * scalar, self.y * scalar)

def __truediv__(self, scalar):
"""Scalar division"""
return Vector(self.x / scalar, self.y / scalar)

def __abs__(self):
"""Vector magnitude"""
return (self.x ** 2 + self.y ** 2) ** 0.5

def __neg__(self):
"""Unary negation"""
return Vector(-self.x, -self.y)

# Usage
v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(2, 2)
print(v1 * 2) # Vector(6, 8)
print(v1 / 2) # Vector(1.5, 2.0)
print(abs(v1)) # 5.0
print(-v1) # Vector(-3, -4)

Container Magic Methods

class CustomList:
def __init__(self):
self._items = []

def __len__(self):
return len(self._items)

def __getitem__(self, index):
return self._items[index]

def __setitem__(self, index, value):
self._items[index] = value

def __delitem__(self, index):
del self._items[index]

def __contains__(self, item):
return item in self._items

def __iter__(self):
return iter(self._items)

def append(self, item):
self._items.append(item)

# Usage
custom_list = CustomList()
custom_list.append(1)
custom_list.append(2)
custom_list.append(3)

print(len(custom_list)) # 3
print(custom_list[0]) # 1
print(2 in custom_list) # True

for item in custom_list:
print(item) # 1, 2, 3

Properties and Descriptors

Properties with @property

class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius

@property
def celsius(self):
"""Get temperature in Celsius"""
return self._celsius

@celsius.setter
def celsius(self, value):
"""Set temperature in Celsius"""
if value < -273.15:
raise ValueError("Temperature below absolute zero is not possible")
self._celsius = value

@property
def fahrenheit(self):
"""Get temperature in Fahrenheit"""
return (self._celsius * 9/5) + 32

@fahrenheit.setter
def fahrenheit(self, value):
"""Set temperature in Fahrenheit"""
self.celsius = (value - 32) * 5/9

@property
def kelvin(self):
"""Get temperature in Kelvin"""
return self._celsius + 273.15

# Usage
temp = Temperature(25)
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0
print(temp.kelvin) # 298.15

temp.fahrenheit = 100
print(temp.celsius) # 37.77777777777778

Computed Properties

class Circle:
def __init__(self, radius):
self._radius = radius

@property
def radius(self):
return self._radius

@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value

@property
def diameter(self):
return self._radius * 2

@property
def area(self):
return 3.14159 * self._radius ** 2

@property
def circumference(self):
return 2 * 3.14159 * self._radius

# Usage
circle = Circle(5)
print(circle.radius) # 5
print(circle.diameter) # 10
print(circle.area) # 78.53975
print(circle.circumference) # 31.4159

circle.radius = 3
print(circle.area) # 28.274309999999996

Descriptors

class Validator:
def __init__(self, min_value=None, max_value=None):
self.min_value = min_value
self.max_value = max_value

def __set_name__(self, owner, name):
self.name = name

def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self.name]

def __set__(self, instance, value):
if self.min_value is not None and value < self.min_value:
raise ValueError(f"{self.name} must be >= {self.min_value}")
if self.max_value is not None and value > self.max_value:
raise ValueError(f"{self.name} must be <= {self.max_value}")
instance.__dict__[self.name] = value

class Person:
age = Validator(min_value=0, max_value=150)
height = Validator(min_value=0)

def __init__(self, name, age, height):
self.name = name
self.age = age
self.height = height

# Usage
person = Person("Alice", 30, 170)
print(person.age) # 30

try:
person.age = -5 # Raises ValueError
except ValueError as e:
print(e) # "age must be >= 0"

Abstract Base Classes

Using abc Module

from abc import ABC, abstractmethod

class Animal(ABC):
def __init__(self, name):
self.name = name

@abstractmethod
def speak(self):
"""Abstract method - must be implemented by subclasses"""
pass

@abstractmethod
def move(self):
"""Abstract method - must be implemented by subclasses"""
pass

def introduce(self):
"""Concrete method - can be inherited"""
return f"I am {self.name}"

class Dog(Animal):
def speak(self):
return f"{self.name} barks"

def move(self):
return f"{self.name} runs"

class Fish(Animal):
def speak(self):
return f"{self.name} cannot speak"

def move(self):
return f"{self.name} swims"

# Usage
# animal = Animal("Generic") # TypeError: Can't instantiate abstract class

dog = Dog("Buddy")
fish = Fish("Nemo")

print(dog.speak()) # "Buddy barks"
print(fish.move()) # "Nemo swims"
print(dog.introduce()) # "I am Buddy"

Abstract Properties

from abc import ABC, abstractmethod

class Shape(ABC):
@property
@abstractmethod
def area(self):
"""Abstract property - must be implemented"""
pass

@property
@abstractmethod
def perimeter(self):
"""Abstract property - must be implemented"""
pass

def describe(self):
return f"Area: {self.area}, Perimeter: {self.perimeter}"

class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height

@property
def area(self):
return self.width * self.height

@property
def perimeter(self):
return 2 * (self.width + self.height)

class Circle(Shape):
def __init__(self, radius):
self.radius = radius

@property
def area(self):
return 3.14159 * self.radius ** 2

@property
def perimeter(self):
return 2 * 3.14159 * self.radius

# Usage
rectangle = Rectangle(4, 5)
circle = Circle(3)

print(rectangle.describe()) # "Area: 20, Perimeter: 18"
print(circle.describe()) # "Area: 28.274309999999996, Perimeter: 18.84954"

Polymorphism and Encapsulation

Polymorphism

class Animal:
def speak(self):
raise NotImplementedError("Subclass must implement speak method")

class Dog(Animal):
def speak(self):
return "Woof!"

class Cat(Animal):
def speak(self):
return "Meow!"

class Cow(Animal):
def speak(self):
return "Moo!"

def animal_sounds(animals):
"""Polymorphic function - works with any Animal subclass"""
for animal in animals:
print(f"{animal.__class__.__name__} says: {animal.speak()}")

# Usage
animals = [Dog(), Cat(), Cow()]
animal_sounds(animals)
# Output:
# Dog says: Woof!
# Cat says: Meow!
# Cow says: Moo!

Encapsulation (Private Members)

class BankAccount:
def __init__(self, account_number, initial_balance=0):
self._account_number = account_number # Protected (convention)
self.__balance = initial_balance # Private (name mangling)
self.__transactions = [] # Private

def deposit(self, amount):
if amount > 0:
self.__balance += amount
self.__transactions.append(f"Deposited: ${amount}")
else:
raise ValueError("Deposit amount must be positive")

def withdraw(self, amount):
if amount > 0 and amount <= self.__balance:
self.__balance -= amount
self.__transactions.append(f"Withdrew: ${amount}")
else:
raise ValueError("Invalid withdrawal amount")

def get_balance(self):
"""Public method to access private balance"""
return self.__balance

def get_statement(self):
"""Public method to access private transactions"""
return self.__transactions.copy()

def _internal_method(self):
"""Protected method (convention)"""
return "Internal processing"

# Usage
account = BankAccount("123456789", 1000)
account.deposit(500)
account.withdraw(200)

print(account.get_balance()) # 1300
print(account.get_statement()) # ['Deposited: $500', 'Withdrew: $200']

# These will raise AttributeError:
# print(account.__balance) # AttributeError
# print(account.__transactions) # AttributeError

# But name mangling allows access (not recommended):
print(account._BankAccount__balance) # 1300

Data Hiding with Properties

class Person:
def __init__(self, name, age):
self._name = name
self._age = age

@property
def name(self):
return self._name

@name.setter
def name(self, value):
if not isinstance(value, str) or len(value) < 1:
raise ValueError("Name must be a non-empty string")
self._name = value

@property
def age(self):
return self._age

@age.setter
def age(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError("Age must be a non-negative integer")
self._age = value

@property
def is_adult(self):
"""Read-only computed property"""
return self._age >= 18

# Usage
person = Person("Alice", 25)
print(person.name) # "Alice"
print(person.age) # 25
print(person.is_adult) # True

person.name = "Bob" # OK
person.age = 30 # OK

try:
person.age = -5 # Raises ValueError
except ValueError as e:
print(e) # "Age must be a non-negative integer"

Design Patterns

Factory Pattern

from abc import ABC, abstractmethod

class Vehicle(ABC):
@abstractmethod
def start(self):
pass

@abstractmethod
def stop(self):
pass

class Car(Vehicle):
def start(self):
return "Car engine started"

def stop(self):
return "Car engine stopped"

class Motorcycle(Vehicle):
def start(self):
return "Motorcycle engine started"

def stop(self):
return "Motorcycle engine stopped"

class Bicycle(Vehicle):
def start(self):
return "Ready to pedal"

def stop(self):
return "Stopped pedaling"

class VehicleFactory:
@staticmethod
def create_vehicle(vehicle_type):
if vehicle_type.lower() == "car":
return Car()
elif vehicle_type.lower() == "motorcycle":
return Motorcycle()
elif vehicle_type.lower() == "bicycle":
return Bicycle()
else:
raise ValueError(f"Unknown vehicle type: {vehicle_type}")

# Usage
factory = VehicleFactory()
car = factory.create_vehicle("car")
bike = factory.create_vehicle("bicycle")

print(car.start()) # "Car engine started"
print(bike.start()) # "Ready to pedal"

Singleton Pattern

class Singleton:
_instance = None
_initialized = False

def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self):
if not self._initialized:
self.value = 0
self._initialized = True

def increment(self):
self.value += 1

def get_value(self):
return self.value

# Usage
s1 = Singleton()
s2 = Singleton()

print(s1 is s2) # True (same instance)

s1.increment()
print(s2.get_value()) # 1 (shared state)

Observer Pattern

class Subject:
def __init__(self):
self._observers = []
self._state = None

def attach(self, observer):
self._observers.append(observer)

def detach(self, observer):
self._observers.remove(observer)

def notify(self):
for observer in self._observers:
observer.update(self._state)

def set_state(self, state):
self._state = state
self.notify()

class Observer:
def __init__(self, name):
self.name = name

def update(self, state):
print(f"{self.name} received update: {state}")

class NewsAgency(Subject):
def __init__(self):
super().__init__()
self._news = ""

def set_news(self, news):
self._news = news
self.set_state(news)

def get_news(self):
return self._news

# Usage
news_agency = NewsAgency()

observer1 = Observer("CNN")
observer2 = Observer("BBC")
observer3 = Observer("Reuters")

news_agency.attach(observer1)
news_agency.attach(observer2)
news_agency.attach(observer3)

news_agency.set_news("Breaking: Python 4.0 Released!")
# Output:
# CNN received update: Breaking: Python 4.0 Released!
# BBC received update: Breaking: Python 4.0 Released!
# Reuters received update: Breaking: Python 4.0 Released!

Strategy Pattern

from abc import ABC, abstractmethod

class SortingStrategy(ABC):
@abstractmethod
def sort(self, data):
pass

class BubbleSort(SortingStrategy):
def sort(self, data):
data = data.copy()
n = len(data)
for i in range(n):
for j in range(0, n - i - 1):
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
return data

class QuickSort(SortingStrategy):
def sort(self, data):
if len(data) <= 1:
return data
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)

class SortContext:
def __init__(self, strategy):
self._strategy = strategy

def set_strategy(self, strategy):
self._strategy = strategy

def sort_data(self, data):
return self._strategy.sort(data)

# Usage
data = [64, 34, 25, 12, 22, 11, 90]

sorter = SortContext(BubbleSort())
print(sorter.sort_data(data)) # [11, 12, 22, 25, 34, 64, 90]

sorter.set_strategy(QuickSort())
print(sorter.sort_data(data)) # [11, 12, 22, 25, 34, 64, 90]

Advanced OOP Concepts

Metaclasses

class SingletonMeta(type):
_instances = {}

def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]

class Database(metaclass=SingletonMeta):
def __init__(self):
self.connection = "Database connection"

def query(self, sql):
return f"Executing: {sql}"

# Usage
db1 = Database()
db2 = Database()
print(db1 is db2) # True

Composition vs Inheritance

# Composition example
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower

def start(self):
return f"Engine with {self.horsepower} HP started"

class Car:
def __init__(self, brand, engine):
self.brand = brand
self.engine = engine # Composition: Car HAS-A Engine

def start(self):
return f"{self.brand} car: {self.engine.start()}"

# Usage
engine = Engine(200)
car = Car("Toyota", engine)
print(car.start()) # "Toyota car: Engine with 200 HP started"

Context Managers

class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None

def __enter__(self):
print(f"Opening file: {self.filename}")
self.file = open(self.filename, self.mode)
return self.file

def __exit__(self, exc_type, exc_value, traceback):
print(f"Closing file: {self.filename}")
if self.file:
self.file.close()

if exc_type:
print(f"Exception occurred: {exc_type.__name__}: {exc_value}")
return False # Don't suppress exceptions

# Usage
with FileManager("test.txt", "w") as f:
f.write("Hello, World!")
# File is automatically closed

Quick Reference

Method Types Summary

class Example:
class_var = "shared"

def __init__(self, value):
self.instance_var = value

def instance_method(self):
# Access to self and class
return self.instance_var

@classmethod
def class_method(cls):
# Access to class, not instance
return cls.class_var

@staticmethod
def static_method():
# No access to class or instance
return "independent function"

@property
def computed_property(self):
# Computed property
return self.instance_var * 2

Common Magic Methods

class MyClass:
def __init__(self): # Constructor
pass

def __str__(self): # String representation
return "MyClass instance"

def __repr__(self): # Developer representation
return "MyClass()"

def __len__(self): # Length
return 0

def __getitem__(self, key): # Indexing
return None

def __setitem__(self, key, value): # Item assignment
pass

def __contains__(self, item): # 'in' operator
return False

def __iter__(self): # Iteration
return iter([])

def __call__(self): # Make instance callable
return "Called!"

def __enter__(self): # Context manager enter
return self

def __exit__(self, exc_type, exc_value, traceback): # Context manager exit
pass

Inheritance Best Practices

class Parent:
def __init__(self, name):
self.name = name

def greet(self):
return f"Hello from {self.name}"

class Child(Parent):
def __init__(self, name, age):
super().__init__(name) # Call parent constructor
self.age = age

def greet(self):
# Extend parent method
parent_greeting = super().greet()
return f"{parent_greeting}, I'm {self.age} years old"

This comprehensive cheatsheet covers all major Python OOP concepts with practical examples. Use it as a reference for building robust object-oriented applications.