skip to content

In Eiffel a subtype literally cannot tighten an inherited precondition, while in Python or Kotlin an override can silently demand more than its parent did. Explain the mechanism behind that difference and what each approach costs.

level: seniorimportance: should knowfreq 36%

answer

  1. Preconditions weaken, postconditions strengthen
  2. Eiffel: require else = OR, ensure then = AND
  3. Ada: Pre'Class inherited and OR-ed; plain Pre is not inherited
  4. D in/out combine the same way; base in passing wins
  5. Kotlin/Python asserts replace, never combine — silent tightening

basics

~20 s

Eiffel's require else is OR-ed with the inherited clause and ensure then is AND-ed with it, so only weakening and strengthening are expressible. Ada 2012's Pre'Class and D's in/out combine the same way. Kotlin and Python assertions merely replace the parent's, so tightening passes unnoticed.

solid answer

~60 s

The rule that preconditions may only weaken and postconditions only strengthen is the Liskov Substitution Principle applied to specifications; the interesting difference is enforcement **by construction** versus **not at all**. - **Eiffel** — a redefined routine may write `require else` and `ensure then`. The effective precondition is the parent's OR the new clause, the effective postcondition is the parent's AND the new clause. Tightening is not forbidden by a checker; it is unwritable. - **Ada 2012** — `Pre'Class` is OR-ed and `Post'Class` AND-ed down the class-wide hierarchy, while a plain `Pre` applies only to that one subprogram and is *not* inherited. Two spellings, two different semantics on the same declaration. - **D** — inherited `in`/`out` blocks combine the same way: an overriding `in` block that throws still lets the call through if the parent's accepted it. - **Kotlin, Python, Java** — `require(...)`, `assert`, or a hand-written guard in the override simply replaces the parent's. Nothing relates the two, so a stricter override compiles and ships. The cost of OR-ing is real: `require else True` weakens a contract to nothing while looking rigorous.

code

eiffel · 21 lines
eiffel
class WITHDRAWAL_ACCOUNT
feature
    withdraw (amount: INTEGER)
        require
            positive: amount > 0
            covered: amount <= balance
        deferred
    end
end

class OVERDRAFT_ACCOUNT
inherit WITHDRAWAL_ACCOUNT redefine withdraw end
feature
    withdraw (amount: INTEGER)
        require else
            within_overdraft: amount <= balance + overdraft_limit
        do ... end
end

-- effective precondition: (amount > 0 and amount <= balance)
--                        or amount <= balance + overdraft_limit  -- strictly weaker

go deeper

for a junior

Know the direction: a subtype may accept more, and must promise at least as much. One concrete keyword pair (require else / ensure then) is enough.

for a middle

Explain why the direction follows from clients holding parent-typed references, and show that mainstream languages do not enforce it while Eiffel does.

for a senior

Contrast enforcement styles: unforgeable by construction (Eiffel, D, Ada's 'Class aspects) versus unenforced statements in a body (Kotlin, Python), and say what review or testing has to catch in the latter.

for a principal

Discuss the residual risk after mechanisation — vacuous weakening, unspecified behaviour outside the clauses — and when to forbid subtype-specific contracts altogether in favour of composition or a closed set of cases.

