✦ For everyone, free.

Practical knowledge for real and everyday life

Home

Object-Oriented Programming in Python

Object-Oriented Programming in Python organizes code through classes and objects, enabling structured and reusable software development.

Object-oriented programming in Python is the design and implementation of software through objects that combine state and behavior, classes that define reusable object models, and relationships such as inheritance, polymorphism, abstraction, composition, and delegation.


Foundations of Object-Oriented Programming in Python

Objects are entities with identity, state, and behavior. Each object has a unique identity distinguishing it from other objects, maintains state through attributes, and exhibits behavior through methods. Classes are reusable definitions that serve as blueprints for creating and organizing related instances and their associated methods.

Object-oriented design emphasizes coherent responsibilities assigned to objects, collaboration among objects through well-defined interfaces, ownership of state by individual objects, and relationships defined among abstractions rather than simplistic usage of class syntax alone. It involves thoughtful structuring of code where objects encapsulate meaningful state and behavior, and interact without exposing unnecessary implementation details.

Key concepts include:

  • Classes: Templates defining structure and behavior for objects.
  • Instances: Individual objects created from classes.
  • Instance state: Data stored uniquely per instance.
  • Class state: Data shared among all instances of a class.
  • Methods: Functions bound to classes or instances to implement behavior.
  • Inheritance: Mechanism for classes to derive from others, reusing and specializing behavior.
  • Polymorphism: Ability to use objects of different classes interchangeably based on shared behavior.
  • Composition: Building objects by combining other objects, expressing "has-a" relationships.
  • Delegation: Forwarding responsibility for behavior from one object to another.
ConceptPrincipal Responsibility
ClassDefine object blueprint, structure, and shared behavior
InstanceRepresent a concrete object with unique identity and state
Instance AttributeStore per-object state
Class AttributeStore shared state or constants associated with the class
Instance MethodProvide behavior bound to an instance, accessing/modifying instance state
Class MethodProvide behavior bound to the class, often factory or alternative constructors
Static MethodProvide class-associated behavior without access to instance or class state
InheritanceEnable specialization and reuse by deriving classes from bases
PolymorphismAllow common operations on different object types via shared interface or behavior
Abstract Base ClassDefine explicit behavioral contracts preventing instantiation without required method implementations
CompositionAssemble objects from collaborating components with distinct responsibilities
DelegationForward operations from one object to another, optionally adapting behavior

A conceptual representation of these relationships:

Class Blueprint for instances Instance Unique state & identity Methods Provide behavior Inheritance Polymorphism Common operations Component Distinct responsibility Composed Object Has-a relationship Delegation

Python Classes

A Python class is an object created by executing a class definition. It defines attributes, methods, inheritance relationships, and behavior shared by its instances.

The basic structure of a class definition uses the class statement, followed by the class name, optional base classes in parentheses, and an indented body containing attributes and methods:

class ClassName(BaseClass1, BaseClass2):
    class_attribute = value

    def __init__(self, param):
        self.instance_attribute = param

    def instance_method(self):
        # behavior using self
        pass

When Python executes the class definition, the body code runs in a new namespace. The resulting namespace dictionary is used to create the class object bound to the class name. This class object can then be used to create instances.

Example:

class BankAccount:
    interest_rate = 0.02  # Class attribute shared by all accounts

    def __init__(self, owner, balance=0):
        self.owner = owner          # Instance attribute
        self.balance = balance      # Instance attribute

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            return amount
        else:
            raise ValueError("Insufficient funds")

# Creating instances
alice_account = BankAccount("Alice", 1000)
bob_account = BankAccount("Bob", 500)

Class names should be descriptive and represent a defensible abstraction with cohesive behavior, avoiding becoming a container for unrelated functions.

Class objects in Python are first-class: they can be referenced, assigned, passed as arguments, stored in collections, inspected, and called to create instances.

Examples of class objects as first-class entities:

AnotherName = BankAccount  # Assign class to another name

all_classes = [BankAccount, AnotherName]

def create_account(account_class, owner):
    return account_class(owner)

new_account = create_account(BankAccount, "Charlie")

Python Instances

An instance is an object associated with a class that carries object-specific state while participating in behavior defined by its class and applicable base classes.

