Skip to content

Python OOP

Object-oriented Python for modeling data and systems: classes, objects, methods, inheritance, encapsulation, abstract classes, dataclasses, properties, and magic methods.

Table of Contents

Installation / Setup

OOP is built into Python — no install needed. Run with Python 3.10+ for the newest syntax (dataclass(slots=True), etc.).

python -c "from dataclasses import dataclass; print('ok')"

Core Syntax

Classes and objects

class Model:
    def __init__(self, name: str, lr: float = 0.01):
        self.name = name          # instance attribute
        self.lr = lr
        self.trained = False

m = Model("linear", lr=0.1)       # __init__ runs; m is the object
m.trained = True                  # attributes are dynamic by default

Instance, class, and static methods

Method type Decorator First arg Use for
Instance none self Work with object state
Class @classmethod cls Factory constructors, class-level state
Static @staticmethod — Utility tied to the class
class Dataset:
    total = 0                     # class attribute (shared)

    def __init__(self, rows):
        self.rows = rows
        Dataset.total += len(rows)

    def shape(self):              # instance method
        return (len(self.rows), len(self.rows[0]))

    @classmethod
    def from_csv(cls, path):      # factory
        import csv
        with open(path) as f:
            return cls(list(csv.reader(f)))

    @staticmethod
    def is_valid(rows):           # utility
        return bool(rows) and all(len(r) == len(rows[0]) for r in rows)

Inheritance

class BaseModel:
    def __init__(self, name):
        self.name = name
    def predict(self, X):          # overridden below
        raise NotImplementedError

class LinearModel(BaseModel):
    def __init__(self, name="linear", lr=0.01):
        super().__init__(name)     # call parent __init__
        self.lr = lr
    def predict(self, X):
        return X @ self.weights    # polymorphism: same interface, new behavior

issubclass(LinearModel, BaseModel)  # True
  • super() resolves the next class in the MRO (ClassName.__mro__).
  • Multiple inheritance works via C3 linearization — keep it shallow.

Polymorphism

Duck typing: an object's behavior matters, not its declared type.

def summarize(x):
    return x.mean()               # works for any object with .mean()

import numpy as np
import pandas as pd
summarize(np.array([1, 2, 3]))    # numpy array
summarize(pd.Series([1, 2, 3]))   # pandas Series

Encapsulation

class Account:
    def __init__(self):
        self._internal = 1        # convention: private by convention
        self.__mangled = 2        # name-mangled to _Account__mangled

    def get_balance(self):        # public API
        return self._internal

_x = "please don't touch" (convention). __x = name-mangled to deter accidental access. Real encapsulation comes from properties.

Abstract classes

from abc import ABC, abstractmethod

class Evaluator(ABC):
    @abstractmethod
    def score(self, y_true, y_pred):
        """Must be implemented by subclasses."""

class Accuracy(Evaluator):
    def score(self, y_true, y_pred):
        return sum(a == b for a, b in zip(y_true, y_pred)) / len(y_true)

# Evaluator() -> TypeError: abstract class cannot be instantiated

Dataclasses

from dataclasses import dataclass, field, asdict

@dataclass
class Config:
    lr: float = 0.01
    epochs: int = 10
    layers: list[int] = field(default_factory=list)   # no mutable default!
    name: str = field(default="model", repr=False)

cfg = Config(epochs=20)
cfg.lr                       # 0.01
asdict(cfg)                  # {'lr': 0.01, 'epochs': 20, 'layers': [], 'name': 'model'}

@dataclass auto-generates __init__, __repr__, __eq__. Use frozen=True for immutability, order=True for comparison operators, slots=True (3.10+) for memory efficiency.

Properties

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

    @property
    def celsius(self):
        return self._celsius

    @property
    def fahrenheit(self):            # computed, read-only
        return self._celsius * 9 / 5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value):     # validated setter
        if value < -459.67:
            raise ValueError("below absolute zero")
        self._celsius = (value - 32) * 5 / 9

