Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Low-Level Design (Object-Oriented Design) Module 4 – Design principles: SOLID and beyond

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle as behavioural subtyping: contract tests that catch a subtype breaking its parent, and fixes that change the hierarchy.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and Node.js 24.21.0
  • By MySmartCoPilot

What you will learn

  • State behavioural subtyping in terms of preconditions, postconditions and invariants
  • Detect substitution failures with contract tests run against every implementation
  • Fix a violation by changing the hierarchy instead of adding type checks
  • Recognise a history constraint and how an extra method in a subtype can break it

Before you start

On this page

The previous lesson added variants by plugging new classes in where old code expected a common type. That only works if each new class behaves the way the old code expects. The Liskov Substitution Principle (LSP) is that condition, and this lesson turns it into tests you can run against every implementation.

Behavioural subtyping

Barbara Liskov and Jeannette Wing made the idea precise in a paper of 1994. A type checker can confirm that a subtype has the right methods with the right signatures, but a program can still go wrong when the methods behave differently. So they defined subtyping by behaviour, in what they called the Subtype Requirement:

Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T.

In other words: whatever a caller can rely on for the parent type must still be true when it is given the child. Robert C. Martin named his principle of class design after Liskov, in the form “derived classes must be substitutable for their base classes”. Later he widened it from inheritance to interfaces of every kind:

A program that uses an interface must not be confused by an implementation of that interface.

In Python that includes duck typing. An object that you pass where a file, an iterable or your own protocol is expected counts as a subtype of that unwritten interface, and the same rules apply to it.

The rules, in terms of contracts

A contract says what a method needs (its precondition), what it promises (its postcondition) and what stays true of the object in between (its invariant). Liskov and Wing’s definition gives one rule for each, and Bertrand Meyer’s Design by Contract states the same rules for methods that a subclass redefines. A subtype’s methods:

  • Accept at least what the parent’s accept. A subtype may weaken a precondition, never strengthen it: if withdraw() works for any amount up to the balance, a subtype cannot add “only once it has matured” or “only below 100”.
  • Promise at least what the parent’s promise. A subtype may strengthen a postcondition, never weaken it: if withdraw() returns the new balance, a subtype cannot return None instead.
  • Keep the parent’s invariants. What is always true of the parent’s objects, such as “the balance is never negative”, stays true of the subtype’s.
  • Raise no new kinds of error. Callers handle the exceptions the parent documents; an exception they have never heard of is a broken promise.
  • Keep the parent’s history properties. Some promises are about change over time, such as “a price never changes”. The subtype’s own extra methods must not break them. This last rule is the one Liskov and Wing added to earlier work, and the one most easily missed.

Symptoms in code

Three patterns usually mean a subtype is not a real subtype:

  • an override that raises “not supported”, including NotImplementedError in a class that is meant to be used;
  • an override that quietly does nothing, so callers believe something happened;
  • callers that check isinstance() before calling a method, because one subtype must be treated differently.

A fixed deposit that refuses withdrawals

A bank’s Account lets you pay money in and take it out, and its docstring says exactly what withdraw() promises. FixedDeposit is locked until it matures, so it overrides withdraw() to refuse. To find out whether each subclass keeps the promise, write the promise once as tests in a mixin class, AccountContract, and run those tests against every implementation:

One contract, run against two implementations

broken/check_accounts.py

import unittest

from accounts import FixedDeposit, SavingsAccount
from contract import AccountContract, run


class SavingsAccountTest(AccountContract, unittest.TestCase):
    def make(self, balance):
        return SavingsAccount(balance)


class FixedDepositTest(AccountContract, unittest.TestCase):
    def make(self, balance):
        return FixedDeposit(balance)


broken = []
for test_class in (SavingsAccountTest, FixedDepositTest):
    name = test_class.__name__.removesuffix("Test")
    print(name)
    if run(test_class):
        broken.append(name)
print(f"Breaks its contract: {', '.join(broken) or 'nothing'}")

Output

SavingsAccount
  ok      deposit adds to the balance
  ok      withdrawing too much is refused
  ok      withdrawing within the balance succeeds
FixedDeposit
  ok      deposit adds to the balance
  FAILED  withdrawing too much is refused: accounts.WithdrawalNotAllowed: a fixed deposit cannot be withdrawn before it matures
  FAILED  withdrawing within the balance succeeds: accounts.WithdrawalNotAllowed: a fixed deposit cannot be withdrawn before it matures
