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:
- Integer multiplication for cross-products (numerator × denominator)
- GCD computation for normalization
- 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:
| N | Time | Final denominator digits |
|---|---|---|
| 100 | 1 ms | 42 digits |
| 1,000 | 50 ms | 434 digits |
| 10,000 | 12 s | 4,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
-
Periodic approximation: Call
limit_denominator()periodically to keep denominators manageable, at the cost of introducing small errors. -
Batch operations: Compute the final result symbolically and simplify once, rather than simplifying at each step.
-
Use Decimal for fixed-point: If you need exact decimal arithmetic (not exact rational),
Decimalavoids 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 operand | Right operand | Result type |
|---|---|---|
| Fraction | int | Fraction |
| Fraction | Fraction | Fraction |
| Fraction | float | float |
| Fraction | complex | complex |
| Fraction | Decimal | TypeError |
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
-
Creating from float captures binary error.
Fraction(1.1)is notFraction(11, 10). Always useFraction("1.1")orFraction(11, 10). -
Denominator growth is unbounded. Repeated operations on unrelated fractions produce increasingly large denominators. Monitor denominator size in long computations.
-
No Decimal interop.
Fraction(1, 3) + Decimal("0.5")raisesTypeError. Convert explicitly. -
mathfunctions return floats.math.sqrt(Fraction(2))returns a float, not a Fraction. For exact square roots, usesympyor check if the result is rational first. -
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.
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.