PY

Classes & OOP

Python · 9 entries

Defining a Class

syntax
class Name:
    def __init__(self, ...):
        self.attr = value
example
class BankAccount:
    def __init__(self, owner: str, balance: float = 0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount: float) -> None:
        self.balance += amount

acct = BankAccount("Alice", 100)
acct.deposit(50)
print(f"{acct.owner}: ${acct.balance}")
output
Alice: $150

Note self is not a keyword; it is a convention for the first parameter of instance methods. Python passes the instance automatically.

Inheritance & super()

syntax
class Child(Parent):
    def __init__(self):
        super().__init__()
example
class Vehicle:
    def __init__(self, make: str, year: int):
        self.make = make
        self.year = year

class ElectricCar(Vehicle):
    def __init__(self, make: str, year: int, range_km: int):
        super().__init__(make, year)
        self.range_km = range_km

    def __repr__(self) -> str:
        return f"{self.year} {self.make} ({self.range_km}km range)"

car = ElectricCar("Tesla", 2026, 600)
print(car)
output
2026 Tesla (600km range)

Note Always call super().__init__() in the child to ensure the parent is properly initialized. Python supports multiple inheritance via MRO (Method Resolution Order).

@property (Getters & Setters)

syntax
@property
def attr(self): ...
@attr.setter
def attr(self, value): ...
example
class Temperature:
    def __init__(self, celsius: float):
        self._celsius = celsius

    @property
    def fahrenheit(self) -> float:
        return self._celsius * 9 / 5 + 32

    @property
    def celsius(self) -> float:
        return self._celsius

    @celsius.setter
    def celsius(self, value: float) -> None:
        if value < -273.15:
            raise ValueError("Below absolute zero")
        self._celsius = value

t = Temperature(100)
print(t.fahrenheit)
t.celsius = 0
print(t.fahrenheit)
output
212.0
32.0

Note @property lets you expose computed values as attributes and add validation to setters without changing the caller's syntax.

@classmethod & @staticmethod

syntax
@classmethod
def method(cls, ...): ...
@staticmethod
def method(...): ...
example
class DateRecord:
    def __init__(self, year: int, month: int, day: int):
        self.year = year
        self.month = month
        self.day = day

    @classmethod
    def from_string(cls, date_str: str) -> "DateRecord":
        y, m, d = map(int, date_str.split("-"))
        return cls(y, m, d)

    @staticmethod
    def is_valid_year(year: int) -> bool:
        return 1 <= year <= 9999

record = DateRecord.from_string("2026-04-04")
print(record.year, DateRecord.is_valid_year(2026))
output
2026 True

Note @classmethod receives the class as first arg (cls) and is commonly used for alternative constructors. @staticmethod receives neither self nor cls.

Dataclasses

syntax
from dataclasses import dataclass
@dataclass
class Name:
    field: type
example
from dataclasses import dataclass, field

@dataclass
class Product:
    name: str
    price: float
    tags: list[str] = field(default_factory=list)

    @property
    def display_price(self) -> str:
        return f"${self.price:.2f}"

p = Product("Widget", 9.99, ["sale"])
print(p)
print(p.display_price)
output
Product(name='Widget', price=9.99, tags=['sale'])
$9.99

Note Dataclasses auto-generate __init__, __repr__, and __eq__. Use field(default_factory=list) for mutable defaults, never a bare [] as a default.

__slots__ (Memory Optimization)

syntax
class Name:
    __slots__ = ('attr1', 'attr2')
example
@dataclass(slots=True)
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
print(p)
# p.z = 3.0  # AttributeError: no __dict__
output
Point(x=1.0, y=2.0)

Note __slots__ prevents dynamic attribute creation and reduces memory by eliminating the per-instance __dict__. Python 3.10+ dataclasses support slots=True directly.

Abstract Base Classes

syntax
from abc import ABC, abstractmethod
class Base(ABC):
    @abstractmethod
    def method(self): ...
example
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

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

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

# shape = Shape()  # TypeError: can't instantiate abstract class
circle = Circle(5)
print(f"Area: {circle.area():.2f}")
output
Area: 78.54

Note Abstract classes cannot be instantiated directly. Subclasses must implement all @abstractmethod methods or they will also be abstract.

Protocols (Structural Typing)

syntax
from typing import Protocol
class Drawable(Protocol):
    def draw(self) -> None: ...
example
from typing import Protocol

class Saveable(Protocol):
    def save(self, path: str) -> None: ...

class Document:
    def save(self, path: str) -> None:
        print(f"Saved to {path}")

def backup(item: Saveable, dest: str) -> None:
    item.save(dest)

backup(Document(), "/tmp/doc.txt")
output
Saved to /tmp/doc.txt

Note Protocols enable duck-typing with static type checking. Classes do not need to explicitly inherit from the Protocol; they just need matching methods.

Common Dunder Methods

syntax
__str__, __repr__, __len__, __eq__, __lt__, __hash__
example
class Money:
    def __init__(self, amount: float, currency: str = "USD"):
        self.amount = amount
        self.currency = currency

    def __repr__(self) -> str:
        return f"Money({self.amount}, '{self.currency}')"

    def __str__(self) -> str:
        return f"{self.currency} {self.amount:.2f}"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Money):
            return NotImplemented
        return self.amount == other.amount and self.currency == other.currency

print(Money(42.50))
print(repr(Money(42.50)))
output
USD 42.50
Money(42.5, 'USD')

Note __repr__ should produce an unambiguous developer-facing string. __str__ is the user-facing version. Return NotImplemented (not raise) from __eq__ for unknown types.