Breaks its contract: FixedDeposit

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_accounts.py

broken/accounts.py

class InsufficientFunds(Exception):
    pass


class WithdrawalNotAllowed(Exception):
    pass


class Account:
    """Money you can pay in and take out.

    withdraw(amount): for 0 < amount <= balance, the balance falls by amount and the new balance is returned;
    for a larger amount, InsufficientFunds is raised and the balance stays as it was.
    """

    def __init__(self, balance=0):
        self._balance = balance

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

    def deposit(self, amount):
        self._balance += amount
        return self._balance

    def withdraw(self, amount):
        if amount > self._balance:
            raise InsufficientFunds(f"balance {self._balance}, asked for {amount}")
        self._balance -= amount
        return self._balance


class SavingsAccount(Account):
    pass


class FixedDeposit(Account):
    """Locked until it matures, so it refuses every withdrawal."""

    def withdraw(self, amount):
        raise WithdrawalNotAllowed("a fixed deposit cannot be withdrawn before it matures")

broken/contract.py

"""The behaviour every Account promises, as tests. Each implementation gets the same tests through make()."""
import unittest

from accounts import InsufficientFunds


class AccountContract:
    def make(self, balance):
        raise NotImplementedError

    def test_deposit_adds_to_the_balance(self):
        account = self.make(100)
        self.assertEqual(account.deposit(50), 150)

    def test_withdrawing_within_the_balance_succeeds(self):
        account = self.make(100)
        self.assertEqual(account.withdraw(30), 70)
        self.assertEqual(account.balance, 70)

    def test_withdrawing_too_much_is_refused(self):
        account = self.make(100)
        with self.assertRaises(InsufficientFunds):
            account.withdraw(500)
        self.assertEqual(account.balance, 100)


def run(test_class):
    """Runs one implementation's tests; prints one line per test and returns how many did not pass."""
    bad = 0
    for test in unittest.defaultTestLoader.loadTestsFromTestCase(test_class):
        result = unittest.TestResult()
        test.run(result)
        name = test.id().rpartition(".")[2].removeprefix("test_").replace("_", " ")
        problems = result.failures + result.errors
        if problems:
            bad += 1
            reason = problems[0][1].strip().splitlines()[-1]
            print(f"  FAILED  {name}: {reason}")
        else:
            print(f"  ok      {name}")
    return bad

SavingsAccount passes. FixedDeposit fails twice. It strengthens the precondition, because no amount at all can be withdrawn, and it raises WithdrawalNotAllowed, an exception that code written for Account has never heard of. Any function that pays a bill from an Account will crash when it is given a fixed deposit.

Common mistake

The quick fix is in the callers: if isinstance(account, FixedDeposit): ... before every withdrawal. It works until the next account type that cannot withdraw, and then every one of those checks must be found and extended. That is the switch statement of the previous lesson again, now spread across the callers. Fix the hierarchy instead.

Contract tests

The test class AccountContract is an ordinary class with test_ methods and one factory method, make(). Each implementation gets a test class that inherits from both the contract and unittest.TestCase and says how to make one of its objects. The contract is then written once, and a new implementation inherits all of its tests by adding a three-line class. That is the mechanical way to find substitution failures: if a subtype fails a test that its parent passes, it is not a subtype in Liskov and Wing’s sense, whatever its class statement says.

Test what callers rely on

A contract test checks the documented promise, not the details of one implementation. “Withdrawing 30 from 100 leaves 70” belongs in the contract; “the balance is stored in _balance” does not, because another implementation may store it differently and still keep the promise.

Fix the hierarchy, not the callers

The parent promised something not every child can do, so move the promise down. Account keeps what all accounts share, a balance and deposits. A new WithdrawableAccount adds withdraw() and its promise, and SavingsAccount inherits from it. FixedDeposit inherits from Account alone and adds what it really does, close() once it has matured. (If the bank’s fixed deposits also refuse top-ups, deposit() has the same problem and moves down as well, leaving Account with only the balance.) The contract splits the same way:

Only the accounts that allow withdrawals promise them

split/check_accounts.py

import unittest

from accounts import FixedDeposit, SavingsAccount
from contract import AccountContract, WithdrawableContract, run