Instantiating a class conceptually involves calling the class, which creates a new instance, initializes its state via __init__, and returns the object. This process excludes the lower-level internal mechanics of object creation.

The __init__ method initializes an already created instance, distinct from the complete object creation process.

Example:

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

p1 = Point(1, 2)
p2 = Point(3, 4)

print(p1.x, p1.y)  # 1 2
print(p2.x, p2.y)  # 3 4

print(p1 is p2)    # False, different identities

The isinstance function checks if an object is an instance of a class or its subclasses, while type returns the exact runtime type.

Example contrasting type and isinstance:

class Animal:
    pass

class Dog(Animal):
    pass

dog = Dog()

print(type(dog) is Dog)        # True
print(type(dog) is Animal)     # False

print(isinstance(dog, Dog))    # True
print(isinstance(dog, Animal)) # True

Instance State in Python

Instance state is data associated with an individual object through instance attributes and other supported storage mechanisms.

Instance attributes are typically initialized and accessed through self, which is the conventional parameter name referring to the current instance passed to an instance method. self is not a reserved keyword but a strong convention.

Example:

class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1

    def reset(self):
        self.count = 0

Mutation of instance state changes the data within the object. This is distinct from rebinding a local variable that refers to an instance. Multiple names (aliases) referring to the same instance observe the same state changes.

Example:

c1 = Counter()
c2 = c1  # Both names refer to the same instance
c3 = Counter()  # Different instance

c1.increment()
print(c1.count)  # 1
print(c2.count)  # 1, same instance as c1
print(c3.count)  # 0, independent instance

Instance attributes can also hold nested mutable values like lists or dictionaries. Ownership of the attribute reference differs from exclusivity of the referenced mutable object.

Alternative instance-storage mechanisms include __slots__, which restrict the attributes an instance can have but do not conceptually define instance state only by the presence of an instance __dict__.


Shared Class State in Python

Class attributes are attributes associated with the class object and commonly visible through instances when no instance-specific attribute shadows the same name.

Shared class state differs from per-instance state. Shared mutable class attributes can intentionally or unintentionally connect all instances, leading to subtle bugs if mutated unintentionally.

Example contrasting immutable and mutable class attributes:

class Config:
    default_timeout = 30  # Immutable shared configuration

class Logger:
    logs = []  # Mutable shared attribute (dangerous)

log1 = Logger()
log2 = Logger()

log1.logs.append("Error 1")  # Affects all instances
print(log2.logs)             # ['Error 1']

# Corrected design: per-instance logs
class LoggerFixed:
    def __init__(self):
        self.logs = []

log3 = LoggerFixed()
log4 = LoggerFixed()

log3.logs.append("Warning")
print(log4.logs)  # []

Instance attribute shadowing occurs when an instance attribute shares the same name as a class attribute. Assigning through an instance creates or updates the instance attribute without affecting the class attribute.

Example:

class Device:
    status = "off"  # Class attribute

d1 = Device()
d2 = Device()

print(d1.status)  # off
print(d2.status)  # off

d1.status = "on"  # Instance attribute shadows class attribute

print(d1.status)  # on
print(d2.status)  # off

Device.status = "standby"

print(d1.status)  # on (instance attribute)
print(d2.status)  # standby (class attribute updated)

Shared class state is appropriate for constants, shared configuration, counters, registries, or metadata, but requires explicit ownership and mutation rules.

Attribute TypeOwnershipLookup PriorityMutation EffectSharingShadowing BehaviorCommon Failure Modes
Instance AttributeIndividual objectInstance first, then classAffects only that instanceNoneShadows class attributeUnintended sharing if assigned at class level
Class AttributeClass objectClass only if not on instanceAffects all instances if mutableShared by all instancesCan be shadowed by instance attributeMutating shared mutable attribute unintentionally

Python Methods

Python methods are callable behaviors associated with classes. The main types differ in binding behavior and intended responsibility: instance methods, class methods, and static methods.

Instance Methods in Python

Instance methods are ordinary functions defined in a class that become bound to instances when accessed through them. They receive the instance as the first parameter, conventionally named self.

Example:

class Greeter:
    def greet(self, name):
        print(f"Hello, {name}! From {self}")

