I modeled a small automation domain with Python classes, abstract base classes, and dataclasses for value objects. An explicit object model with clear interfaces made the system easy to extend and unit-test without touching existing code.

Objective & Context

OOP organizes state and behaviour behind interfaces. This lab covers instance vs class attributes, inheritance, polymorphism via abstract base classes, and boilerplate-free value types with @dataclass, applying the open/closed principle to automation components.

Learning Objectives

  • Define interfaces with abc.ABC and @abstractmethod.
  • Model value objects with @dataclass.
  • Use inheritance and polymorphism for extensible dispatch.
  • Separate data (dataclasses) from behaviour (services).
  • Verify polymorphic behaviour with pytest.

Environment & Prerequisites

Prerequisites

  • Python 3.11 with dataclasses and abc (standard library)
  • A domain with multiple related entity types to model
  • pytest for behaviour verification
  • Assumed skill: functions and modules

Step-by-Step Execution

1. Define an interface with abc

from abc import ABC, abstractmethod
class Notifier(ABC):
    @abstractmethod
    def send(self, msg: str) -> bool: ...

Expected output:

(no output; Notifier ABC defined)

What this teaches: An abstract base class turns an informal contract into an enforced one: subclasses must implement send() or they cannot be instantiated.

2. A dataclass value object

cat > alert.py <<'EOF' from dataclasses import dataclass @dataclass class Alert: ip: str sev: int if __name__ == "__main__": print(Alert("1.2.3.4", 2)) EOF python alert.py

Expected output:

Alert(ip='1.2.3.4', sev=2)

What this teaches: The dataclass generated __init__, __repr__, and __eq__ from two field declarations; the readable repr is free and invaluable in logs. Writing real files instead of one-liners keeps the examples runnable and reviewable.

3. Polymorphic dispatch

cat > notify.py <<'EOF' from abc import ABC, abstractmethod class Notifier(ABC): @abstractmethod def send(self, msg: str) -> bool: ... class SlackNotifier(Notifier): def send(self, msg: str) -> bool: print(f"slack: {msg}") return True EOF python -c "from notify import SlackNotifier; print(SlackNotifier().send('up'))"

Expected output:

slack: up
True

What this teaches: Polymorphic dispatch means the caller works against the Notifier interface while each subclass decides how sending works; adding a new channel needs no caller changes. The abstract method blocks instantiating a half-built notifier, so missing implementations fail at construction time, not at 3am.

How It Works

How It Works

Abstract base classes formalize the dependency inversion idea: high-level code depends on the Notifier abstraction, not on Slack or email details. @abstractmethod makes the contract compiler-enforced at instantiation time rather than documentation-only.

Dataclasses remove the boilerplate tax on value objects. A class that only carries data gets __init__, __repr__, and __eq__ generated from field declarations, and frozen=True additionally makes instances immutable and hashable.

Polymorphism pays off when the set of variants grows. Each new notifier is a new class implementing send(); the dispatching code never changes, which is the open/closed principle in its most practical form.

Validation & Testing

Add a new Notifier subclass without modifying the dispatcher and confirm it works via the shared interface. Pass criteria: subclasses are interchangeable, dataclasses compare by value, and abstract methods enforce the contract.

Advanced: Troubleshooting
  • Cannot instantiate ABC: implement all abstract methods in the subclass.
  • Mutable dataclass default: use field(default_factory=list).
  • Shared class attribute mutated: initialize per-instance state in __init__.

Troubleshooting

SymptomLikely causeFix
TypeError: Can't instantiate abstract classa subclass missed one @abstractmethodimplement every abstract method or mark the subclass abstract too
dataclass equality surpriseseq defaults to True and compares all fieldsset eq=False or compare selected fields when identity semantics are needed
mutable dataclass field shares statefield default like [] is shared across instancesuse field(default_factory=list) instead
isinstance checks everywheredispatch logic leaked into callerspush the variation into polymorphic methods instead of branching on type
super().__init__() missed in subclassparent initialization never rancall super().__init__() first in every overriding __init__

Key Results

  • Modeled the domain behind a single abstract interface.
  • Added new behaviour via subclassing with zero changes to callers.
  • Eliminated boilerplate with dataclass value objects.
  • Verified polymorphism and value equality with unit tests.

Key Takeaways

  • ABCs turn informal contracts into enforced interfaces.
  • Dataclasses generate boilerplate (__init__, __repr__, __eq__) from field declarations.
  • Polymorphism lets callers depend on abstractions while variants multiply.
  • Separate data carriers (dataclasses) from behaviour (services).
  • New variants should require new classes, not edits to dispatch code.

Practice Challenges

Challenge 1: Frozen alert

Goal: Make Alert frozen and hashable, then use alerts as dict keys in a deduplication pass.

Hint: frozen=True gives immutability and a hash; verify duplicates collapse.

Challenge 2: Third channel

Goal: Add a PagerNotifier subclass and prove the existing dispatch code needs zero changes.

Hint: Inherit Notifier, implement send(), and pass it to the same caller.