CSC148H1/Unit 2
Unit 2 · Introduction to Computer Science

Object-Oriented Design

CSC108H1 showed you how to write a class. This unit is about designing one: what the attributes mean, what must always be true of them, and how to state that so the invariant survives every method you add.

By the end you can
  • Write a representation invariant and check methods preserve it.
  • Document attributes with types and meanings.
  • Decide what belongs in the public interface.
  • Implement __eq__, __str__ and __len__ where they earn their place.
Why it matters & where it comes from
Why it exists

A class is a promise that its data stays in a sensible state. Writing that promise down turns a vague intention into something you can check every method against — and most bugs in this course are a method quietly breaking one.

In the real world

A bank account whose balance and transaction history can disagree is a serious bug. Stating "balance equals the sum of the transactions" as an invariant makes the requirement explicit and testable rather than assumed.

Story & open questions

Design by contract came from Bertrand Meyer's Eiffel language in the 1980s, where preconditions, postconditions and invariants were checked by the compiler. Python leaves them as documentation, so the discipline has to be yours.

Documenting the representation

Rule What a class docstring carries
What the class represents in one line.
Attributes, each with its type and what it means.
Representation invariants — conditions on the attributes that must hold at all times.
Example A fully documented class
python
class Course:
    """A course with a fixed capacity and a list of enrolled students.

    Attributes:
        code: the course code, such as 'CSC148H1'
        capacity: the maximum number of students, a positive int
        students: the names of enrolled students

    Representation invariants:
        - len(self.students) <= self.capacity
        - there are no duplicates in self.students
        - self.capacity > 0
    """
    code: str
    capacity: int
    students: list

    def __init__(self, code: str, capacity: int) -> None:
        """Initialize an empty course with the given code and capacity.

        Preconditions: capacity > 0
        """
        self.code = code
        self.capacity = capacity
        self.students = []
The invariants are the interesting part. They say what "valid" means for this class, and every method now has something concrete to be checked against.
Note the constructor establishes them: an empty list has no duplicates and its length is at most any positive capacity.
Rule Every method must preserve the invariant

If the invariant holds when a method starts, it must hold when the method ends. That is the obligation, and it is what makes the class trustworthy from the outside.

Example A method that respects it
python
def enrol(self, name: str) -> bool:
    """Add name to this course and return whether it succeeded.

    Fails if the course is full or name is already enrolled.

    >>> c = Course('CSC148H1', 2)
    >>> c.enrol('Ada')
    True
    >>> c.enrol('Ada')
    False
    """
    if len(self.students) >= self.capacity:
        return False
    if name in self.students:
        return False
    self.students.append(name)
    return True
The two guards correspond exactly to the two invariants. That is not a coincidence — writing the invariant first tells you which checks the method needs.
Returning a boolean rather than raising is a design choice. Either works; the docstring records which was chosen.

Public interface

Rule Expose behaviour, not data

Ask what callers need to do, not what they need to see. A method can enforce the invariant; a bare attribute cannot.

Example Where the invariant leaks
python
c = Course('CSC148H1', 2)
c.students.append('Ada')
c.students.append('Ada')     # duplicate - invariant broken
c.students.append('Alan')    # over capacity - invariant broken
Because students is a public list, a client can bypass enrol entirely and leave the object in a state the class claims is impossible.
Making it _students and exposing a class_list() method that returns a copy closes the hole. Returning the list itself would reopen it, since the caller could mutate the copy's original.
Example Returning a copy
python
def class_list(self) -> list:
    """Return a new list of the names enrolled in this course.

    Mutating the returned list does not affect this course.
    """
    return self._students[:]

The docstring states the guarantee. Without it, a caller cannot tell whether mutating the result is safe.

Special methods worth defining

Rule Define them when the concept applies
__str__ — always. Debugging without it means reading memory addresses.
__eq__ — when two distinct objects can be meaningfully equal.
__len__ — when the object has an obvious size.
__contains__ — when membership makes sense, enabling in.
Example Adding them
python
def __len__(self) -> int:
    """Return the number of students enrolled in this course.

    >>> c = Course('CSC148H1', 2)
    >>> c.enrol('Ada')
    True
    >>> len(c)
    1
    """
    return len(self._students)

def __contains__(self, name: str) -> bool:
    """Return whether name is enrolled in this course.

    >>> c = Course('CSC148H1', 2)
    >>> c.enrol('Ada')
    True
    >>> 'Ada' in c
    True
    """
    return name in self._students