class SavingsAccountTest(WithdrawableContract, unittest.TestCase):
    def make(self, balance):
        return SavingsAccount(balance)


class FixedDepositTest(AccountContract, unittest.TestCase):
    def make(self, balance):
        return FixedDeposit(balance)

    def test_closing_pays_out_once_matured(self):
        deposit = FixedDeposit(100, matured=True)
        self.assertEqual(deposit.close(), 100)
        self.assertEqual(deposit.balance, 0)


broken = []
for test_class in (SavingsAccountTest, FixedDepositTest):
    name = test_class.__name__.removesuffix("Test")
    print(name)
    if run(test_class):
        broken.append(name)
print(f"Breaks its contract: {', '.join(broken) or 'nothing'}")

Output

SavingsAccount
  ok      deposit adds to the balance
  ok      withdrawing too much is refused
  ok      withdrawing within the balance succeeds
FixedDeposit
  ok      closing pays out once matured
  ok      deposit adds to the balance
Breaks its contract: nothing

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_accounts.py

split/accounts.py

class InsufficientFunds(Exception):
    pass


class Account:
    """Every account has a balance and accepts deposits. Nothing here promises withdrawals."""

    def __init__(self, balance=0):
        self._balance = balance

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

    def deposit(self, amount):
        self._balance += amount
        return self._balance


class WithdrawableAccount(Account):
    """withdraw(amount): for 0 < amount <= balance, the balance falls by amount and the new balance is returned;
    for a larger amount, InsufficientFunds is raised and the balance stays as it was."""

    def withdraw(self, amount):
        if amount > self._balance:
            raise InsufficientFunds(f"balance {self._balance}, asked for {amount}")
        self._balance -= amount
        return self._balance


class SavingsAccount(WithdrawableAccount):
    pass


class FixedDeposit(Account):
    """Locked until it matures. It offers no withdraw() at all, so nobody can call it by mistake."""

    def __init__(self, balance=0, matured=False):
        super().__init__(balance)
        self.matured = matured

    def close(self):
        """Pays out the whole balance once the deposit has matured."""
        if not self.matured:
            raise ValueError("this fixed deposit has not matured yet")
        payout, self._balance = self._balance, 0
        return payout

split/contract.py

"""Contracts as tests: one for every Account, and one more for accounts that allow withdrawals."""
import unittest

from accounts import InsufficientFunds


class AccountContract:
    def make(self, balance):
        raise NotImplementedError

    def test_deposit_adds_to_the_balance(self):
        account = self.make(100)
        self.assertEqual(account.deposit(50), 150)


class WithdrawableContract(AccountContract):
    def test_withdrawing_within_the_balance_succeeds(self):
        account = self.make(100)
        self.assertEqual(account.withdraw(30), 70)
        self.assertEqual(account.balance, 70)

    def test_withdrawing_too_much_is_refused(self):
        account = self.make(100)
        with self.assertRaises(InsufficientFunds):
            account.withdraw(500)
        self.assertEqual(account.balance, 100)


def run(test_class):
    """Runs one implementation's tests; prints one line per test and returns how many did not pass."""
    bad = 0
    for test in unittest.defaultTestLoader.loadTestsFromTestCase(test_class):
        result = unittest.TestResult()
        test.run(result)
        name = test.id().rpartition(".")[2].removeprefix("test_").replace("_", " ")
        problems = result.failures + result.errors
        if problems:
            bad += 1
            reason = problems[0][1].strip().splitlines()[-1]
            print(f"  FAILED  {name}: {reason}")
        else:
            print(f"  ok      {name}")
    return bad
Before: SavingsAccount and FixedDeposit inherit withdraw() from Account. After: only SavingsAccount, via WithdrawableAccount, has it.Before: FixedDeposit inheritsa promise it cannot keepAfter: only accounts that allowwithdrawals promise themAccountbalance, deposit()withdraw(): succeedswithin the balanceSavingsAccountFixedDepositwithdraw() always raisesWithdrawalNotAllowedAccountbalance, deposit()WithdrawableAccountwithdraw(): succeedswithin the balanceSavingsAccountFixedDepositclose() once ithas maturedinherits frominherits frominherits frominherits frominherits from

The account hierarchy before and after the split

Text description of the diagram

