Skip to content

OOP Advanced

★★★★★ Advanced

Advanced OOP patterns in Python include Abstract Base Classes for defining interfaces, descriptors for attribute access control, metaclasses for class creation customization, dataclasses for boilerplate reduction, and __slots__ for memory optimization.

Key Facts

  • ABCs enforce interface contracts - cannot instantiate without implementing all abstract methods
  • Descriptors are objects with __get__/__set__/__delete__ - they control attribute access when placed as class attributes
  • Data descriptors (__set__) take priority over instance __dict__; non-data descriptors don't
  • __slots__ restricts attributes and saves 30-40% memory per instance (no __dict__)
  • @dataclass auto-generates __init__, __repr__, __eq__ from type-annotated fields
  • __init_subclass__ (Python 3.6+) is a simpler alternative to metaclasses for most use cases

Patterns

Abstract Base Classes

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

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

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

    def perimeter(self):
        return 2 * 3.14159 * self.radius

# Shape()  -> TypeError: Can't instantiate abstract class

Descriptors

class PositiveNumber:
    def __set_name__(self, owner, name):
        self.name = name
        self.storage_name = f'_{name}'

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.storage_name, None)

    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError(f"{self.name} must be positive")
        setattr(obj, self.storage_name, value)

class Rectangle:
    width = PositiveNumber()   # descriptor as class attribute
    height = PositiveNumber()

    def __init__(self, width, height):
        self.width = width     # triggers __set__
        self.height = height

Lookup priority: data descriptor > instance __dict__ > non-data descriptor > class __dict__.

Metaclasses

class ShapeMeta(type):
    registry = set()

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if name != 'Shape':
            mcs.registry.add(cls)
        return cls

class Shape(metaclass=ShapeMeta):
    pass

class Circle(Shape): pass
class Rect(Shape): pass
# ShapeMeta.registry == {Circle, Rect}

init_subclass (Simpler Alternative)

class Shape:
    registry = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Shape.registry.append(cls)

class Circle(Shape): pass
# Shape.registry == [Circle]

new vs init

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        self.value = 42
  • __new__ creates the instance (called before __init__)
  • __init__ initializes the instance (called after __new__)

slots

class Point:
    __slots__ = ('x', 'y')

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)
p.z = 3  # AttributeError

Benefits: 30-40% less memory, slightly faster access. Trade-off: no dynamic attributes, no __dict__.

Dataclasses (Python 3.7+)

from dataclasses import dataclass, field

@dataclass
class Point:
    x: float
    y: float
    label: str = "origin"
    tags: list = field(default_factory=list)  # mutable default

    def distance(self):
        return (self.x**2 + self.y**2) ** 0.5

p = Point(3.0, 4.0)
print(p)  # Point(x=3.0, y=4.0, label='origin', tags=[])

Options: @dataclass(frozen=True) for immutable, @dataclass(order=True) for comparison operators.

Combining Patterns

from abc import ABCMeta, abstractmethod

class ValidatedMeta(ABCMeta):
    registry = {}

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if not getattr(cls, '__abstractmethods__', None):
            mcs.registry[name] = cls
        return cls

class Shape(metaclass=ValidatedMeta):
    @property
    @abstractmethod
    def area(self): ...

Gotchas

  • property, classmethod, staticmethod are all descriptors under the hood
  • __slots__ does not work with multiple inheritance if both parents define __slots__
  • Metaclasses are rarely needed - prefer __init_subclass__ or class decorators
  • @dataclass with mutable default (list, dict) requires field(default_factory=list)
  • Abstract methods can have implementations (subclass can call super().method())

See Also