These let client code read naturally — len(c) and 'Ada' in c — while keeping the list private.

__eq__ has consequences

Defining __eq__ without __hash__ makes instances unhashable, so they can no longer go in a set or serve as dictionary keys. Python does this deliberately: two objects that compare equal must hash equally, and it will not guess how.

Common pitfalls

• Stating an invariant and then writing a method that breaks it.
• Exposing a mutable attribute and expecting clients not to touch it.
• Returning an internal list directly from a getter.
• Documenting attribute types but not their meanings. "students: list" says nothing about what is in it.
• Defining __eq__ and then wondering why instances cannot go in a set.

Practice

Write out each answer before opening it. Original exercises written for this guide in the style of the course's weekly work.

Practice 1 · Invariant

Write a representation invariant for a Rectangle class with width and height.

Answer
self.width > 0 and self.height > 0. Whether zero is allowed is a design decision, but it must be stated either way.
Practice 2 · Which method breaks it

A Playlist has invariant "no duplicate songs". Which method is most likely to violate it?

Answer
Whichever adds a song. It needs a membership check before appending, and the invariant is what tells you so.
Practice 3 · Getter safety

Why return self._items[:] rather than self._items?

Answer
Returning the list itself hands the caller a reference to the internal state, which they can mutate — breaking any invariant the class maintains. A copy is safe.
Practice 4 · Equality

Should two Course objects with the same code be equal?

Answer
A design decision. If the code identifies the course, yes. If two sections can share a code, no. Document whichever you choose.
Practice 5 · Write the class

Design a BankAccount with a balance that cannot go negative.

Solution
python
class BankAccount:
    """An account with a non-negative balance.

    Attributes:
        owner: the account holder's name

    Representation invariants:
        - self._balance >= 0
    """

    def __init__(self, owner: str) -> None:
        """Initialize an account for owner with a zero balance."""
        self.owner = owner
        self._balance = 0

    def withdraw(self, amount: int) -> bool:
        """Withdraw amount and return whether it succeeded.

        Fails if amount exceeds the balance.

        Preconditions: amount > 0
        """
        if amount > self._balance:
            return False
        self._balance = self._balance - amount
        return True
The balance is private precisely because a public one could be set negative directly.
Practice 6 · Which specials

Which special methods suit a Deck of cards?

Answer
__len__ for the count, __contains__ for whether a card is present, __str__ for display. __eq__ is doubtful — two decks with the same cards in a different order probably are not the same deck.
Practice 7 · Hashability

You define __eq__ and then cannot put instances in a set. Why?

Answer
Defining __eq__ sets __hash__ to None. Objects that compare equal must hash equally, and Python refuses to assume how. Define __hash__ yourself if the objects are immutable.
Practice 8 · Interface design

A class stores a sorted list. Should sort() be public?

Answer
No. If sortedness is a representation invariant, the class must maintain it itself — every insert keeps it sorted. Exposing sort() implies the list might be unsorted, which contradicts the invariant.

Expanded topic checklist

Course coverage. This unit is part of the complete CSC148H1 topic map. It should cover the following items before you move on:

  • Class documentation. Purpose, attributes with meanings, representation invariants.
  • Invariant preservation. Checking every method against the stated conditions.
  • Encapsulation. Private attributes, safe getters, and why exposing mutable state leaks the invariant.
  • Special methods. Which to define and the hashability consequence of __eq__.
Textbook/source alignment

The calendar names "encapsulation and information-hiding" and "object-oriented programming". Representation invariants are marked on assignments; check Quercus for the exact required format.

Core idea map

The invariant defines what a valid object is. Encapsulation protects it from outside interference. Methods are obliged to preserve it. Every class in the rest of the course is written this way.

Study routine. After writing any method, reread the invariant and ask whether this method could break it. If it could, add the guard. That habit catches most bugs before the code ever runs.

Deep practice extensions

Use these after the main practice set. They push on definitions and edge cases rather than routine calculation.

Extension 1 · Invariant

What is a representation invariant?

Guide
A condition on the attributes that must hold at all times, before and after every method.
Extension 2 · Obligation

What does every method owe the invariant?

Guide
To leave it true. It may break it temporarily inside the body, but not on exit.
Extension 3 · Getter risk

Why is returning an internal list dangerous?

Guide
The caller gets a reference to the real object and can mutate it, bypassing every check the class performs.
Extension 4 · Equality cost

What does defining __eq__ break?

Guide
Hashability. Instances can no longer go in sets or serve as dictionary keys unless you also define __hash__.