g = Greeter()
g.greet("Alice")  # Called through instance

# Calling instance method through class with explicit instance
Greeter.greet(g, "Bob")

Instance methods can read and modify instance state, use class-level information where appropriate, and collaborate with other methods while focusing on behavior of one instance.

Class Methods in Python

Class methods are decorated with @classmethod. They receive the class as the first parameter, conventionally named cls, providing behavior bound to the class.

Class methods are often used for alternative constructors and enable subclass-aware construction by referring to cls rather than a hard-coded class name.

Example:

class Person:
    def __init__(self, name):
        self.name = name

    @classmethod
    def from_full_name(cls, full_name):
        first_name = full_name.split()[0]
        return cls(first_name)

p = Person.from_full_name("John Smith")
print(p.name)  # John

class Employee(Person):
    pass

e = Employee.from_full_name("Jane Doe")
print(e.name)  # Jane

Static Methods in Python

Static methods are decorated with @staticmethod. They are class-associated callables that receive no automatic instance or class argument.

Static methods are coherent when the operation conceptually belongs near the class but does not require access to instance or class state. Otherwise, a module-level function may be clearer.

Example:

class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b

print(MathUtils.add(3, 4))  # 7

Example class with all three method types:

class Example:
    class_value = 42

    def instance_method(self):
        print(f"Instance method, class_value: {self.class_value}")

    @classmethod
    def class_method(cls):
        print(f"Class method, class_value: {cls.class_value}")

    @staticmethod
    def static_method():
        print("Static method, no access to instance or class state")

e = Example()
e.instance_method()
Example.class_method()
Example.static_method()
Method TypeAutomatic BindingConventional First ParameterAccess to Instance StateAccess to Class StateSubclass AwarenessTypical Use Case
Instance MethodBound to instanceselfYesVia instance or classYesBehavior of a particular object
Class MethodBound to classclsNoYesYesAlternative constructors, class-level behavior
Static MethodNo automatic bindingNoneNoNoNoUtility functions related to class

Decorators like @classmethod and @staticmethod modify descriptor access behavior, not just serving as documentation.

Example of method selection and implementation:

  • Use instance method when behavior depends on instance state.
  • Use class method for alternative constructors or behaviors involving class state.
  • Use static method for utility functions logically grouped with class but independent of instance or class state.
  • Use module-level function if behavior is unrelated to class abstraction.

Encapsulation in Python

Encapsulation involves designing objects so that state and behavior are exposed through coherent interfaces while implementation details remain replaceable or intentionally non-public.

Leading-underscore naming conventions signal implementation-oriented attributes or methods but do not enforce access restrictions.

Double-leading-underscore names are transformed by name mangling to reduce accidental name collisions in subclasses, not to provide security or true privacy.

Example:

class Example:
    public_attr = "public"
    _impl_attr = "internal use"
    __mangled_attr = "name mangled"

e = Example()
print(e.public_attr)      # public
print(e._impl_attr)       # internal use
# print(e.__mangled_attr) # AttributeError

print(e._Example__mangled_attr)  # name mangled, accessible but discouraged

Behavioral encapsulation is achieved by methods that validate state transitions and preserve invariants, rather than exposing unrestricted direct mutation.

Example:

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

    def get_celsius(self):
        return self._celsius

    def set_celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero")
        self._celsius = value

t = Temperature(20)
t.set_celsius(-300)  # Raises ValueError

Managed Attributes with Python Properties

Properties provide managed attributes that preserve attribute-access syntax while delegating retrieval, assignment, or deletion to methods.

Read-only computed properties use @property to compute a value derived from instance state without storing redundant data.

Example of read-only property:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):
        return self.width * self.height

r = Rectangle(3, 4)
print(r.area)  # 12

Property setters enable validation, normalization, or coordinated state updates while preserving object invariants.

Example with setter:

class Person:
    def __init__(self, age):
        self._age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value

p = Person(30)
p.age = 35  # Valid
# p.age = -5  # Raises ValueError

Property deleters are used only when deletion is meaningful. Properties do not require defining all getter, setter, and deleter methods—only those needed.

Python encourages starting with simple public attributes and introducing properties when behavior or invariants require controlled access.


