Codeskill

Learn to code, step by step

OOP patterns you’ll use

A look at object-oriented patterns that show up in everyday Python, not abstract factory diagrams. The intro tutorials introduced classes; here we focus on structures that keep code readable.

Dataclasses for plain data

If a class mostly holds data, use @dataclass instead of writing __init__, __repr__, and equality by hand.

from dataclasses import dataclass

@dataclass
class Contact:
    name: str
    email: str
    active: bool = True

alice = Contact("Alice", "alice@example.com")
print(alice)  # Contact(name='Alice', email='alice@example.com', active=True)

Composition over deep inheritance

Python supports inheritance, but long chains of base classes get hard to follow. Prefer building behaviour from smaller objects.

class Mailer:
    def send(self, to: str, subject: str, body: str) -> None:
        print(f"Sending to {to}: {subject}")

class UserNotifier:
    def __init__(self, mailer: Mailer) -> None:
        self.mailer = mailer

    def welcome(self, email: str) -> None:
        self.mailer.send(email, "Welcome", "Thanks for signing up.")

You can swap Mailer for a test double in unit tests without subclassing half your codebase.

Properties for lightweight validation

Use @property when an attribute needs a guard or computed value, not for every field ‘because objects’.

class Account:
    def __init__(self, balance: float) -> None:
        self.balance = balance

    @property
    def balance(self) -> float:
        return self._balance

    @balance.setter
    def balance(self, value: float) -> None:
        if value < 0:
            raise ValueError("balance cannot be negative")
        self._balance = value

Dunder methods worth knowing

You do not need to implement every magic method. These three appear often:

  • __str__ – human-readable string for logging and print
  • __repr__ – unambiguous debug representation
  • __len__ – when your object wraps a collection
class TagList:
    def __init__(self, tags: list[str]) -> None:
        self._tags = tags

    def __len__(self) -> int:
        return len(self._tags)

    def __repr__(self) -> str:
        return f"TagList({self._tags!r}"

Protocols and duck typing

Python cares what an object can do, not its class name. If it has a write() method, it can act like a file-like object.

def save_report(output, rows: list[dict]) -> None:
    for row in rows:
        output.write(f"{row['id']},{row['name']}n")

# works with a real file or io.StringIO in tests

When not to use a class

Functions and modules are underrated. If you have no state to manage, a function is simpler than a class with one method. Save classes for data plus behaviour that genuinely belong together.

Next: type hints that help your editor and teammates without turning Python into another language.

PreviousVirtualenvs and pinning