t = Temperature(25)
t.fahrenheit          # 77.0
t.fahrenheit = 212    # setter runs, validation applies

Magic methods

Method Called when Purpose
__init__(self, ...) Class(...) Initialize instance
__repr__ / __str__ repr(x) / str(x) Developer / user-facing string
__len__ len(x) Size
__getitem__ / __setitem__ x[i], x[i] = v Subscription
__iter__ / __next__ iter(x), for i in x Iteration
__eq__ / __lt__ / __gt__ ==, <, > Comparison
__hash__ hash(x) Dict/set membership
__call__ x(...) Make object callable
__enter__ / __exit__ with x: Context manager
__add__ / __mul__ x + y, x * y Operators
__bool__ bool(x), if x Truthiness
class SparseVector:
    def __init__(self, data): self.data = dict(data)
    def __getitem__(self, i): return self.data.get(i, 0.0)
    def __len__(self): return max(self.data, default=-1) + 1
    def __repr__(self): return f"SparseVector({self.data})"

Practical Examples

Model wrapper with dataclass + property

from dataclasses import dataclass
import numpy as np

@dataclass
class LinearRegression:
    lr: float = 0.01
    epochs: int = 100
    weights: np.ndarray | None = None

    def fit(self, X, y):
        X = np.c_[np.ones(len(X)), X]          # bias column
        self.weights = np.linalg.pinv(X.T @ X) @ X.T @ y
        return self

    def predict(self, X):
        X = np.c_[np.ones(len(X)), X]
        return X @ self.weights

model = LinearRegression(epochs=500).fit(X, y)

Callable metric

class Accuracy:
    def __call__(self, y_true, y_pred):
        return float(sum(a == b for a, b in zip(y_true, y_pred))) / len(y_true)

acc = Accuracy()
acc(y_true, y_pred)     # objects that behave like functions

Common Patterns

# Factory via classmethod (also see dataclass + classmethod)
@dataclass
class Dataset:
    path: str
    data: list = field(default_factory=list)

    @classmethod
    def load(cls, path):
        return cls(path, data=read_file(path))

# Composition over inheritance: give objects collaborators
class Pipeline:
    def __init__(self, steps: list):
        self.steps = steps
    def run(self, x):
        for s in self.steps:
            x = s(x)
        return x

# __slots__ to cut memory for millions of small objects
class Point:
    __slots__ = ("x", "y")
    def __init__(self, x, y): self.x, self.y = x, y

Best Practices

  • Prefer @dataclass for data containers — less boilerplate than hand-written __init__/__repr__.
  • Use super().__init__(...) in subclasses — don't re-implement parent setup.
  • Keep inheritance shallow (1–2 levels); prefer composition.
  • Use @property instead of bare get_/set_ methods.
  • Use abstract base classes to define interfaces for pluggable components (models, metrics, loaders).
  • Design for duck typing — accept objects with the needed methods, matching how pandas/NumPy interop.
  • Never use mutable default arguments, including in dataclasses (field(default_factory=...)).

Common Mistakes

Mistake Consequence Correct approach
def __init__(self, layers=[]) Shared list across instances layers: list = field(default_factory=list)
Defining __eq__ without __hash__ Objects unhashable Set __hash__ = None intentionally, or use frozen dataclass
Calling super().__init__ with wrong args Attribute errors in parent Match the parent signature
Deep inheritance chains Fragile, hard to debug Composition / mixins
Mutable class attributes State shared by all instances Use instance attributes in __init__

Troubleshooting

Symptom Cause Fix
TypeError: Can't instantiate abstract class Missing @abstractmethod implementations Implement all abstract methods
AttributeError: 'X' object has no attribute 'y' Attribute set on instance only / typo Set it in __init__; check spelling
TypeError: 'Model' object is not callable Called object without __call__ Add __call__ or call a real method
TypeError: unhashable type: 'list' List used in dict/set Convert to tuple
Dataclass __init__ ignores new fields Mutable default without default_factory Use field(default_factory=...)

Official Documentation

Last Updated

2026-09-06 — Initial version.