Python fractions Module — Deep Dive

Fraction internals

Fraction inherits from numbers.Rational, which inherits from numbers.Real in Python’s numeric tower (PEP 3141). This means Fractions can be used anywhere a Real is expected — including with math.floor, math.ceil, and comparison operators.

Storage

Internally, a Fraction stores two Python int values: _numerator and _denominator. Since Python ints have arbitrary precision, Fractions can represent any rational number regardless of size.

from fractions import Fraction

f = Fraction(1, 7)
f.numerator    # 1
f.denominator  # 7

Both are read-only properties — Fraction is immutable, like int and float.

Construction from float

When you create Fraction(0.1), Python uses float.as_integer_ratio():

(0.1).as_integer_ratio()
# (3602879701896397, 36028797018963968)

Fraction(0.1)
# Fraction(3602879701896397, 36028797018963968)

This is the exact rational representation of the IEEE 754 double-precision binary approximation of 0.1. It’s not a bug — it’s the truthful representation of what 0.1 actually is in binary floating point.

Construction from Decimal

Fraction(Decimal("0.1")) produces Fraction(1, 10) because Decimal stores the exact decimal representation, not a binary approximation.

The GCD normalization

Every Fraction operation normalizes the result by dividing by math.gcd(numerator, denominator). In CPython 3.9+, math.gcd is implemented in C and handles arbitrary-precision integers efficiently. The sign is always moved to the numerator.

The limit_denominator algorithm

limit_denominator(max_denominator) uses the Stern-Brocot tree / mediant-based continued fraction algorithm. Pseudocode:

p0/q0 = 0/1 (lower bound)
p1/q1 = 1/0 (upper bound, conceptually infinity)

while True:
    mediant = (p0 + p1) / (q0 + q1)
    if q0 + q1 > max_denominator:
        break
    if mediant < target:
        p0, q0 = mediant numerator/denominator
    else:
        p1, q1 = mediant numerator/denominator

return closer of p0/q0 and p1/q1

This produces the best rational approximation with denominator ≤ max_denominator. It’s the same algorithm that generates the famous approximations of π:

from fractions import Fraction
import math

pi = Fraction(math.pi)
for d in [1, 7, 10, 100, 1000, 10000]:
    approx = pi.limit_denominator(d)
    error = abs(float(approx) - math.pi)
    print(f"d≤{d:5d}: {str(approx):>12s}  error={error:.2e}")

Output:

d≤    1:            3  error=1.42e-01
d≤    7:         22/7  error=1.26e-03
d≤   10:         22/7  error=1.26e-03
d≤  100:       311/99  error=2.91e-05
d≤ 1000:      355/113  error=2.67e-07
d≤10000:    355/113    error=2.67e-07

355/113 is the famous Milü approximation, accurate to 7 decimal places.

Performance characteristics

Arithmetic cost

Each Fraction operation involves:

  1. Integer multiplication for cross-products (numerator × denominator)
  2. GCD computation for normalization
  3. Integer division for simplification

For small fractions (numerators/denominators < 2^64), these are fast C operations. For large fractions, Python’s arbitrary-precision integer arithmetic kicks in.

Benchmark: repeated addition

Adding N fractions 1/n for n=1 to N:

NTimeFinal denominator digits
1001 ms42 digits
1,00050 ms434 digits
10,00012 s4,346 digits
100,000> 30 min~43,000 digits

The denominator grows roughly as the LCM of 1 through N, which grows exponentially. This is the fundamental limitation of exact rational arithmetic — intermediate values balloon in size.

Mitigation strategies

  1. Periodic approximation: Call limit_denominator() periodically to keep denominators manageable, at the cost of introducing small errors.

  2. Batch operations: Compute the final result symbolically and simplify once, rather than simplifying at each step.

  3. Use Decimal for fixed-point: If you need exact decimal arithmetic (not exact rational), Decimal avoids the denominator growth problem entirely.

Numeric tower integration

Fraction participates in Python’s numeric tower:

from fractions import Fraction
import numbers

isinstance(Fraction(1, 2), numbers.Rational)  # True
isinstance(Fraction(1, 2), numbers.Real)      # True
isinstance(Fraction(1, 2), numbers.Complex)    # True

Mixed-type arithmetic rules

Left operandRight operandResult type
FractionintFraction
FractionFractionFraction
Fractionfloatfloat
Fractioncomplexcomplex
FractionDecimalTypeError

The Fraction + Decimal case raises TypeError because neither type knows how to convert the other losslessly. Convert explicitly:

from fractions import Fraction
from decimal import Decimal

# Option 1: Convert Decimal to Fraction
Fraction(1, 3) + Fraction(Decimal("0.5"))  # Fraction(5, 6)

# Option 2: Convert Fraction to Decimal (may lose precision)
Decimal(str(Fraction(1, 3))) + Decimal("0.5")
# Decimal('0.8333333333333333333333333335')

