Abstract Base Classes
Background reading: how Python ABCs define interfaces and enforce contracts
The Problem ABCs Solve
Suppose you want to build a system where different teams can independently write different metric implementations (line length, Hjorth, PSD, entropy, ...) and have them all work interchangeably with the rest of the system.
The challenge: how do you guarantee that every metric implementation provides a
compute() function? Without any enforcement, someone might forget to add it,
or name it differently (run(), calculate()), and the system breaks at runtime
in hard-to-debug ways.
Abstract Base Classes solve this by defining an interface contract: a set of methods that every subclass must implement. If a subclass is missing any required method, Python refuses to instantiate it — you get a clear error immediately.
How ABCs Work in Python
Python's abc module provides two key tools:
ABC (the base class to inherit from) and @abstractmethod (the decorator
that marks a method as required).
from abc import ABC, abstractmethod
class Animal(ABC): # ABC = Abstract Base Class
@abstractmethod
def speak(self) -> str:
... # no implementation here — just the signature
def greet(self): # regular method — concrete, inherited as-is
print(f"I say: {self.speak()}")
# This works — Dog provides speak():
class Dog(Animal):
def speak(self) -> str:
return "Woof"
d = Dog()
d.greet() # "I say: Woof"
# This fails at instantiation — Cat doesn't implement speak():
class Cat(Animal):
pass
c = Cat() # TypeError: Can't instantiate abstract class Cat with abstract method speak
The Metric ABC in Montage Concord
Here is the actual Metric ABC from concord-core:
from abc import ABC, abstractmethod
from .containers import Recording, MetricResult
class Metric(ABC):
@property
@abstractmethod
def name(self) -> str: ...
@property
@abstractmethod
def requires(self) -> MetricRequirements: ...
@abstractmethod
def compute(self, recording: Recording) -> MetricResult: ...
Any class that inherits from Metric but doesn't implement all three
(name, requires, compute) cannot be instantiated.
Python enforces this automatically.
@property + @abstractmethod
name and requires use both @property and @abstractmethod.
The @property decorator means they are accessed as attributes, not called as methods:
# With @property:
metric = LineLength(window_s=1.0)
print(metric.name) # "line_length" — no parentheses!
# Without @property, you'd call it as a method:
print(metric.name()) # would fail with @property
This is a design choice: name and requires are conceptually constant properties
of a metric class, not computations. Using @property signals this intent.
The Implementation Pattern
Here is how a concrete metric class is structured, following the contract:
from concord_core.abc import Metric, MetricRequirements
from concord_core import Recording, MetricResult
class LineLength(Metric):
def __init__(self, window_s=None, step_s=None):
self.window_s = window_s # config stored here
self.step_s = step_s
@property
def name(self) -> str:
return "line_length"
@property
def requires(self) -> MetricRequirements:
return MetricRequirements(
min_duration_s=self.window_s or 0.0
)
def compute(self, recording: Recording) -> MetricResult:
# ... actual computation ...
return MetricResult(data=result, metric_name=self.name, ...)
Why This Design?
Interchangeability
Because every metric has compute(recording), you can write code that works with
any metric without knowing which one it is:
def run_all_metrics(recording, metrics: list[Metric]):
results = []
for metric in metrics:
results.append(metric.compute(recording))
return results
# Works with any combination:
run_all_metrics(rec, [LineLength(), HjorthParameters(), WelchPSD()])
Discoverability
The concord-fit package (future) will discover all installed metrics via
entry_points and run them uniformly — without knowing their names in advance.
It just needs to know they all have compute().
Validation via requires
Before running a metric, code can check its requirements:
def safe_compute(metric: Metric, recording: Recording):
req = metric.requires
if recording.duration < req.min_duration_s:
raise ValueError(f"Recording too short for {metric.name}")
if recording.n_channels < req.min_channels:
raise ValueError(f"Not enough channels for {metric.name}")
return metric.compute(recording)
Stateless Design
Notice that a Metric subclass only stores configuration in its __init__
(window size, filter parameters, etc.) — never intermediate computation results.
The result comes out of compute() and is returned as a new MetricResult object.
This makes metrics pickle-safe: you can send a Metric object to a worker process
via multiprocessing, have it call compute(), and get the MetricResult back.
If the metric stored its own state (half-computed arrays, etc.), pickling could fail or produce wrong results.
The Model ABC — Same Pattern, Different Domain
Montage Concord uses the same ABC pattern for neural mass models.
The Model ABC in concord-core defines the simulation contract:
class Model(ABC):
@property
@abstractmethod
def name(self) -> str: ...
@abstractmethod
def default_parameters(self) -> ParameterVector: ...
@abstractmethod
def simulate(self, parameters, duration_s, fs, seed=None) -> ModelOutput: ...
Just like Metric guarantees every metric has compute(),
Model guarantees every model has simulate().
The optimization engine (concord-fit) can discover and run any installed model
without knowing its internals — it only needs to know the ABC contract.
See Neural Mass Models for more about what these models simulate.