Inheritance in Python

Inheritance is a class relationship where a subclass participates in behavior and attribute lookup defined by one or more base classes while being able to specialize or extend that behavior.

Subclass construction specifies base classes in parentheses after the class name. Inherited methods and attributes become available via ordinary attribute resolution.

Example:

class Animal:
    def speak(self):
        print("Animal sound")

class Dog(Animal):
    def speak(self):
        print("Woof!")

a = Animal()
d = Dog()

a.speak()  # Animal sound
d.speak()  # Woof!

issubclass and isinstance check subtype relationships, distinguishing subclass compatibility from exact type identity.

Method Overriding in Python

Subclasses can provide methods with the same name as base classes. These override base methods and take precedence in method resolution.

Example:

class Vehicle:
    def move(self):
        print("Moving")

class Car(Vehicle):
    def move(self):
        print("Driving")

v = Vehicle()
c = Car()

v.move()  # Moving
c.move()  # Driving

Subclasses may extend inherited behavior by invoking base methods rather than completely replacing them.

Multiple Inheritance in Python

Python supports multiple inheritance where a class can have more than one base class.

Example:

class Flyer:
    def fly(self):
        print("Flying")

class Swimmer:
    def swim(self):
        print("Swimming")

class Duck(Flyer, Swimmer):
    pass

d = Duck()
d.fly()   # Flying
d.swim()  # Swimming

The diamond problem occurs when multiple inheritance forms a diamond shape. Naive repeated base-class calls may duplicate shared ancestor initialization.

Python Method Resolution Order

The Method Resolution Order (MRO) is a deterministic linearization used to search classes for attributes and cooperative methods.

Example:

class A:
    def greet(self):
        print("A")

class B(A):
    def greet(self):
        print("B")

class C(A):
    def greet(self):
        print("C")

class D(B, C):
    pass

print(D.__mro__)
d = D()
d.greet()  # B, because B precedes C in MRO

MRO preserves local precedence and consistent ordering.

ConceptPurposePrincipal MechanismCommon Risk
Single InheritanceSimple specializationOne base classDeep hierarchies become complex
Multiple InheritanceCombine independent behaviorsLinearized MROAmbiguous attribute resolution
OverridingCustomize inherited behaviorMethod replacementViolating behavioral contract
MROAttribute lookup orderC3 linearizationUnexpected method called
Cooperative InvocationEnsuring all classes participateUse of super()Omitting base calls causes bugs

Cooperative Inheritance with super() in Python

super() allows continuing attribute or method lookup according to MRO, not hard-coding a particular parent class.

Example:

class A:
    def process(self):
        print("A")
        super().process()

class B(A):
    def process(self):
        print("B")
        super().process()

class C(B):
    def process(self):
        print("C")
        super().process()

class D(C):
    def process(self):
        print("D")
        # End of chain; no super call

d = D()
d.process()

Output:

D
C
B
A

Cooperative constructors forward arguments and honor calling conventions to ensure all initialization occurs once and in order.

Example contrasting direct base calls with cooperative super():

class Base:
    def __init__(self):
        print("Base init")

class Left(Base):
    def __init__(self):
        print("Left init")
        super().__init__()

class Right(Base):
    def __init__(self):
        print("Right init")
        super().__init__()

class Child(Left, Right):
    def __init__(self):
        print("Child init")
        super().__init__()

c = Child()

Output:

Child init
Left init
Right init
Base init

Direct base-class calls (e.g., Base.__init__(self)) can cause duplicate initializations or skip classes, breaking cooperative behavior.


Polymorphism in Python

Polymorphism is the ability to apply a common operation to different objects whose compatible behavior allows each object to provide its appropriate result.

Subclass Polymorphism in Python

Instances of different subclasses can be used through behavior defined or expected by a common base abstraction.

Example:

class Shape:
    def area(self):
        raise NotImplementedError

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

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

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side * self.side

shapes = [Circle(2), Square(3)]

for shape in shapes:
    print(shape.area())

Behavioral substitutability requires that subclasses preserve the expectations of callers regarding operations, result meaning, accepted operations, and invariants.

Duck Typing in Python

Duck typing bases compatibility on supported behavior rather than explicit inheritance.

Example:

class Duck:
    def quack(self):
        print("Quack")

class Person:
    def quack(self):
        print("I'm quacking like a duck")

def make_it_quack(obj):
    obj.quack()

make_it_quack(Duck())
make_it_quack(Person())

Python often attempts the required operation and handles meaningful failure rather than performing explicit concrete-type checks.

Example contrasting rigid type check and duck typing:

def speak(obj):
    if isinstance(obj, Dog):
        obj.bark()
    elif isinstance(obj, Cat):
        obj.meow()
    else:
        print("Unknown animal")

# Versus duck typing:

def speak_duck(obj):
    try:
        obj.speak()
    except AttributeError:
        print("Object cannot speak")

Inheritance-based polymorphism and duck typing can coexist; Python code may deliberately use explicit nominal relationships or behavioral compatibility depending on abstraction.

FeatureSubclass PolymorphismDuck Typing
Required RelationshipExplicit inheritance hierarchySupported behavior, no inheritance required
Compatibility BasisClass-based interfaceBehavior-based interface
Interface ExpressionBase class and overridden methodsPresence of required methods
FlexibilityLess flexible, rigid hierarchyMore flexible, dynamic
Representative RisksInappropriate inheritance, brittleRuntime errors if behavior missing

Abstract Base Classes in Python

Abstract base classes (ABCs) define explicit behavioral abstractions and prevent ordinary instantiation while required abstract operations remain unimplemented.

Using abc.ABC as a base and @abstractmethod decorator declares abstract methods while allowing concrete shared behavior.

Example:

from abc import ABC, abstractmethod

class Vehicle(ABC):
    @abstractmethod
    def drive(self):
        pass

    def stop(self):
        print("Stopping")

class Car(Vehicle):
    def drive(self):
        print("Driving car")

class Bike(Vehicle):
    def drive(self):
        print("Riding bike")

vehicles = [Car(), Bike()]
for v in vehicles:
    v.drive()
    v.stop()

Attempting to instantiate an abstract base class or subclass lacking implementations of abstract methods raises TypeError.

Example:

try:
    v = Vehicle()
except TypeError as e:
    print(e)  # Can't instantiate abstract class Vehicle with abstract methods drive

ABCs are useful for explicit shared contracts, while duck typing or composition may avoid unnecessary nominal hierarchy.


Composition in Python

Composition builds an object from collaborating component objects whose responsibilities remain distinct, commonly expressing a has-a relationship rather than an is-a specialization.

Dependency ownership in composition varies: components may be created internally, supplied externally, shared among objects, or replaceable through configuration.

Example:

class Engine:
    def start(self):
        print("Engine started")

class Car:
    def __init__(self, engine):
        self.engine = engine  # Composition

    def start(self):
        self.engine.start()
        print("Car is running")

engine = Engine()
car = Car(engine)
car.start()

Composition reduces inheritance coupling by allowing behavior to vary through replacement of collaborating objects rather than creating additional subclasses.

Contrasting design:

# Inheritance-based design
class CarWithEngine(Engine):
    def start(self):
        super().start()
        print("Car is running")

# Composition-based design
class Car:
    def __init__(self, engine):
        self.engine = engine

    def start(self):
        self.engine.start()
        print("Car is running")

The composed version better represents independent responsibilities.

Lifecycle considerations include whether the composed object owns, shares, or merely references collaborators.


Delegation in Python

Delegation involves an object receiving an operation and forwarding all or part of the responsibility to another object, optionally adapting inputs, outputs, policy, or surrounding behavior.

Explicit delegation is implemented by methods calling corresponding behavior on a collaborator, distinct from inheritance or merely storing another object.

Example of explicit delegation:

class Logger:
    def log(self, message):
        print(f"Log: {message}")

class Service:
    def __init__(self, logger):
        self.logger = logger

    def process(self, data):
        self.logger.log(f"Processing {data}")
        # perform processing
        return data.upper()

logger = Logger()
service = Service(logger)
result = service.process("example")
print(result)

Dynamic delegation uses __getattr__ to forward unresolved attribute access, but can hide interfaces and forward unintended behavior.

Example of dynamic delegation:

