Create your own
Lesson illustration

Defining Clear Class and Interface Contracts

Good design needs more than well-chosen classes and relationships. Once Employee delegates to a pay policy, or a parking service delegates to a pricing policy, both sides need an explicit answer to a practical question: what may I assume, and what must I guarantee?

In the previous lesson, composition separated independently changing behaviors, while narrow inheritance was reserved for genuine subtypes. This lesson makes those boundaries reliable. You will learn to specify a contract for a public class or interface: valid inputs, promised outputs and state changes, error behavior, side effects, and invariants. You will also choose appropriately between Python Protocol and ABC when expressing that contract.


A contract is the boundary, not merely a type signature

A method signature communicates only a fraction of its contract:

def quote_fee(duration: timedelta, vehicle_type: VehicleType) -> int:
    ...

It tells us parameter and return types, but leaves important questions unanswered:

  • Is a negative duration valid?
  • Is the result in cents, dollars, or some money object?
  • Can the result be negative?
  • Is a partial hour rounded up or down?
  • Does the method mutate an object, write to a database, or call an external service?
  • What happens for malformed input or unavailable data?
  • May callers depend on the same inputs producing the same result?

A contract is the complete externally visible promise. It separates responsibilities:

  • The client must satisfy stated preconditions.
  • The provider must deliver stated postconditions whenever those preconditions hold.
  • The object must preserve its invariants across all successful public operations.
The Design by Contract diagram shows that a software component accepts inputs subject to preconditions, produces outputs subject to postconditions, and must make errors, exceptions, and side effects explicit parts of its behavior.

The diagram is useful because it corrects a common LLD mistake: documenting only the happy-path input and output. A method that changes state, sends a message, or throws a domain-relevant exception has a larger contract than its return type suggests.

Design-by-Contract programming with Python

Watch “Design-by-Contract programming with Python” by Dev Internals for a concise visual treatment of the three core parts of a contract: preconditions, postconditions, and invariants.

Watch the core model to establish the definitions. Later, watch class invariants, which shows why an invariant protects an object from invalid state changes throughout its lifetime. Focus on the difference between a condition for one call and a condition that must hold for the whole object lifecycle.

The three core promises

Contract elementMeaningExample for a parking-fee quote
PreconditionMust be true before the call beginsduration is not negative
PostconditionMust be true after a successful returnResult is a non-negative integer amount in cents
InvariantMust remain true for every valid state of an objectA closed ticket has an exit time; an open ticket does not

A contract can also state two more things that are especially important in backend code:

  • Failure semantics: which exceptions or error results can occur, and whether state changes happen before failure.
  • Side effects: whether the operation persists data, emits an event, sends a request, or is otherwise externally observable.

For example, “raises ValueError for a negative duration without mutating state” is much stronger and safer than “may fail for bad input.”


Write behavior declaratively, not as an implementation story

A clear contract says what clients receive, not how the method achieves it.

Consider these alternatives:

# Weak and operational
def find_available_spot(vehicle: Vehicle) -> ParkingSpot:
    """Loop through spots from the lowest floor and return the first compatible one."""
# Declarative
def find_available_spot(vehicle: Vehicle) -> ParkingSpot:
    """Return a currently available spot compatible with vehicle.

    Raises NoCompatibleSpotError if no such spot exists.
    Does not reserve the returned spot.
    """

The second version is better because it does not accidentally promise that the lowest-floor spot will be selected. The implementation can later change to optimize walking distance, balance occupancy, or use a cache without breaking callers.

This does not mean a contract should be vague. If product behavior requires “the closest compatible spot,” that is an observable requirement and belongs in the contract. The principle is to avoid exposing incidental internal choices.

Reading 7: Designing Specifications

Read the selected parts of MIT OpenCourseWare’s “Designing Specifications.” It gives a rigorous foundation for contracts: describe outcomes declaratively, keep interfaces coherent, and make promises that remain safe to implement and evolve.

In “Declarative vs. operational specs,” read the comparison and connect it to the parking-spot example above. In “Stronger vs. weaker specs,” read the substitution rules: a replacement is safer when it demands less from callers and promises more to them. Then read the full “Designing good specifications” section, especially the design guidance. Finish with “Precondition or postcondition?” and the validation trade off. In “About access control,” note that public methods form the advertised contract, while helpers should remain implementation details.

Contract strength and substitutability

This connects directly to the substitution test from the previous lesson. If HourlyPricing and FlatRatePricing both implement PricingPolicy, either must be usable where the policy interface is expected.

A concrete implementation may safely:

  • accept more inputs than the interface requires;
  • guarantee more than the interface promises.

It must not:

  • require extra conditions that interface clients were never told to meet;
  • return less useful results;
  • introduce undocumented side effects or failures.