The diagram shows two class hierarchies, one above the other. Each arrow points from a class to the class it inherits from.

  1. Before: SavingsAccount and FixedDeposit both inherit from Account, which offers a balance, deposit() and withdraw(), and promises that a withdrawal within the balance succeeds. FixedDeposit overrides withdraw() to always raise WithdrawalNotAllowed, so it cannot keep that promise.
  2. After: Account offers only a balance and deposit(). WithdrawableAccount inherits from Account and adds withdraw() with the promise, and SavingsAccount inherits from WithdrawableAccount. FixedDeposit inherits from Account directly and adds close(), which pays out once the deposit has matured; it never promised withdrawals, so it breaks nothing.

Code that takes money out now asks for a WithdrawableAccount, in its type hints and in its documentation, so a type checker rejects a fixed deposit before the program runs, and no caller needs an isinstance() check. When splitting the interface does not fit, there are two other honest fixes. Composition: a class can hold and use an account without claiming to be one. Or a weaker contract: if withdraw() may be refused for reasons callers cannot predict, say so in the parent and return or raise something every caller handles. Then every caller changes once, on purpose, instead of failing later by surprise.

History: promises about change

The hardest rule to spot is the one about history, because no single call misbehaves. FixedPriceList promises that its prices never change, and Quote trusts that promise, so it keeps no copy of the prices. EditablePriceList inherits from it and adds one method, set_price():

An extra method that breaks a promise about time Python · history/price_lists.py
"""A promise about history: "prices never change". A subclass that adds set_price() breaks it."""
from types import MappingProxyType


class FixedPriceList:
    """Prices for one season. Promise: once the list exists, no price in it changes."""

    def __init__(self, prices):
        self._prices = dict(prices)

    def price(self, item):
        return self._prices[item]


class EditablePriceList(FixedPriceList):
    """Answers price() like its parent, and adds set_price()."""

    def set_price(self, item, price):
        self._prices[item] = price


class Quote:
    """A quote given to a customer. It trusts the list's promise, so it keeps no copy of the prices."""

    def __init__(self, price_list, lines):
        self.price_list, self.lines = price_list, lines

    def total(self):
        return sum(self.price_list.price(item) * quantity for item, quantity in self.lines)


prices = EditablePriceList({"tea": 30, "toast": 45})
quote = Quote(prices, [("tea", 2), ("toast", 1)])
print("quote given to the customer:", quote.total())
prices.set_price("tea", 35)
print("the same quote, later:      ", quote.total())

# The standard library promises less: a read-only view of a dict says nothing about the dict changing.
menu = {"tea": 30}
view = MappingProxyType(menu)
menu["tea"] = 35
print("read-only view after the dict changed:", view["tea"])
try:
    view["tea"] = 40
except TypeError as error:
    print("writing through the view:", error)

Output

quote given to the customer: 105
the same quote, later:       115
read-only view after the dict changed: 35
writing through the view: 'mappingproxy' object does not support item assignment

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 price_lists.py

Each price() call answers correctly, yet a quote already given to a customer changed its total from 105 to 115. The parent’s promise was about every sequence of states, and the child’s new method made a sequence in which it is false. Liskov and Wing give the same verdict on a mutable array posing as a sequence whose users expect it never to change: it is not a subtype.

Python’s standard library avoids the trap by promising less. In collections.abc, Sequence and MutableSequence are the read-only and mutable interfaces, and MutableSequence extends Sequence. That is safe because “read-only” only means the interface has no methods for changing the sequence; it does not mean that nothing else can change it. The last lines above show the difference: a types.MappingProxyType refuses writes made through it, yet it shows every change made to the dictionary behind it. When your code needs “this never changes”, take a copy at the moment you rely on it, such as a tuple or a frozen data class, instead of trusting a read-only interface.

In a design interview

Each time you draw an inheritance arrow, say what the child promises its callers. When one child cannot keep a promise of the parent (a read-only account, a vehicle that cannot be charged, a bounded queue), split the interface or use composition, and say that you are doing it to keep substitution safe.

Python Online Compiler Add a third Account implementation in the runner and see whether it passes the contract.

Key takeaways

  • A subtype must keep every promise its parent makes, so code written for the parent cannot tell the difference; this covers duck typing and protocols as well as inheritance.
  • Subtypes may accept more and promise more, never accept less, promise less, break an invariant or raise new kinds of error; their extra methods must not break the parent’s promises about history.
  • Contract tests make LSP mechanical: write the parent’s promises once as a test mixin and run them against every implementation.
  • When a subtype cannot keep a promise, fix the hierarchy (split the interface, compose, or weaken the contract honestly), never the callers with isinstance() checks.

