Skip to content

03

Rational numbers as ratios

A pair of integers (p, q), with q ≠ 0, represents the ratio p/q. Cross multiplication defines equality.

What we already know

Integers are equivalence classes of natural-number pairs. Their operations are available before we use them as numerators and denominators.

By the end, you can
  • derive rational equality and addition from integer operations;
  • identify the invariant enforced by __post_init__;
  • explain why reduction is explicit rather than automatic;
  • explain equality and hashing across equivalent representatives.
Read beside this chapter

Rational source and rational tests.

Definition

For nonzero denominators:

p/q ~ r/s exactly when p·s = q·r

Addition and multiplication are:

p/q + r/s = (p·s + q·r)/(q·s)
(p/q)·(r/s) = (p·r)/(q·s)

These formulas use only operations already constructed for integers.

Representation

@dataclass(frozen=True, slots=True, eq=False, repr=False)
class Rational:
    p: Integer
    q: Integer

    def __post_init__(self) -> None:
        if not isinstance(self.p, Integer) or not isinstance(self.q, Integer):
            raise TypeError("Rational.p and Rational.q must be Integer values")
        if self.q == Z_ZERO:
            raise ZeroDivisionError("the denominator cannot be zero")

The generated initializer stores the fields, then __post_init__ rejects values outside the representation. It does not reduce 2/4 to 1/2; both representatives remain available for study.

Validation follows generated initialization

The caller never invokes __post_init__ directly. This hook connects dataclass-generated code to a mathematical invariant.

Implementation

@log(log_level=21)
def __eq__(self, other: object) -> tuple[bool, LogMessage]:
    converted = _coerce_rational(other)
    if converted is None:
        return cast(bool, NotImplemented), lambda: "NotImplemented"
    result = self.p * converted.q == self.q * converted.p
    return result, lambda: (
        f"{self!r} == {converted!r} ⇔ "
        f"{self.p!r} * {converted.q!r} == "
        f"{self.q!r} * {converted.p!r}"
    )

Again, one line is the mathematical definition with fields substituted. The addition method applies the formula above and calls reduction() on the result. reduction() makes the denominator positive and divides numerator and denominator by their greatest common divisor.

Hashing also uses a reduced canonical pair. Therefore 1/2 and 2/4 compare equal and have equal hashes, as Python requires for dictionary keys and sets.

Trace

Predict equality without reducing

What two integer products does 1/2 == 2/4 compare? Why can the result be true even though the stored fields differ?

Experiment 3 · Cross-product equality Not run
⌘ / Ctrl + Enter
Output
Read the cross-product trace before the printed booleans.

Test

Tests check zero-denominator rejection, equivalent representatives, arithmetic, ordering with negative denominators, reduction, and the equality/hash contract. They protect implementation behavior for concrete cases, while the formulas provide the general mathematical reasoning.

Boundary

Rationals contain every integer but still have gaps: there is no rational whose square is 2. The next chapters keep rational arithmetic and use a polynomial plus an interval to identify such a real number.

Why can 1/2 equal 2/4 before reduction?

Next: represent polynomials as coefficient sequences →