For instance, if PricingPolicy.quote() promises to accept every non-negative duration, an HourlyPricing implementation cannot reject durations shorter than one hour. That would strengthen the precondition and break substitutability.


A practical contract: a pricing policy

Suppose an LLD prompt requires a parking lot with replaceable fee rules. The service needs to calculate a fee, but it should not know whether the rule is hourly, flat-rate, or event-based.

Start by specifying the client-visible behavior in plain language:

quote(duration, vehicle_type) contract
Inputs: duration is non-negative; vehicle_type is a supported category.
Result: returns a non-negative fee in integer cents.
State and side effects: does not mutate the ticket or persist anything.
Failure: raises ValueError for a negative duration or unsupported vehicle type.
Deliberately unspecified: the pricing formula is selected by the concrete policy.

This is narrow but useful. A caller knows the unit, valid inputs, failure mode, and absence of side effects. It does not know, and should not need to know, the formula.

Here is one way to implement that contract with an abstract base class:

from abc import ABC, abstractmethod
from datetime import timedelta
from enum import Enum
from math import ceil


class VehicleType(Enum):
    MOTORCYCLE = "motorcycle"
    CAR = "car"
    TRUCK = "truck"


class PricingPolicy(ABC):
    """Calculates a parking fee without changing parking-lot state."""

    def quote(self, duration: timedelta, vehicle_type: VehicleType) -> int:
        """Return a non-negative fee in cents.

        Raises:
            ValueError: if duration is negative.
        """
        if duration.total_seconds() < 0:
            raise ValueError("duration cannot be negative")

        fee_cents = self._fee_cents(duration, vehicle_type)

        # This is a programmer-error check: subclasses must preserve
        # the public contract.
        if fee_cents < 0:
            raise AssertionError("pricing policy returned a negative fee")

        return fee_cents

    @abstractmethod
    def _fee_cents(
        self,
        duration: timedelta,
        vehicle_type: VehicleType,
    ) -> int:
        """Calculate the fee for already validated inputs."""
        raise NotImplementedError
class HourlyPricing(PricingPolicy):
    def __init__(self, hourly_rate_cents: int) -> None:
        if hourly_rate_cents < 0:
            raise ValueError("hourly rate cannot be negative")
        self._hourly_rate_cents = hourly_rate_cents

    def _fee_cents(
        self,
        duration: timedelta,
        vehicle_type: VehicleType,
    ) -> int:
        started_hours = ceil(duration.total_seconds() / 3600)
        return started_hours * self._hourly_rate_cents

Notice the division of responsibility:

  • PricingPolicy.quote() owns the universal public contract: no negative duration and no negative result.
  • HourlyPricing owns a rule specific to its policy: each started hour is charged.
  • The caller depends on quote(), not _fee_cents(). The leading underscore signals that the latter is an internal extension hook rather than a public service.

The abstract base class is not what makes the contract good. The semantic promises do that. @abstractmethod only prevents an incomplete subclass from being instantiated; it cannot verify that a subclass returns a fee in cents, avoids I/O, or produces a non-negative result.


Preconditions, validation, and exceptions

A precondition does not mean “ignore bad input.” It means the contract must state how invalid input is handled.

For a public Python API, validating inexpensive input and failing close to the source is usually preferable:

if duration.total_seconds() < 0:
    raise ValueError("duration cannot be negative")

This turns a caller mistake into a predictable, local failure. It is much better than allowing a negative fee to flow through invoices, payment records, and reports.

There are cases where a checked precondition is impractical. Binary search is a classic example: its input must be sorted, but scanning the entire input to verify sortedness would remove the performance benefit of binary search. In that case, the contract should document the sorted-input precondition clearly.

For interview answers, state the engineering judgment:

“This is a public method and checking the value is constant-time, so I will validate it and raise a specific exception. For an expensive global property, I would document it as a precondition rather than re-check it on every call.”

Avoid vague failures such as “raises an exception on error.” A useful contract names the expected category and specifies state:

def close(self, exit_time: datetime) -> int:
    """Close an open ticket and return its final fee in cents.

    Raises:
        TicketAlreadyClosedError: if this ticket is already closed.
        ValueError: if exit_time precedes entry_time.

    Postconditions:
        On success, this ticket is closed and has a final fee.
        On failure, this ticket remains unchanged.
    """

The final sentence is vital. A caller that retries after an error needs to know whether the first attempt changed anything.


Interface contracts in Python: Protocol or ABC?

Python gives you two common tools for expressing interfaces. They solve related but distinct design problems.

Use a Protocol for a capability

A Protocol describes the minimum shape a collaborator must have. It fits a consumer that needs behavior but should not require a particular inheritance hierarchy.