Exercise

Exercise · Medium · Python

Write the contract tests for a queue

A team has several queue classes, and code that is written for one of them must work with any of them. Write that promise down as checks. contract_failures(make_queue) gets a class (or any function) that makes a new, empty queue with put(item), get() and len(), and returns a list of messages, one for each rule below that those queues break. An empty list means the implementation keeps the contract:

  1. A new queue is empty: len(queue) == 0.
  2. put(item) always accepts the item, however many are already queued, and len() grows by exactly one.
  3. get() removes and returns the oldest item, and len() shrinks by one.
  4. Items leave in the order they arrived, also when puts and gets are mixed.
  5. get() on an empty queue raises IndexError, and the queue stays empty.

The sample tests run your checks against six implementations. Two keep the contract and four break it in different ways, one of them by raising an error the contract never mentions. Your function must report all four and must not crash on any of them: an unexpected exception from a queue is a broken rule, not a reason for your checks to stop.

Starter code · queue_contract.py

def contract_failures(make_queue):
    """Checks the queue contract against queues made by make_queue().

    Returns a list of messages, one for each rule of the contract that the queues break. An empty list means that
    the implementation keeps the contract.
    """
    failures = []
    # Write your checks here.
    return failures
The sample tests · test_queue_contract.py
from collections import deque

from queue_contract import contract_failures


class ListQueue:
    def __init__(self):
        self._items = []

    def put(self, item):
        self._items.append(item)

    def get(self):
        if not self._items:
            raise IndexError("get from an empty queue")
        return self._items.pop(0)

    def __len__(self):
        return len(self._items)


class DequeQueue:
    def __init__(self):
        self._items = deque()

    def put(self, item):
        self._items.append(item)

    def get(self):
        return self._items.popleft()  # raises IndexError when empty

    def __len__(self):
        return len(self._items)


class RingQueue(DequeQueue):
    """Holds at most four items and silently drops the oldest one."""

    def __init__(self):
        self._items = deque(maxlen=4)


class ShortCountQueue(ListQueue):
    """Keeps every item, but len() never reports more than four."""

    def __len__(self):
        return min(len(self._items), 4)


class QuietQueue(ListQueue):
    """Returns None from an empty queue instead of raising IndexError."""

    def get(self):
        return self._items.pop(0) if self._items else None


class CrashingQueue(ListQueue):
    """Fails in a way the contract never allows."""

    def get(self):
        raise RuntimeError("storage unavailable")


def test_list_queue_keeps_the_contract():
    """reports nothing for a correct list-based queue"""
    assert contract_failures(ListQueue) == []


def test_deque_queue_keeps_the_contract():
    """reports nothing for a correct deque-based queue"""
    assert contract_failures(DequeQueue) == []


def test_ring_queue_breaks_it():
    """catches a queue that drops items once it holds four"""
    assert contract_failures(RingQueue) != []


def test_short_count_queue_breaks_it():
    """catches a queue whose len() stops counting at four"""
    assert contract_failures(ShortCountQueue) != []


def test_quiet_queue_breaks_it():
    """catches a queue that returns None when it is empty"""
    assert contract_failures(QuietQueue) != []


def test_reports_instead_of_crashing():
    """reports a queue whose get() raises an unexpected error, and returns messages"""
    failures = contract_failures(CrashingQueue)
    assert failures != []
    assert all(isinstance(message, str) for message in failures)
A hint