Implementing custom number types with Fraction

Fraction can serve as the base for exact arithmetic systems:

from fractions import Fraction
from dataclasses import dataclass

@dataclass(frozen=True)
class Money:
    """Exact monetary amount using Fraction internally."""
    cents: Fraction

    @classmethod
    def from_str(cls, amount: str) -> "Money":
        return cls(Fraction(amount) * 100)

    @property
    def dollars(self) -> str:
        d = self.cents / 100
        return f"${float(d):.2f}"

    def __add__(self, other: "Money") -> "Money":
        return Money(self.cents + other.cents)

    def split(self, n: int) -> list["Money"]:
        """Split evenly with remainder going to first shares."""
        base = Fraction(int(self.cents // n), 1)
        remainder = int(self.cents - base * n)
        return [Money(base + (1 if i < remainder else 0)) for i in range(n)]

# Split $100 three ways
total = Money.from_str("100.00")
shares = total.split(3)
# Each share is exactly $33.33 or $33.34, totaling exactly $100.00
assert sum(s.cents for s in shares) == total.cents

Continued fractions

Fractions connect naturally to continued fraction representations, which are fundamental in number theory:

from fractions import Fraction

def to_continued_fraction(frac, max_terms=20):
    """Convert a Fraction to its continued fraction coefficients."""
    coeffs = []
    while max_terms > 0:
        whole = int(frac)
        coeffs.append(whole)
        remainder = frac - whole
        if remainder == 0:
            break
        frac = Fraction(1, remainder)
        max_terms -= 1
    return coeffs

def from_continued_fraction(coeffs):
    """Reconstruct a Fraction from continued fraction coefficients."""
    result = Fraction(coeffs[-1])
    for c in reversed(coeffs[:-1]):
        result = c + Fraction(1, result)
    return result

# π approximation
pi_frac = Fraction(math.pi)
cf = to_continued_fraction(pi_frac)
# [3, 7, 15, 1, 292, ...] — the famous continued fraction of π

The first few convergents are 3, 22/7, 333/106, 355/113 — progressively better approximations.

Hashing and equality

Fractions hash-compatibly with ints and floats when they represent the same value:

hash(Fraction(1, 1)) == hash(1) == hash(1.0)  # True
Fraction(1, 2) == 0.5  # True
{Fraction(1, 2), 0.5}  # {Fraction(1, 2)} — only one element

This is mandated by Python’s numeric tower: if a == b, then hash(a) == hash(b). The implication is that Fractions, ints, and floats can coexist as dictionary keys and set members with correct deduplication.

Serialization

Fractions aren’t JSON-serializable by default:

import json
from fractions import Fraction

# Custom serializer
class FractionEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Fraction):
            return {"_type": "Fraction", "n": obj.numerator, "d": obj.denominator}
        return super().default(obj)

def fraction_decoder(dct):
    if dct.get("_type") == "Fraction":
        return Fraction(dct["n"], dct["d"])
    return dct

data = {"ratio": Fraction(22, 7)}
s = json.dumps(data, cls=FractionEncoder)
loaded = json.loads(s, object_hook=fraction_decoder)
assert loaded["ratio"] == Fraction(22, 7)

For database storage, store numerator and denominator as separate integer columns, or store the string representation and reconstruct with Fraction("22/7").

Pitfalls

  1. Creating from float captures binary error. Fraction(1.1) is not Fraction(11, 10). Always use Fraction("1.1") or Fraction(11, 10).

  2. Denominator growth is unbounded. Repeated operations on unrelated fractions produce increasingly large denominators. Monitor denominator size in long computations.

  3. No Decimal interop. Fraction(1, 3) + Decimal("0.5") raises TypeError. Convert explicitly.

  4. math functions return floats. math.sqrt(Fraction(2)) returns a float, not a Fraction. For exact square roots, use sympy or check if the result is rational first.

  5. Not suitable for irrational numbers. π, e, √2 cannot be represented as Fractions. Using Fraction(math.pi) gives a rational approximation of the float approximation — two layers of imprecision.

The one thing to remember: Fraction gives you exact rational arithmetic with arbitrary precision, but denominators grow with each operation — understand the cost, use limit_denominator strategically, and always construct from strings or integers to preserve exactness from the start.

pythonstandard-librarymath

See Also

  • Python Atexit How Python's atexit module lets your program clean up after itself right before it shuts down.
  • Python Bisect Sorted Lists How Python's bisect module finds things in sorted lists the way you'd find a word in a dictionary — by jumping to the middle.
  • Python Contextlib How Python's contextlib module makes the 'with' statement work for anything, not just files.
  • Python Copy Module Why copying data in Python isn't as simple as it sounds, and how the copy module prevents sneaky bugs.
  • Python Dataclass Field Metadata How Python dataclass fields can carry hidden notes — like sticky notes on a filing cabinet that tools read automatically.