Back to Python tutorials
Intermediate18 min read

Object-Oriented Programming

Model domains with classes, inheritance, polymorphism, and Python special methods.

Classes and Objects

Classes define attributes and methods. __init__ initializes instance state. self refers to the current instance.

Instance attributes live on objects; class attributes are shared. Use @classmethod and @staticmethod for alternate constructors and utility functions tied to the class namespace.

Dataclasses reduce boilerplate for data containers with auto-generated __init__, __repr__, and equality.

  • Prefer composition over deep inheritance hierarchies
  • Use @property for computed attributes with validation
  • Keep __init__ lightweight; defer heavy work to factories
@dataclass
class Product:
    id: int
    name: str
    price: float

    def with_tax(self, rate: float) -> float:
        return self.price * (1 + rate)

Inheritance and super

Subclass with class Child(Parent): and override methods. super() calls parent implementations—critical in __init__ chains and cooperative multiple inheritance.

Abstract base classes in abc module enforce interfaces. Raise NotImplementedError in base methods when ABC is overkill.

Favor duck typing: if it quacks like a file object, accept it without isinstance checks when behavior is the contract.

  • Document extension points for subclasses
  • Avoid calling super().__init__ twice accidentally
  • Use Protocol for structural typing in modern codebases
class AdminUser(User):
    def permissions(self):
        base = super().permissions()
        return base | {"admin": True}

Dunder Methods

Special methods customize behavior: __str__ for readable prints, __repr__ for developers, __eq__ for equality, __len__ for len().

Context managers implement __enter__ and __exit__ for with blocks. Iterators use __iter__ and __next__.

Implement only dunder methods that match your object semantics—do not add __add__ unless addition is meaningful.

  • __repr__ should ideally be unambiguous for debugging
  • Use @functools.total_ordering when implementing rich comparisons
  • Context managers guarantee cleanup in __exit__ even on exceptions
def __repr__(self):
    return f"Order(id={self.id!r}, total={self.total!r})"

Get In Touch


Ready to discuss your next project? Drop me a message.