๐ฆ Classes & Instances fundamentals
__init__ โข self โข class vs instance attributes
class Employee:
company = "TechCorp" # class variable - shared
def __init__(self, name, salary):
self.name = name # instance variable - unique
self.salary = salary
def give_raise(self, percent):
self.salary *= (1 + percent/100)
def __repr__(self):
return f"Employee({self.name}, ${self.salary:,.0f})"
alice = Employee("Alice Chen", 95000)
bob = Employee("Bob Smith", 82000)
alice.give_raise(10)
print(alice) # Employee(Alice Chen, $104,500)
self โ explicit instance reference
- Class variables shared across instances
- Instance variables stored in
__dict__
__repr__ for developer-friendly output
#python__init__self__dict__
๐งฌ Inheritance & MRO is-a relationships
super() โข method overriding โข C3 linearization
class Vehicle:
def __init__(self, brand):
self.brand = brand
def move(self):
return f"{self.brand} moves"
class Car(Vehicle):
def __init__(self, brand, model):
super().__init__(brand) # call parent constructor
self.model = model
def move(self): # override
return f"{self.brand} {self.model} drives"
class Boat(Vehicle):
def move(self):
return f"{self.brand} sails"
vehicles = [Car("Tesla", "Model 3"), Boat("Yamaha")]
for v in vehicles:
print(v.move())
super() follows MRO, not just parent
isinstance(), issubclass() checks
ClassName.__mro__ shows resolution order
super()MROpolymorphism
๐ Encapsulation name mangling
protected _single โข private __double โข properties
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # name mangled to _BankAccount__balance
@property
def balance(self): # getter
return self.__balance
@balance.setter
def balance(self, amount): # setter with validation
if amount < 0:
raise ValueError("Balance cannot be negative")
self.__balance = amount
def withdraw(self, amount):
if amount <= self.__balance:
self.__balance -= amount
return amount
raise ValueError("Insufficient funds")
acc = BankAccount("Bob", 5000)
print(acc.balance) # 5000 (uses getter)
acc.balance = 6000 # uses setter
_single โ protected by convention
__double โ name mangling prevents accidental override
@property โ controlled attribute access
@property@setterencapsulation
๐ญ Polymorphism duck typing + ABC
abstract base classes โข protocols โข duck typing
from abc import ABC, abstractmethod
class Drawable(ABC):
@abstractmethod
def draw(self): pass
@abstractmethod
def area(self): pass
class Circle(Drawable):
def __init__(self, radius):
self.radius = radius
def draw(self):
return "โ"
def area(self):
return 3.14159 * self.radius ** 2
class Square(Drawable):
def __init__(self, side):
self.side = side
def draw(self):
return "โก"
def area(self):
return self.side ** 2
# Duck typing example - any object with .draw() works
def render(shape):
print(shape.draw()) # doesn't care about type
render(Circle(5)) # โ
render(Square(4)) # โก
- Duck typing: "if it walks like a duck"
- ABCs enforce interface contracts
@abstractmethod requires implementation
ABCduck typingprotocol
โ๏ธ Dunder Methods operator overloading
__repr__ โข __len__ โข __eq__ โข __call__
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __repr__(self): # unambiguous representation
return f"Book('{self.title}', '{self.author}')"
def __str__(self): # user-friendly string
return f"'{self.title}' by {self.author}"
def __len__(self): # enable len()
return self.pages
def __eq__(self, other): # enable ==
return self.title == other.title and self.author == other.author
def __lt__(self, other): # enable sorting
return self.pages < other.pages
def __call__(self): # object as function
return f"Reading {self.title}"
book = Book("1984", "Orwell", 328)
print(book()) # Reading 1984 (__call__)
print(len(book)) # 328 (__len__)
__str__ for users, __repr__ for devs
__add__, __sub__ for arithmetic
__getitem__ for container behavior
__repr____eq____call__
๐ท๏ธ Class & Static Methods
@classmethod โข @staticmethod โข factories
from datetime import datetime
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_birth_year(cls, name, birth_year):
"""Alternative constructor - creates Person from birth year"""
age = datetime.now().year - birth_year
return cls(name, age)
@classmethod
def from_string(cls, data_string):
"""Factory: 'name,age' -> Person"""
name, age = data_string.split(',')
return cls(name.strip(), int(age))
@staticmethod
def is_adult(age):
"""Utility function - no self or cls needed"""
return age >= 18
def __repr__(self):
return f"Person('{self.name}', {self.age})"
p1 = Person.from_birth_year("Alice", 1995)
p2 = Person.from_string("Bob, 25")
print(Person.is_adult(20)) # True
@classmethod receives class (cls)
@staticmethod no self/cls โ like regular function
- Classmethods for alternative constructors
@classmethod@staticmethodfactory
๐ Composition vs Inheritance
"has-a" โข "uses-a" โข loose coupling
class Engine:
def start(self):
return "Engine started"
def stop(self):
return "Engine stopped"
class Wheels:
def __init__(self, count=4):
self.count = count
def rotate(self):
return f"{self.count} wheels rotating"
class Car:
"""Composition: Car has an Engine and Wheels"""
def __init__(self):
self.engine = Engine() # composition - strong ownership
self.wheels = Wheels() # parts created with car
def drive(self):
return f"{self.engine.start()}, {self.wheels.rotate()}"
class MusicPlayer:
def play(self):
return "Playing music"
class ModernCar(Car):
"""Inheritance: ModernCar is a Car with extra features"""
def __init__(self):
super().__init__()
self.player = MusicPlayer() # composition within inheritance
def drive_with_music(self):
return f"{self.drive()} while {self.player.play()}"
my_car = ModernCar()
print(my_car.drive_with_music())
- Favor composition over inheritance
- Composition: flexible, loose coupling
- Inheritance: use for true is-a relationships
has-acompositionloose coupling
๐งฐ Dataclasses (3.7+) modern Python
@dataclass โข frozen โข field
from dataclasses import dataclass, field
from typing import List
@dataclass(order=True, frozen=False)
class InventoryItem:
name: str
unit_price: float
quantity: int = 0
@property
def total_cost(self):
return self.unit_price * self.quantity
@dataclass(frozen=True) # immutable
class Point:
x: float
y: float
z: float = 0.0
def distance_from_origin(self):
return (self.x**2 + self.y**2 + self.z**2) ** 0.5
@dataclass
class ShoppingCart:
items: List[InventoryItem] = field(default_factory=list)
customer: str = "Guest"
def add_item(self, item):
self.items.append(item)
@property
def total(self):
return sum(item.total_cost for item in self.items)
item = InventoryItem("Laptop", 999.99, 2)
print(item.total_cost) # 1999.98
- Auto-generates
__init__, __repr__, __eq__
frozen=True for immutability
default_factory for mutable defaults
@dataclassfrozentype hints
๐ง Advanced OOP Concepts
๐ Multiple Inheritance C3 linearization
MRO โข diamond problem โข mixins
class LoggerMixin:
"""Mixin adding logging capabilities"""
def log(self, message):
print(f"[{self.__class__.__name__}] {message}")
class SerializableMixin:
"""Mixin for JSON serialization"""
def to_dict(self):
return self.__dict__
def to_json(self):
import json
return json.dumps(self.to_dict())
class ValidatorMixin:
"""Mixin for data validation"""
def validate_positive(self, value, name):
if value < 0:
raise ValueError(f"{name} must be positive")
return value
class Product(LoggerMixin, SerializableMixin, ValidatorMixin):
def __init__(self, name, price):
self.name = name
self.price = self.validate_positive(price, "price")
self.log(f"Created product: {name}")
def __repr__(self):
return f"Product({self.name}, ${self.price})"
# Method Resolution Order demonstration
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass # MRO: D -> B -> C -> A -> object
print(D.__mro__) # Shows resolution order
- Mixins add focused, reusable behavior
super() follows MRO cooperatively
__mro__ attribute shows order
mixinMROdiamond
๐จ Descriptors __get__ / __set__
Power behind @property, @classmethod, @staticmethod
class PositiveNumber:
"""Descriptor that only allows positive values"""
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.name, 0)
def __set__(self, obj, value):
if value < 0:
raise ValueError(f"{self.name} must be >= 0")
obj.__dict__[self.name] = value
def __delete__(self, obj):
del obj.__dict__[self.name]
class Temperature:
celsius = PositiveNumber()
fahrenheit = PositiveNumber()
def __init__(self, celsius=0, fahrenheit=32):
self.celsius = celsius
self.fahrenheit = fahrenheit
@property
def kelvin(self):
return self.celsius + 273.15
temp = Temperature(25, 77)
print(temp.celsius) # 25
# temp.celsius = -10 # ValueError: celsius must be >= 0
- Data descriptor: defines
__get__ + __set__
- Non-data descriptor: only
__get__
__set_name__ gets attribute name (3.6+)
__get____set__descriptor
๐๏ธ __slots__ memory optimization
Replace __dict__ with fixed array โ 40-60% less memory
class PointSlots:
"""Memory-efficient point class"""
__slots__ = ('x', 'y') # No __dict__ created
def __init__(self, x, y):
self.x = x
self.y = y
def distance(self, other):
return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5
class PointDict:
"""Traditional point with __dict__ overhead"""
def __init__(self, x, y):
self.x = x
self.y = y
# Memory comparison
import sys
ps = PointSlots(10, 20)
pd = PointDict(10, 20)
print(f"__slots__ size: {sys.getsizeof(ps)} bytes")
print(f"__dict__ size: {sys.getsizeof(pd)} bytes")
# ps.z = 30 # AttributeError - can't add new attributes
# For data-heavy applications (millions of objects)
class Vector3D:
__slots__ = ('x', 'y', 'z', '_length')
def __init__(self, x, y, z):
self.x, self.y, self.z = x, y, z
self._length = None
@property
def length(self):
if self._length is None:
self._length = (self.x**2 + self.y**2 + self.z**2) ** 0.5
return self._length
- ~3x faster attribute access
- Prevents dynamic attribute creation
- Child classes need their own
__slots__
__slots__memoryperformance
๐ __iter__ & __next__ custom iterators
Make any object iterable โข generators โข lazy evaluation
class Fibonacci:
"""Iterator that generates Fibonacci numbers"""
def __init__(self, max_count):
self.max_count = max_count
self.count = 0
self.a, self.b = 0, 1
def __iter__(self):
return self # iterator returns itself
def __next__(self):
if self.count >= self.max_count:
raise StopIteration
self.count += 1
self.a, self.b = self.b, self.a + self.b
return self.a
def __repr__(self):
return f"Fibonacci({self.max_count})"
# Using the iterator
fib = Fibonacci(10)
for num in fib:
print(num, end=' ') # 1 1 2 3 5 8 13 21 34 55
# Generator version (simpler)
def fibonacci_gen(max_count):
a, b = 0, 1
for _ in range(max_count):
a, b = b, a + b
yield a
# Reversible iterable
class Range:
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
return (i for i in range(self.start, self.end))
def __reversed__(self):
return (i for i in range(self.end-1, self.start-1, -1))
__iter__ returns iterator object
__next__ returns next value
StopIteration ends iteration
__iter____next__generator
๐งต Metaclasses class factories
type โข __new__ โข class creation control
class SingletonMeta(type):
"""Metaclass that creates singleton classes"""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class AutoPropertyMeta(type):
"""Metaclass that auto-adds properties"""
def __new__(mcs, name, bases, namespace):
# Auto-create properties for attributes starting with '_'
for key, value in list(namespace.items()):
if key.startswith('_') and not key.startswith('__'):
prop_name = key[1:] # remove leading underscore
namespace[prop_name] = property(
lambda self, k=key: getattr(self, k),
lambda self, val, k=key: setattr(self, k, val)
)
return super().__new__(mcs, name, bases, namespace)
class Logger(metaclass=SingletonMeta):
"""Only one instance ever created"""
def __init__(self):
self.logs = []
def log(self, msg):
self.logs.append(msg)
print(f"LOG: {msg}")
class Person(metaclass=AutoPropertyMeta):
def __init__(self, name):
self._name = name # automatically gets .name property
p = Person("Alice")
print(p.name) # Alice (auto-generated property)
- Metaclasses control class creation
type is default metaclass
- Use
__init_subclass__ for simpler cases
metaclasstypesingleton
๐ช __getattr__ & __getattribute__
Dynamic attribute handling โข proxies โข fallbacks
class DynamicAttributes:
"""Returns default for missing attributes"""
def __init__(self):
self._data = {}
def __getattr__(self, name):
"""Called only when normal lookup fails"""
if name.startswith('get_'):
key = name[4:] # remove 'get_' prefix
return lambda: self._data.get(key, None)
return f"Attribute '{name}' not found"
def __setattr__(self, name, value):
"""Intercepts all attribute setting"""
if name.startswith('_'):
super().__setattr__(name, value) # bypass for internal
else:
self._data[name] = value
def __getattribute__(self, name):
"""Called for EVERY attribute access - use carefully!"""
print(f"Accessing: {name}")
return super().__getattribute__(name)
class Proxy:
"""Proxy pattern with __getattr__"""
def __init__(self, target):
self._target = target
def __getattr__(self, name):
"""Delegate to target object"""
print(f"Proxy: delegating {name}")
return getattr(self._target, name)
def __setattr__(self, name, value):
if name == '_target':
super().__setattr__(name, value)
else:
print(f"Proxy: setting {name} on target")
setattr(self._target, name, value)
obj = DynamicAttributes()
obj.name = "test"
print(obj.name) # test (via __setattr__ and __getattribute__)
__getattr__ โ fallback for missing attrs
__getattribute__ โ intercepts all access
- Use
object.__getattribute__ to avoid recursion
__getattr____setattr__proxy
โก __new__ vs __init__ creation pipeline
__new__ creates โข __init__ initializes
class ImmutablePoint:
"""Immutable point using __new__"""
def __new__(cls, x, y):
# Create instance
instance = super().__new__(cls)
# Initialize once and make immutable
instance._x = x
instance._y = y
return instance
def __init__(self, x, y):
# This won't be called if __new__ doesn't call it
# But if it is, we need to handle it
pass
@property
def x(self):
return self._x
@property
def y(self):
return self._y
def __setattr__(self, name, value):
if hasattr(self, '_x'): # Already initialized
raise AttributeError("ImmutablePoint is immutable")
super().__setattr__(name, value)
class DatabaseConnection:
"""Connection pool with __new__"""
_pool = {}
def __new__(cls, db_name):
if db_name not in cls._pool:
instance = super().__new__(cls)
cls._pool[db_name] = instance
return instance
return cls._pool[db_name]
def __init__(self, db_name):
self.db_name = db_name
# This runs every time, even if returning cached
# Usage
p1 = ImmutablePoint(3, 4)
p2 = ImmutablePoint(3, 4)
print(p1.x, p1.y) # 3 4
# p1.x = 5 # AttributeError
__new__ is class method, returns instance
__init__ initializes existing instance
- Use
__new__ for immutable types, singletons
__new____init__immutable
๐ __init_subclass__ subclass hooks
Simpler alternative to metaclasses for subclass registration
class PluginBase:
"""Base class that auto-registers all plugins"""
_registry = {}
def __init_subclass__(cls, name=None, **kwargs):
"""Called when a subclass is created"""
super().__init_subclass__(**kwargs)
plugin_name = name or cls.__name__
cls.name = plugin_name
PluginBase._registry[plugin_name] = cls
print(f"Registered plugin: {plugin_name}")
def process(self, data):
raise NotImplementedError
class JSONPlugin(PluginBase, name="json"):
def process(self, data):
import json
return json.dumps(data)
class CSVPlugin(PluginBase, name="csv"):
def process(self, data):
return ",".join(str(x) for x in data)
class XMLPlugin(PluginBase):
def process(self, data):
return f"{data}"
# All plugins auto-registered
print(PluginBase._registry)
# {'json': JSONPlugin, 'csv': CSVPlugin, 'XMLPlugin': XMLPlugin}
# Plugin factory
def process_data(data, format_name):
plugin_cls = PluginBase._registry.get(format_name)
if not plugin_cls:
raise ValueError(f"Unknown format: {format_name}")
return plugin_cls().process(data)
- Called automatically on subclass creation
- Receives keyword arguments from class definition
- Perfect for plugin systems, registries
__init_subclass__registryplugin
๐๏ธ Design Patterns with Python
๐ญ Factory Pattern creational
Abstract factory โข factory method โข dependency injection
from abc import ABC, abstractmethod
class Button(ABC):
@abstractmethod
def render(self): pass
@abstractmethod
def on_click(self): pass
class WindowsButton(Button):
def render(self):
return "Windows style button"
def on_click(self):
return "Windows click sound"
class MacButton(Button):
def render(self):
return "Mac style button"
def on_click(self):
return "Mac click sound"
class Checkbox(ABC):
@abstractmethod
def render(self): pass
class WindowsCheckbox(Checkbox):
def render(self):
return "Windows style checkbox"
class MacCheckbox(Checkbox):
def render(self):
return "Mac style checkbox"
class GUIFactory(ABC):
@abstractmethod
def create_button(self) -> Button: pass
@abstractmethod
def create_checkbox(self) -> Checkbox: pass
class WindowsFactory(GUIFactory):
def create_button(self):
return WindowsButton()
def create_checkbox(self):
return WindowsCheckbox()
class MacFactory(GUIFactory):
def create_button(self):
return MacButton()
def create_checkbox(self):
return MacCheckbox()
# Factory function (simpler approach)
def get_factory(os_type):
factories = {
'windows': WindowsFactory,
'mac': MacFactory
}
return factories.get(os_type, WindowsFactory)()
# Usage
factory = get_factory('mac')
button = factory.create_button()
checkbox = factory.create_checkbox()
print(button.render()) # Mac style button
- Encapsulates object creation logic
- Lets subclasses decide which class to instantiate
- Promotes loose coupling
factorycreationalDI
๐ฆ Builder Pattern complex construction
Step-by-step โข fluent interface โข immutable result
class Computer:
def __init__(self):
self.cpu = None
self.ram = None
self.storage = None
self.gpu = None
self.os = None
def __repr__(self):
parts = []
if self.cpu: parts.append(f"CPU:{self.cpu}")
if self.ram: parts.append(f"RAM:{self.ram}GB")
if self.storage: parts.append(f"Storage:{self.storage}")
if self.gpu: parts.append(f"GPU:{self.gpu}")
if self.os: parts.append(f"OS:{self.os}")
return "Computer(" + ", ".join(parts) + ")"
class ComputerBuilder:
def __init__(self):
self.computer = Computer()
def add_cpu(self, cpu):
self.computer.cpu = cpu
return self
def add_ram(self, ram_gb):
self.computer.ram = ram_gb
return self
def add_storage(self, storage):
self.computer.storage = storage
return self
def add_gpu(self, gpu):
self.computer.gpu = gpu
return self
def install_os(self, os):
self.computer.os = os
return self
def build(self):
return self.computer
# Usage with method chaining
gaming_pc = (ComputerBuilder()
.add_cpu("Intel i9-13900K")
.add_ram(64)
.add_storage("2TB NVMe SSD")
.add_gpu("NVIDIA RTX 4090")
.install_os("Windows 11")
.build())
print(gaming_pc)
# Computer(CPU:Intel i9-13900K, RAM:64GB, Storage:2TB NVMe SSD, GPU:NVIDIA RTX 4090, OS:Windows 11)
workstation = (ComputerBuilder()
.add_cpu("AMD Threadripper")
.add_ram(128)
.add_storage("4TB NVMe + 8TB HDD")
.install_os("Ubuntu 22.04")
.build())
- Each method returns self for chaining
- Build method returns final product
- Separates construction from representation
fluentbuilderchaining
๐ฏ Strategy Pattern behavioral
Encapsulate algorithms โข runtime selection โข dependency injection
from typing import List, Callable
import math
# Strategy as interface
class SortingStrategy:
def sort(self, data: List[int]) -> List[int]:
raise NotImplementedError
class BubbleSort(SortingStrategy):
def sort(self, data):
arr = data[:]
n = len(arr)
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
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 PythonSort(SortingStrategy):
def sort(self, data):
return sorted(data)
# Strategy as function (Pythonic approach)
def bubble_sort(data):
arr = data[:]
n = len(arr)
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
class Sorter:
def __init__(self, strategy: SortingStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortingStrategy):
self._strategy = strategy
def sort(self, data):
return self._strategy.sort(data)
def benchmark(self, data):
import time
start = time.time()
result = self.sort(data)
elapsed = time.time() - start
return result, elapsed
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(PythonSort())
print(sorter.sort(data))
sorter.set_strategy(BubbleSort())
result, time_taken = sorter.benchmark(data)
print(f"Bubble sort took {time_taken:.6f}s")
- Strategy injected via constructor or setter
- Algorithms interchangeable at runtime
- Functions can replace strategy classes in Python
strategyDIopen/closed
๐ Observer Pattern pub-sub
Event handling โข loose coupling โข notifications
from abc import ABC, abstractmethod
from typing import List, Any
class Observer(ABC):
@abstractmethod
def update(self, subject: 'Subject', event: str, data: Any):
pass
class Subject:
def __init__(self):
self._observers: List[Observer] = []
self._state = {}
def attach(self, observer: Observer):
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer: Observer):
self._observers.remove(observer)
def notify(self, event: str, data: Any = None):
for observer in self._observers:
observer.update(self, event, data)
@property
def state(self):
return self._state.copy()
def update_state(self, key, value):
self._state[key] = value
self.notify("state_changed", {key: value})
class Logger(Observer):
def update(self, subject, event, data):
print(f"[Logger] Event: {event}, Data: {data}")
class EmailNotifier(Observer):
def __init__(self, email):
self.email = email
def update(self, subject, event, data):
if event == "state_changed":
print(f"Sending email to {self.email}: State changed to {data}")
class Analytics(Observer):
def __init__(self):
self.events = []
def update(self, subject, event, data):
self.events.append((event, data))
print(f"[Analytics] Total events: {len(self.events)}")
def report(self):
from collections import Counter
event_counts = Counter(e[0] for e in self.events)
return dict(event_counts)
# Usage
subject = Subject()
logger = Logger()
notifier = EmailNotifier("admin@example.com")
analytics = Analytics()
subject.attach(logger)
subject.attach(notifier)
subject.attach(analytics)
subject.update_state("temperature", 22)
subject.update_state("humidity", 65)
subject.detach(notifier)
subject.update_state("pressure", 1013)
print(analytics.report()) # {'state_changed': 3}
- Subject maintains observer list
- Observers notified of state changes
- Loose coupling between components
observerpub-subevents
๐ฌ Modern Python OOP (3.10-3.12)
๐ Protocol (Structural Typing)
PEP 544 โข static duck typing โข runtime_checkable
from typing import Protocol, runtime_checkable, List
from dataclasses import dataclass
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> str: ...
@property
def area(self) -> float: ...
@runtime_checkable
class Comparable(Protocol):
def __lt__(self, other) -> bool: ...
@dataclass
class Circle:
radius: float
def draw(self) -> str:
return "โ"
@property
def area(self) -> float:
return 3.14159 * self.radius ** 2
@dataclass
class Square:
side: float
def draw(self) -> str:
return "โก"
@property
def area(self) -> float:
return self.side ** 2
class TextDrawable:
"""No inheritance, just implements required methods"""
def __init__(self, text):
self.text = text
self.area = len(text) # duck typing - area property
def draw(self):
return f"'{self.text}'"
def render_all(shapes: List[Drawable]):
for shape in shapes:
print(f"{shape.draw()} (area: {shape.area:.1f})")
shapes = [
Circle(5),
Square(4),
TextDrawable("Hello")
]
render_all(shapes) # Works for any Drawable-compatible object
# Runtime check
print(isinstance(Circle(3), Drawable)) # True (with runtime_checkable)
print(isinstance(TextDrawable("test"), Drawable)) # True
# Generic Protocol
class SupportsSum(Protocol):
def __add__(self, other): ...
def double[T: SupportsSum](x: T) -> T:
return x + x
print(double(5)) # 10
print(double(3.14)) # 6.28
print(double("abc")) # abcabc
- Structural typing - no inheritance needed
@runtime_checkable enables isinstance()
- Perfect for adapting external code
Protocolstructuraltype hints
โ๏ธ Frozen Dataclasses & NamedTuple
Immutable data โข hashing โข memory efficiency
from dataclasses import dataclass, field
from typing import NamedTuple
from enum import Enum
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
@dataclass(frozen=True, slots=True) # Python 3.10+ slots with dataclass
class Point3D:
"""Immutable 3D point with automatic hashing"""
x: float
y: float
z: float = 0.0
def __post_init__(self):
# Can't modify frozen instance in __post_init__
# Use object.__setattr__ if needed
pass
def distance_to(self, other: 'Point3D') -> float:
return ((self.x-other.x)**2 + (self.y-other.y)**2 + (self.z-other.z)**2) ** 0.5
def as_tuple(self):
return (self.x, self.y, self.z)
@dataclass(frozen=True)
class ColorPoint:
point: Point3D
color: Color
name: str = field(compare=False) # exclude from equality
def __hash__(self):
return hash((self.point, self.color))
class City(NamedTuple):
"""NamedTuple - even lighter than dataclass"""
name: str
population: int
country: str
coordinates: tuple[float, float]
@property
def population_density(self):
# Can add properties to NamedTuple
area = 100 # dummy area
return self.population / area
# Usage
p1 = Point3D(1, 2, 3)
p2 = Point3D(1, 2, 3)
print(p1 == p2) # True (automatic __eq__)
print(hash(p1)) # Works (frozen=True enables __hash__)
cp = ColorPoint(p1, Color.RED, "start")
points = {cp} # Can be used in sets/dict keys
city = City("Tokyo", 37400068, "Japan", (35.6762, 139.6503))
print(city.name) # Tokyo
print(city[0]) # Tokyo (tuple indexing works)
frozen=True creates immutable objects
slots=True (3.10+) reduces memory
- NamedTuple for simple, lightweight data
frozenimmutableNamedTuple
๐ TypeVar & Generics type-safe containers
PEP 695 (3.12) โข TypeVar โข Generic types
from typing import TypeVar, Generic, Iterator, Optional
from collections.abc import Sequence
# Python 3.11 and earlier
T = TypeVar('T')
K = TypeVar('K')
V = TypeVar('V')
class Stack(Generic[T]):
"""Generic stack with type safety"""
def __init__(self, initial: Optional[list[T]] = None):
self._items: list[T] = initial or []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if not self._items:
raise IndexError("pop from empty stack")
return self._items.pop()
def peek(self) -> T:
if not self._items:
raise IndexError("peek from empty stack")
return self._items[-1]
def __len__(self) -> int:
return len(self._items)
def __iter__(self) -> Iterator[T]:
return reversed(self._items)
# Python 3.12+ simplified syntax
class Pair[T, U]:
def __init__(self, first: T, second: U):
self.first = first
self.second = second
def __repr__(self):
return f"Pair({self.first!r}, {self.second!r})"
def swap(self) -> 'Pair[U, T]':
return Pair(self.second, self.first)
# Usage
stack_int: Stack[int] = Stack()
stack_int.push(1)
stack_int.push(2)
print(stack_int.pop()) # 2
stack_str = Stack[str]()
stack_str.push("hello")
# stack_str.push(1) # Type checker would error
pair = Pair(1, "one")
print(pair) # Pair(1, 'one')
swapped = pair.swap()
print(swapped) # Pair('one', 1)
# Multiple type variables
class Dictionary[K, V]:
def __init__(self):
self._data: dict[K, V] = {}
def set(self, key: K, value: V) -> None:
self._data[key] = value
def get(self, key: K) -> Optional[V]:
return self._data.get(key)
d = Dictionary[str, int]()
d.set("age", 30)
print(d.get("age")) # 30
- Type-safe containers with generics
- Python 3.12 simplified generic syntax
- Better IDE support and type checking
GenericTypeVartype hints
๐งฉ Match Statement with Classes
Python 3.10+ โข structural pattern matching
from dataclasses import dataclass
from typing import Any
import math
@dataclass
class Point:
x: float
y: float
@dataclass
class Circle:
center: Point
radius: float
@dataclass
class Rectangle:
top_left: Point
bottom_right: Point
@dataclass
class Line:
start: Point
end: Point
def describe_shape(shape: Any) -> str:
match shape:
case Point(x, y):
return f"Point at ({x}, {y})"
case Circle(Point(x, y), r) if r > 0:
area = math.pi * r * r
return f"Circle at ({x},{y}) radius={r}, area={area:.1f}"
case Circle(_, r):
return f"Invalid circle with radius {r}"
case Rectangle(Point(x1, y1), Point(x2, y2)):
width = abs(x2 - x1)
height = abs(y2 - y1)
return f"Rectangle {width}ร{height}, area={width*height}"
case Line(Point(x1, y1), Point(x2, y2)):
length = math.hypot(x2-x1, y2-y1)
return f"Line from ({x1},{y1}) to ({x2},{y2}), length={length:.1f}"
case _:
return f"Unknown shape: {shape}"
shapes = [
Point(3, 4),
Circle(Point(0, 0), 5),
Rectangle(Point(1, 1), Point(4, 5)),
Line(Point(0, 0), Point(3, 4))
]
for shape in shapes:
print(describe_shape(shape))
# Match with guards and conditions
def process_command(cmd):
match cmd.split():
case ["quit"]:
return "Goodbye!"
case ["load", filename] if filename.endswith(('.txt', '.md')):
return f"Loading text file: {filename}"
case ["load", filename]:
return f"Unknown file type: {filename}"
case ["save", filename, *rest] if rest:
return f"Save with extra args: {rest}"
case ["save", filename]:
return f"Saving to {filename}"
case _:
return "Unknown command"
- Pattern matching with class structures
- Guards (
if) for additional conditions
- Clean alternative to if-elif chains
matchpattern matching3.10+
โก Quick Reference
๐ Dunder Methods
__init__(self, ...) โ constructor
__new__(cls, ...) โ instance creator
__del__(self) โ destructor
__repr__(self) โ debug representation
__str__(self) โ user-friendly string
__format__(self, spec) โ formatting
__bytes__(self) โ bytes conversion
__hash__(self) โ hash for dict keys
__bool__(self) โ truthiness
__len__(self) โ container length
__getitem__(self, key) โ indexing
__setitem__(self, key, val) โ assignment
__delitem__(self, key) โ deletion
__iter__(self) โ iterator
__next__(self) โ next value
__contains__(self, item) โ membership
__call__(self, ...) โ callable object
__enter__ / __exit__ โ context manager
__add__, __sub__, __mul__ โ arithmetic
__eq__, __ne__, __lt__, __gt__ โ comparisons
__getattr__, __setattr__ โ dynamic attrs
__getattribute__ โ all attribute access
__dir__(self) โ dir() listing
__slots__ โ memory optimization
๐งช SOLID Principles
- Single Responsibility: One class, one job
- Open/Closed: Open for extension, closed for modification
- Liskov Substitution: Subtypes must be substitutable for base types
- Interface Segregation: Many specific interfaces better than one general
- Dependency Inversion: Depend on abstractions, not concretions
SRP
OCP
LSP
ISP
DIP
๐ Design Principles
- DRY โ Don't Repeat Yourself
- KISS โ Keep It Simple, Stupid
- YAGNI โ You Ain't Gonna Need It
- Composition over Inheritance
- Program to an Interface
- Tell, Don't Ask
๐ง Useful Built-ins
isinstance(obj, Class) โ type check
issubclass(Derived, Base) โ subclass check
hasattr(obj, 'attr') โ attribute exists
getattr(obj, 'attr', default) โ safe get
setattr(obj, 'attr', val) โ set attribute
delattr(obj, 'attr') โ delete attribute
dir(obj) โ list attributes
vars(obj) โ instance __dict__
type(obj) โ get class
id(obj) โ unique identifier
callable(obj) โ check if callable
super() โ proxy to parent
@property โ getter/setter
@classmethod โ class methods
@staticmethod โ static methods
@functools.total_ordering โ fill comparisons
๐งฐ Standard Library
dataclasses.dataclass
abc.ABC, @abstractmethod
typing.Protocol
enum.Enum
functools.wraps
๐ Best Practices
- โ Use dataclasses for data containers
- โ Prefer composition over inheritance
- โ Use @property for computed attributes
- โ Keep classes focused (SRP)
- โ Use __slots__ for millions of objects
- โ Write type hints for better IDE support
โ ๏ธ Common Pitfalls
- โ Mutable default arguments
- โ Forgetting super() in inheritance
- โ Deep inheritance hierarchies
- โ Overusing __getattribute__
- โ Not using @wraps in decorators
๐ Recommended Resources
- ๐ Fluent Python โ Luciano Ramalho
- ๐ Python Cookbook โ David Beazley
- ๐ Design Patterns โ Gamma et al.
- ๐ docs.python.org/3/tutorial/classes
- ๐ mypy.readthedocs.io
๐ Pro Tips:
- โข
__new__ runs before __init__ โ use for immutable types
- โข
super() follows MRO โ essential for multiple inheritance
- โข Use
@functools.total_ordering to fill missing comparisons
- โข
__slots__ can reduce memory usage by 40-60%
- โข Dataclasses +
frozen=True = immutable, hashable objects
- โข Protocols enable structural typing without inheritance