from datetime import timedelta
from typing import Protocol


class FeeQuoter(Protocol):
    """Capability for producing non-negative parking fees in cents."""

    def quote(self, duration: timedelta, vehicle_type: VehicleType) -> int:
        """Return a fee in cents for a non-negative duration."""
        ...

Now a parking service can accept any compatible object:

class ParkingService:
    def __init__(self, pricing: FeeQuoter) -> None:
        self._pricing = pricing

    def calculate_exit_fee(
        self,
        duration: timedelta,
        vehicle_type: VehicleType,
    ) -> int:
        return self._pricing.quote(duration, vehicle_type)

HourlyPricing does not need to inherit from FeeQuoter. A static type checker can verify that it has a compatible quote() method.

This is particularly useful for dependency injection. A test fake, an adapter around an external library, or a future promotional-pricing component can satisfy the same narrow contract without being forced into your class hierarchy.

Use an ABC for a controlled family with shared logic

Choose an abstract base class when you own the implementations and want both:

  1. runtime prevention of incomplete subclasses; and
  2. shared state, helpers, or a partial algorithm.

PricingPolicy above is a reasonable ABC because it owns shared validation and the public quote() workflow, while letting subclasses supply only the fee formula.

The standard library’s ABC and @abstractmethod enforce that concrete subclasses implement required abstract members before they can be instantiated.

Protocols vs ABCs in Python - When to Use Which One?

Watch “Protocols vs ABCs in Python - When to Use Which One?” by ArjanCodes to see the practical difference between inheritance-based abstract base classes and structural Protocols.

Watch the ABC example to see abstract methods combined with shared concrete logic. Then watch the Protocol example for structural typing without explicit inheritance. Watch the runtime caveat: runtime-checkable Protocols perform only shallow checks and do not validate complete method signatures. Finish with the selection guidance.

A concise decision rule:

NeedPrefer
A function needs one small capability from arbitrary objectsProtocol
A third-party or test-double object should be usable without modifying itProtocol
You need shared implementation, state, or a template workflowABC
You want incomplete subclasses to fail at instantiationABC
You only need polymorphism among a few classes you controlEither; choose the simpler boundary

Do not use a Protocol merely as a runtime validator. Even with @runtime_checkable, Python can confirm that an attribute exists, but it does not fully validate parameter types, return types, or semantics. The meaningful enforcement remains tests, validation, and careful contract design.


Class contracts: invariants and a deliberately small public surface

A method contract describes one operation. A class contract adds promises about valid object state and its lifecycle.

Consider a ParkingTicket:

class ParkingTicket:
    """Represents one vehicle's visit to a parking lot.

    Invariants:
        - entry_time is always present.
        - an open ticket has no exit_time or final_fee_cents.
        - a closed ticket has both exit_time and final_fee_cents.
        - final_fee_cents, when present, is non-negative.
    """

These invariants guide implementation decisions:

  • The constructor establishes them.
  • Every public mutating method preserves them.
  • Callers cannot directly assign arbitrary values to internal fields.
  • A second close() call should be explicitly rejected or made idempotent; it must not silently create contradictory state.

This is why public methods should be chosen carefully. A public method is an API promise that other code may begin to rely on. Private helpers are free to change; public operations are expensive to change because their contract has clients.

In an LLD interview, a reliable contract-writing pass takes only a few minutes:

  1. Identify each collaborator boundary, such as ParkingService and FeeQuoter.
  2. State inputs and units precisely.
  3. Define the successful result and any state mutation.
  4. Name predictable failures and the state after failure.
  5. State side effects: persistence, event publication, external call, or none.
  6. Add object invariants for stateful entities.
  7. Check substitution: can every implementation preserve the interface promises?

This is enough detail to show sound engineering judgment without writing production-scale documentation for every getter.


Key takeaways

A class or interface contract makes collaboration dependable:

  • A type signature is necessary but incomplete; it does not specify units, state changes, side effects, or failure behavior.
  • Write contracts declaratively: promise observable outcomes, not internal algorithms.
  • Define preconditions, postconditions, and invariants. For public methods, validate inexpensive invalid inputs and fail predictably.
  • State whether an operation changes state or has external side effects, especially on failure.
  • Concrete implementations must not demand more from callers or promise less than their interface.
  • Use a Protocol for a narrow capability needed from potentially unrelated objects. Use an ABC when a controlled family needs shared logic or enforced abstract methods.
  • Keep the public surface small: every public method becomes part of the class’s long-term promise.

Next, you will translate these contracts and relationships into a UML class diagram for a small object-oriented design. The operations and interfaces you draw will be much clearer because you now know what each boundary actually guarantees.

Can't find a good explanation? Sign up and we'll make it for you

Sign up