Write each rule as a small function that builds a fresh queue with make_queue() and returns True when the rule holds. Then call every rule through one helper that appends a message when the rule returns False or raises an exception (except Exception as error:), so one broken rule cannot stop the checks of the others.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 Account.withdraw(amount) accepts any amount above 0 and up to the balance, lowers the balance by it and returns the new balance; a larger amount raises InsufficientFunds. Which override keeps the Liskov Substitution Principle?

    Choose one answer.

    Show the answer to question 1

    Answer: An account whose withdraw() also accepts an amount of 0 and returns the balance unchanged

    Accepting a call the parent did not allow (a weaker precondition) cannot surprise code written for the parent: every call that worked before still works and returns what it did. Refusing calls the parent accepts, returning less than it promised and raising a new kind of error all break code that relies on the parent's contract. An overdraft account would break it too, because the parent promises to refuse amounts above the balance.

  2. Question 2 of 6 Notifier.send() promises to deliver any text and return a delivery id. What is wrong with SmsNotifier?

    Read the code, then choose one answer.

    class SmsNotifier(Notifier):
        def send(self, user, text):
            if len(text) > 160:
                raise ValueError("an SMS holds at most 160 characters")
            return self.gateway.deliver(user.phone, text)
    Show the answer to question 2

    Answer: It strengthens the precondition and raises an error the parent never mentions, so callers written for Notifier can crash

    Code written for Notifier may pass any text, so a subtype that refuses long texts accepts less than its parent. The usual fixes are to keep the promise (split a long text into several SMS messages) or to make the limit part of the parent's contract, so that every caller knows about it.

  3. Question 3 of 6 EditablePriceList inherits from FixedPriceList, which promises that prices never change. What does price_lists.py print?

    What does this program print? Choose one answer.

    """A promise about history: "prices never change". A subclass that adds set_price() breaks it."""
    from types import MappingProxyType
    
    
    class FixedPriceList:
        """Prices for one season. Promise: once the list exists, no price in it changes."""
    
        def __init__(self, prices):
            self._prices = dict(prices)
    
        def price(self, item):
            return self._prices[item]
    
    
    class EditablePriceList(FixedPriceList):
        """Answers price() like its parent, and adds set_price()."""
    
        def set_price(self, item, price):
            self._prices[item] = price
    
    
    class Quote:
        """A quote given to a customer. It trusts the list's promise, so it keeps no copy of the prices."""
    
        def __init__(self, price_list, lines):
            self.price_list, self.lines = price_list, lines
    
        def total(self):
            return sum(self.price_list.price(item) * quantity for item, quantity in self.lines)
    
    
    prices = EditablePriceList({"tea": 30, "toast": 45})
    quote = Quote(prices, [("tea", 2), ("toast", 1)])
    print("quote given to the customer:", quote.total())
    prices.set_price("tea", 35)
    print("the same quote, later:      ", quote.total())
    
    # The standard library promises less: a read-only view of a dict says nothing about the dict changing.
    menu = {"tea": 30}
    view = MappingProxyType(menu)
    menu["tea"] = 35
    print("read-only view after the dict changed:", view["tea"])
    try:
        view["tea"] = 40
    except TypeError as error:
        print("writing through the view:", error)
    Show the answer to question 3

    Answer: it prints

    quote given to the customer: 105
    the same quote, later:       115
    read-only view after the dict changed: 35
    writing through the view: 'mappingproxy' object does not support item assignment

    The quote keeps no copy of the prices, so set_price() changes its total from 105 to 115: the subtype's extra method broke the parent's promise about history. The mapping proxy refuses writes made through it, but it is a view, so it shows the change made to the dictionary behind it.

  4. Question 4 of 6 Which of these are signs that a class is not a real subtype of its parent?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • An override raises NotImplementedError in a class that is meant to be used
    • Callers check isinstance() before calling one of the parent's methods
    • An override does nothing, so callers believe something happened

    Type checks in callers, "not supported" errors and silent overrides all mean the child cannot do what the parent promises. Adding unrelated methods is fine, and a more specific return type keeps the parent's promise, which is the covariant result rule.

  5. Question 5 of 6 What does a contract test suite for a parent type check?

    Choose one answer.

    Show the answer to question 5

    Answer: The parent's documented promises, run against every implementation

    The contract is written once, from the parent's documented behaviour, and every implementation runs it through its own factory method. A subtype that fails a test its parent passes is not substitutable. Implementation details stay out of it, because other implementations may store data differently and still keep every promise.

  6. Question 6 of 6 After the split, code that takes money out asks for a WithdrawableAccount. Why is that better than checking isinstance(account, FixedDeposit) before each withdrawal?

    Choose one answer.

    Show the answer to question 6

    Answer: Only types that promise withdrawals can be passed, so no caller needs to know about special cases

    The type says what the caller needs, and a type checker can reject a fixed deposit before the program runs. With isinstance() checks, every caller must know every account type that cannot withdraw, and each new such type means finding and editing all of those checks.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.