## The specification, not the signature Most languages check that an override is *type*-compatible: the parameter types line up, the return type is compatible, visibility is not reduced. None of that says anything about the *conditions* the routine imposes. A subtype can keep a perfectly legal signature and still demand that the argument now be positive, or stop guaranteeing something the parent promised. That is a substitutability defect, and it is the part of the story that Design by Contract mechanises. (The principle itself belongs to the Liskov Substitution Principle; here the question is purely how languages implement it.) The two directions are: - **Preconditions may only be weakened.** A subtype may accept more than its parent, never less, because client code holding a parent-typed reference was written against the parent's requirement. - **Postconditions and invariants may only be strengthened.** A subtype may promise more, never less, because clients were told to expect at least the parent's guarantee. ## Enforcement by construction: Eiffel Eiffel does not add a checker that rejects a tightened clause. It removes the ability to write one. A redefined routine may not write a bare `require`; it writes `require else <extra>`, and the runtime evaluates `parent_clause or extra`. Symmetrically `ensure then <extra>` evaluates `parent_clause and extra`. Because the parent's clause is always a disjunct of the effective precondition, the effective precondition can only get looser as you descend; because the parent's postcondition is always a conjunct, the guarantee can only get tighter. A routine that omits the clauses entirely inherits them unchanged. This is the key insight to state in an interview: the language makes the illegal state unrepresentable at the *specification* level, exactly as a newtype does at the value level. There is no rule to remember, no lint to run and no way for a careless override to slip through review. ## The same idea with different scoping: Ada 2012 Ada 2012 added `Pre` and `Post` aspects, and then had to answer a question Eiffel never faced: Ada separates a subprogram from the tagged type it dispatches on. The answer is two spellings. `Pre'Class` and `Post'Class` are the *class-wide* contracts: they are inherited by every overriding subprogram, with `Pre'Class` combined by disjunction and `Post'Class` by conjunction, exactly as in Eiffel. A plain `Pre` is *specific*: it applies to that one body only, is not inherited, and is checked in addition. So in Ada the very same source line means one thing with the `'Class` suffix and something incompatible without it, and only the class-wide form participates in substitutability. Candidates who have only seen Eiffel are usually surprised by this. ## The same rules, no dispatch story: D D builds contracts into the language with `in`, `out` and `invariant` blocks. Inherited `in` contracts are combined disjunctively: if the base class's `in` block passes, the call proceeds even when the derived class's `in` block throws. `out` contracts are combined conjunctively — all of them must hold. Again the combination rule, not a diagnostic, does the work. ## No mechanism at all: Kotlin, Python, Java In Kotlin, `require(...)` in an override is an ordinary statement in an ordinary function body. Nothing links it to whatever the parent's body did. The same is true of Python's bare `assert`, of Java's `assert` and of hand-written `if (...) throw` guards. An override can therefore raise the bar in silence, and the failure will appear in production only when a client that had a parent-typed reference happens to pass a value the parent accepted. Python's third-party `icontract` restores the model deliberately — its decorators track inherited preconditions and weaken them by disjunction — which is itself evidence that the language does not. The practical consequence for a reviewer: in these languages, a tightened precondition in an override is a defect that only code review or a property test comparing subtype against supertype behaviour will catch. That is why the substitutability rule is taught as a principle in mainstream languages and as a keyword in Eiffel. ## What OR-ing does not buy you Mechanised weakening prevents one failure mode and creates another. Because the effective precondition is a disjunction, a subtype can write `require else True` and thereby accept every input — including inputs it has no sensible behaviour for. The contract is now technically weaker (legal) and practically useless, and the burden moves to the postcondition, which must still be at least as strong as the parent's for those newly accepted inputs. A subtype that widens acceptance without being able to deliver the inherited guarantee has broken the contract on the other side. So the honest summary is: Eiffel and Ada make the *direction* of change unmistakable and unforgeable, but no language can stop you from specifying something vacuous.

  • Ada 2012 offers both Pre and Pre'Class on the same subprogram. Why does only one of them participate in substitutability?
    Pre'Class is the class-wide contract: it is inherited by every overriding subprogram and combined by disjunction down the hierarchy, so it constrains dispatching calls made through a class-wide view. A plain Pre is specific to one body, is not inherited, and is checked in addition to any inherited class-wide contract. A designer who writes the strict condition as Pre rather than Pre'Class gets a check that no subtype is obliged to honour, which is exactly the substitutability hole the aspect was meant to close.
  • If Eiffel makes tightening unwritable, can a subtype still break its contract?
    Yes, in two ways. It can weaken the precondition to something vacuous with require else True while remaining unable to honour the inherited postcondition on the newly accepted inputs, which breaks the guarantee side. And it can satisfy the letter of both clauses while changing behaviour the specification never captured — contracts constrain what is written down, and no realistic contract fully pins down behaviour.

saying these in an interview costs you the question

  • Saying the weaken/strengthen rule is checked by the compiler in Java, Kotlin or Python — nothing checks it there
  • Believing Eiffel rejects a stricter override at compile time, rather than making it unexpressible via OR-ing
  • Treating Ada's plain Pre as inherited by overrides
  • Assuming a legal, type-compatible override is automatically a legal specification
  • Concluding that mechanised weakening makes contracts safe, ignoring the vacuous 'require else True' case

context