TIL: Liskov substitution principle

May 13, 2024 • TIL, software engineering

Today, I wanted to give a subclass of an Abstract Base Class a more specific behaviour by overriding a method and changing the method interface:

from abc import ABC

class DecisionTree(ABC):
    ...

    def evaluate(
        self,
        activity: Activity,
        params: Params,
    ) -> industry_models.DecisionPath:
        _evaluate_steps(step_map=self.step_map, params=params)
        ...


class InvoiceDecisionTree(tree.DecisionTree):
    ...

    def evaluate(
        self, invoice: models.Invoice
    ) -> models.DecisionPath:
        assert invoice.activity
        params = tree.Params(state={"invoice": invoice})
        return super().evaluate(activity=invoice.activity, params=params)

However, I got this mypy error:

error: Signature of "evaluate" incompatible with supertype "DecisionTree"  [override]`.

Here is what the mypy docs say about it:

It’s unsafe to override a method with a more specific argument type, as it violates the Liskov substitution principle. For return types, it’s unsafe to override a method with a more general return type.

Instead of ignoring this with # ignore: type[override], I had a chat with ChatGPT about different ways to address this issue. I ended up opting for a simple wrapper method for the customized behaviour.

class InvoiceDecisionTree(tree.DecisionTree):
    ...

    def evaluate_for_invoice(
        self, invoice: models.Invoice
    ) -> models.DecisionPath:
        assert invoice.activity
        params = tree.Params(state={"invoice": invoice})
        return self.evaluate(activity=invoice.activity, params=params)

Recent posts