class Wrapper:
    def __init__(self, delegate):
        self._delegate = delegate

    def __getattr__(self, name):
        return getattr(self._delegate, name)

class Target:
    def action(self):
        print("Action performed")

t = Target()
w = Wrapper(t)
w.action()  # Delegated to Target.action()

Explicit delegation is usually clearer when only a small stable interface should be exposed.

Delegation coexists with composition and polymorphism without implying that every composed object should expose its collaborator's full interface.


Solved Object-Oriented Programming Exercises in Python

Exercise 1: BankAccount Class

class BankAccount:
    interest_rate = 0.03  # shared class attribute

    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance  # instance attribute with controlled access

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount

    def withdraw(self, amount):
        if amount > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= amount

    @classmethod
    def from_string(cls, account_str):
        owner, balance = account_str.split(',')
        return cls(owner.strip(), float(balance))

    @staticmethod
    def validate_amount(amount):
        return amount > 0

    @property
    def balance(self):
        return self._balance

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

# Usage
account1 = BankAccount("Alice", 1000)
account2 = BankAccount.from_string("Bob, 500")

account1.deposit(200)
account2.withdraw(100)

print(account1.balance)  # 1200
print(account2.balance)  # 400

Explanation:

  • Defined class with shared interest rate.
  • Instance attributes for owner and balance.
  • Methods enforce validation and state changes.
  • Alternative constructor from_string uses class method.
  • Static method validates amounts without needing instance or class state.
  • Managed property balance enforces invariant on assignment.

Exercise 2: Abstract Base Class and Duck Typing

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

class CreditCardProcessor(PaymentProcessor):
    def pay(self, amount):
        print(f"Charging credit card: ${amount}")

class PaypalProcessor(PaymentProcessor):
    def pay(self, amount):
        print(f"Processing PayPal payment: ${amount}")

class CashPayment:
    def pay(self, amount):
        print(f"Paying cash: ${amount}")

def process_payment(processor, amount):
    processor.pay(amount)

cc = CreditCardProcessor()
pp = PaypalProcessor()
cash = CashPayment()

for p in [cc, pp, cash]:
    process_payment(p, 100)

Explanation:

  • PaymentProcessor is an abstract base class defining a payment interface.
  • CreditCardProcessor and PaypalProcessor implement the abstract method.
  • CashPayment does not inherit but supports the same method (pay) — demonstrating duck typing.
  • Client code treats all processors uniformly without branching on concrete types.

Exercise 3: Composition, Delegation, and Cooperative Multiple Inheritance

class LoggerMixin:
    def log(self, message):
        print(f"[LOG]: {message}")

class Storage:
    def __init__(self):
        self._data = {}

    def save(self, key, value):
        self._data[key] = value

    def load(self, key):
        return self._data.get(key, None)

class Service(LoggerMixin, Storage):
    def __init__(self):
        super().__init__()  # cooperative call

    def process(self, key, value):
        self.log(f"Processing key={key}, value={value}")
        self.save(key, value)

s = Service()
s.process("x", 42)
print(s.load("x"))

# Inspect MRO
print(Service.__mro__)

Output:

[LOG]: Processing key=x, value=42
42
(<class '__main__.Service'>, <class '__main__.LoggerMixin'>, <class '__main__.Storage'>, <class 'object'>)

Explanation:

  • LoggerMixin and Storage define independent behaviors.
  • Service composes behavior via multiple inheritance and uses super() cooperatively.
  • process delegates logging and storage responsibilities to respective base classes.
  • MRO shows deterministic search order, ensuring each base is initialized once.

Reviewing an object-oriented Python design involves checking for:

  • Classes with unrelated responsibilities that should be split.
  • Unnecessary inheritance that can be replaced by composition.
  • Duplicated state or accidental shared mutable class data.
  • Weak or missing invariants leading to inconsistent object states.
  • Excessive getters and setters that break encapsulation.
  • Fragile multiple inheritance without cooperative super() use.
  • Inappropriate concrete-type branching instead of polymorphism.
  • Abstract classes without meaningful contracts or incomplete implementations.
  • Delegation exposing too much of collaborator internals instead of a minimal interface.

Adhering to clear subclassing, encapsulation, and collaboration principles leads to maintainable and correct object-oriented Python code.