Representation Invariants
Introduction to Software Engineering (CSSE 1001)
Preliminaries
What are representation invariants?
Representation invariants are conditions or properties that must remain true about the internal state of an object throughout its lifetime.
Why do they matter?
Representation invariants can:
- prevent bugs and inconsistent behaviour from triggering
- simplify the implementation of methods (can assume valid state),
- make code more maintainable and robust,
- help detect errors early, and
- serve as documentation for developers.
Examples of representation invariants
- A stack’s size must never be negative.
- A list’s length must equal the number of elements it contains.
- All elements in a set must be unique.
- A fraction’s denominator cannot be zero.
- A date that must follow calendar rules (e.g., no 30 Feb).
- For free/basic X accounts, the standard limit is 280 characters per tweet.
Writing Representation Invariants
Clearly state invariants in docstrings. We document representation invariants in the docstring of a class, underneath its attributes.
class Fraction():
"""Represents a mathematical fraction.
Representation Invariants:
- denominator != 0
- if fraction is zero, it is represented as 0/1
- if fraction is negative, numerator is negative
"""Enforcing Representation Invariants
Assertions
Directly check conditions in the initializer and setter methods with assert:
assert <statement that should be true>, "Error message if not True"For example:
class Fraction():
def __init__(self, numer: int, denom: int) -> None:
assert denom != 0, "Denominator cannot be zero."
assert isinstance(numer, int), "Numerator must be an integer."
assert isinstance(denom, int), "Denominator must be an integer."
# Ensure denominator is positive
if denom < 0:
numer, denom = -numer, -denom
self._numer = numer
self._denom = denom
# Special case for zero
if self._numer == 0:
assert self._denom == 1, "Zero must be 0/1"Private attributes and encapsulation
Use private (
_variable) names to discourage direct accessProvide public methods that maintain invariants
Never expose mutable objects directly
Helper methods
Define a private method (e.g., _check_invariants()) that validates the object’s state and call it after any state changes.
- state change → _check_invariants() → valid
When to call _check_invariants()
At the end of
__init__At the end of methods that modify object state
At the beginning of methods that rely on invariants being true
Example
class Fraction():
def __init__(self, numer: int, denom: int) -> None:
self._numer = numer
self._denom = denom
self._check_invariants()
def _check_invariants(self) -> None:
"""Verify that representation invariants hold."""
assert isinstance(self._numer, int), "Numerator must be an integer."
assert isinstance(self._denom, int), "Denominator must be an integer."
assert self._denom != 0, "Denominator cannot be zero."
assert self._denom > 0, "Denominator must be positive."
if self._numer == 0:
assert self._denom == 1, "Zero must be 0/1"Best practices
Document invariants in docstrings
Centralise invariant checking in a single method
Do not expose methods that could violate invariants
Create comprehensive test cases focusing on edge cases
Python’s type hints can express some invariants
Balance strictness with practicality
Exercise
You have joined a start‑up that records quick test flights for hobby drones. Write a minimal DroneFlight class that always keeps its state valid by enforcing these representation invariants:
Drone ID – exactly six alphanumeric characters (e.g., “A1B2C3”)
Altitude – must stay within 0 m – 120 m (the CASA legal ceiling for recreational drones)
Battery – an integer percentage in the range 0 – 100
DroneFlight class — solution
class DroneFlight:
"""
Model a quick test‑flight for a small hobby drone.
Attributes:
_id (str): 6‑character alphanumeric flight identifier.
_altitude (float): Current altitude in metres above launch point.
_battery (int): Remaining battery charge as a percentage (0–100).
Representation invariants:
- _id is a 6‑character alphanumeric string
- 0 ≤ _altitude ≤ 120
- 0 ≤ _battery ≤ 100
"""
_ID_LEN = 6 # One place to change if the rule changes
_MAX_ALT = 120.0 # metres (CASA limit)
def __init__(self, flight_id: str) -> None:
self._id = flight_id
self._altitude = 0.0
self._battery = 100
self._check_invariants()
def _check_invariants(self) -> None:
assert (
isinstance(self._id, str)
and self._id.isalnum()
and len(self._id) == self._ID_LEN
), "ID must be a 6‑character alphanumeric string."
assert 0.0 <= self._altitude <= self._MAX_ALT, "Altitude out of range."
assert 0 <= self._battery <= 100, "Battery out of range."
# Getter methods
def get_flight_id(self) -> str:
"""Return the immutable flight identifier."""
return self._id
def get_altitude(self) -> float:
"""Return current altitude in metres."""
return self._altitude
def get_battery(self) -> int:
"""Return remaining battery charge (percentage)"""
return self._battery
def set_altitude(self, value: float) -> None:
"""Set altitude, clamping to legal ceiling."""
if not isinstance(value, (int, float)):
raise TypeError("Altitude must be a number.")
if not 0.0 <= value <= self._MAX_ALT:
raise ValueError(f"Altitude must be 0–{self._MAX_ALT} m.")
self._altitude = float(value)
self._check_invariants()
def __str__(self) -> str:
return (
f"DroneFlight({self._id}, "
f"alt={self._altitude:.1f} m, bat={self._battery} %)"
)
def ascend(self, metres: float) -> None:
"""Climb *metres* metres (cannot exceed the legal ceiling)."""
if metres < 0:
raise ValueError("ascend() expects a non‑negative distance.")
self.set_altitude(min(self._altitude + metres, self._MAX_ALT))
def land(self) -> None:
"""Land the drone and consume 5 % battery."""
self.set_altitude(0.0)
self._battery = max(self._battery - 5, 0)
self._check_invariants()>>> d = DroneFlight("ABC123")
>>> d.ascend(50)
>>> print(d)
DroneFlight(ABC123, alt=50.0 m, bat=100 %)
>>> d.altitude = 200
Caught: Altitude must be 0–120 m.
>>> DroneFlight("BAD!")
Caught: ID must be a 6‑character alphanumeric string.
>>> d.land()
DroneFlight(ABC123, alt=0.0 m, bat=95 %)Summary
Representation invariants define the valid states of an object
They help catch bugs early and document assumptions (mostly during development time)
Use _check_invariants() to verify invariants after every state change
Good invariants are specific and testable