Fractions

The fractions module represents rational numbers exactly as numerator/denominator pairs, avoiding both float's binary imprecision and decimal's base-10 limits.

fractions.Fraction represents rational numbers -- any number expressible as one integer divided by another -- exactly, as a reduced numerator/denominator pair, with no rounding at all. Where Decimal gives exact base-10 arithmetic, Fraction gives exact arithmetic for any ratio of integers, including ones like 1/3 that Decimal can only approximate.

What Is / Why It Matters

Both float and Decimal are ultimately limited: float can't exactly represent most decimal fractions (see the Decimal topic), and Decimal can't exactly represent values like 1/3 that don't terminate in base 10 -- it can only round them to its configured precision. Fraction sidesteps both limitations by keeping numbers as an exact numerator/denominator pair for as long as you stay within rational arithmetic (+, -, *, /).

from fractions import Fraction
from decimal import Decimal

print(1 / 3)                      # 0.3333333333333333 -- float approximation
print(Decimal(1) / Decimal(3))    # 0.3333333333333333333333333333 -- rounded to context precision
print(Fraction(1, 3))             # 1/3 -- exact, forever

Creating Fractions

Fraction can be constructed from two integers (numerator, denominator), from a single integer, from a str written as "numerator/denominator" or a plain decimal string, or from a float/Decimal (which captures that value's exact underlying representation, imprecision and all).

Just like Decimal, constructing Fraction from a float literal captures that float's exact (but already-imprecise) binary value -- construct from a string or two integers when you want the mathematically intended fraction.

from fractions import Fraction

print(Fraction(3, 4))          # 3/4
print(Fraction(6, 8))          # 3/4 -- automatically reduced to lowest terms
print(Fraction(5))             # 5/1 -- from an int
print(Fraction("3/4"))         # 3/4 -- from a fraction string
print(Fraction("0.75"))        # 3/4 -- from a terminating decimal string, exact
print(Fraction(0.1))           # 3602879701896397/36028797018963968 -- inherits float's imprecision!

Automatic Reduction to Lowest Terms

A Fraction is always stored in lowest terms (reduced by the greatest common divisor of numerator and denominator), with the denominator always positive -- Fraction(6, 8) and Fraction(-3, -4) both normalize to the same canonical form.

from fractions import Fraction

f = Fraction(-6, -8)
print(f)               # 3/4 -- sign normalized, reduced
print(f.numerator, f.denominator)  # 3 4

Rational Arithmetic

Fraction supports the standard arithmetic operators, and every result of +, -, *, /, and ** (with an integer exponent) between two Fractions -- or between a Fraction and an int -- is itself an exact Fraction, automatically reduced.

from fractions import Fraction

a = Fraction(1, 3)
b = Fraction(1, 6)

print(a + b)     # 1/2 -- exact
print(a - b)     # 1/6
print(a * b)     # 1/18
print(a / b)     # 2
print(a ** 2)    # 1/9
print(a + 1)     # 4/3 -- mixes freely with int

Exact Representation and Comparisons

Because a Fraction never loses precision under rational arithmetic, exact equality comparisons behave the way math suggests they should -- something that's not reliably true for float.

from fractions import Fraction

total = sum((Fraction(1, 10) for _ in range(10)), Fraction(0))
print(total)                     # 1 -- exactly, no drift
print(total == 1)                # True

float_total = sum(0.1 for _ in range(10))
print(float_total)               # 0.9999999999999999
print(float_total == 1.0)        # False

Converting To and From Other Types

float(fraction) converts to the nearest float (introducing float's usual binary imprecision, deliberately, at that point); int(fraction) truncates toward zero. Fraction also participates correctly in comparisons with int, float, and Decimal is not directly comparable/combinable without conversion, mirroring the Decimal-vs-float restriction.

from fractions import Fraction

f = Fraction(7, 2)
print(float(f))     # 3.5
print(int(f))       # 3 -- truncates toward zero
print(f > 3)         # True -- compares directly with int
print(f > 3.4)       # True -- compares directly with float

Practical Usage

Fraction is the right tool whenever a problem is genuinely about exact ratios: probabilities expressed as exact fractions, unit conversions defined by exact ratios (e.g. recipe scaling), music theory (note-length ratios), or any algorithm (like the classic reduction of a ratio, or exact rational solutions to linear systems) where accumulating floating-point error across many operations would be unacceptable.

from fractions import Fraction

def scale_recipe(ingredient_amounts: dict[str, Fraction], factor: Fraction) -> dict[str, Fraction]:
    return {name: amount * factor for name, amount in ingredient_amounts.items()}

recipe = {"flour": Fraction(2, 3), "sugar": Fraction(1, 4)}
doubled = scale_recipe(recipe, Fraction(2))
print(doubled)   # {'flour': Fraction(4, 3), 'sugar': Fraction(1, 2)}

Edge Cases

Dividing by Fraction(0, ...) raises ZeroDivisionError, same as int/float division by zero.

Fraction is exact but not necessarily fast or memory-bounded -- repeated arithmetic can grow the numerator and denominator to very large integers before reduction catches up, since Python's arbitrary-precision int has no fixed size; this is a real performance consideration for long chains of operations, even though correctness is never in question.

Fraction(some_float) captures the float's exact binary value rather than the decimal you probably meant -- use Fraction(some_float).limit_denominator() to get the simplest fraction that approximates it within a reasonable denominator, if that's the intent.

from fractions import Fraction

print(Fraction(0.1))                        # ugly exact binary fraction
print(Fraction(0.1).limit_denominator(100))  # 1/10 -- the simple fraction you likely meant

Complete Example

from fractions import Fraction

def add_fractions(*fractions: Fraction) -> Fraction:
    total = Fraction(0)
    for f in fractions:
        total += f
    return total

def probability_of_all(events: list[Fraction]) -> Fraction:
    """Exact probability that all independent events occur."""
    result = Fraction(1)
    for p in events:
        result *= p
    return result

parts = [Fraction(1, 4), Fraction(1, 6), Fraction(1, 12)]
print("sum:", add_fractions(*parts))                 # 1/2, exact

probs = [Fraction(1, 2), Fraction(1, 3), Fraction(1, 6)]
print("combined probability:", probability_of_all(probs))  # 1/36, exact

Quick Reference

Fraction(3, 4)             from numerator, denominator
Fraction("3/4")            from a fraction string
Fraction("0.75")           from an exact decimal string
Fraction(5)                 from an int
Fraction(0.1)               DON'T for "intended" values -- captures float imprecision
f.numerator / f.denominator  always in lowest terms, denominator > 0
+ - * / **                   exact rational arithmetic
float(f)                    convert to nearest float (imprecise, deliberate)
f.limit_denominator(n)      simplest approximation with denominator <= n

Key Takeaways

Fraction represents rational numbers exactly as a reduced numerator/denominator pair -- no rounding under +, -, *, /.

It's always stored in lowest terms with a positive denominator, regardless of how it was constructed.

Construct from two ints or a string for exact intended values; constructing from a float literal captures that float's own binary imprecision instead.

float(fraction) and int(fraction) convert deliberately, introducing float imprecision or truncation only at that explicit point.

Use Fraction for exact ratios (probabilities, unit conversions, exact rational math); it's exact but not necessarily fast, since numerators/denominators can grow large over long chains of operations.

half = Fraction(0.5) # happens to work, but the habit is unsafe half = Fraction(1, 2) # or Fraction("0.5")

Constructing from a float literal captures that float's exact binary value; it happens to be exact for 0.5 (a power-of-two fraction) but silently breaks for values like 0.1 that aren't exactly representable in binary.

result = Fraction(1, 3) + 0.5 # mixes Fraction and float, losing exactness result = Fraction(1, 3) + Fraction(1, 2)

Mixing Fraction with float in arithmetic produces a float result (Fraction quietly converts), reintroducing binary imprecision into what should be exact rational arithmetic.

approx = Fraction(math.pi) # gigantic, meaningless exact fraction approx = Fraction(math.pi).limit_denominator(1000) math.pi is irrational and float can only approximate it; Fraction(math.pi) captures that approximation exactly as an unwieldy huge fraction, while.limit_denominator() gives a small, human-readable rational approximation.

What does Fraction represent, and how is it stored internally?

It represents a rational number (a ratio of two integers) as a numerator and denominator, automatically reduced to lowest terms with the denominator kept positive.

Why is Fraction(1, 3) exact while Decimal(1) / Decimal(3) is only exact up to the context's precision?

Fraction stores the ratio itself (1 over 3) rather than trying to expand it into digits, so no rounding ever happens for rational values; Decimal represents numbers as base-10 digit expansions, and 1/3 doesn't terminate in base 10, so it must be rounded to fit the configured precision.

Why does constructing Fraction from a float literal risk capturing unwanted imprecision?

Fraction(some_float) captures the exact value the float already holds in binary, which for most decimal fractions is only an approximation of the intended decimal value -- the resulting Fraction is exact about the wrong number.

What does .limit_denominator(max_denominator) do, and when is it useful?

It returns the simplest Fraction (smallest denominator) that approximates the current value within a denominator bound; it's useful for turning an ugly float-derived fraction (or an irrational approximation) into the simple, human-meaningful fraction you likely intended.

What happens when you add a Fraction and a float together?

The result is a float -- Fraction cooperates with float arithmetic by converting, which means the exactness of the Fraction side is lost in the combined result.

Give an example domain where Fraction's exactness matters more than float's speed.

Exact probability calculations (e.g. combining independent event probabilities as exact fractions), recipe/ratio scaling, or music-theory note-length ratios -- any case where accumulated rounding error across many operations would be unacceptable.

Why can repeated Fraction arithmetic become slow even though it's always exact?

Because numerators and denominators are unbounded Python ints that can grow very large across a long chain of operations before reduction catches up, making each subsequent operation more expensive even though correctness never degrades.

Write a function simplify_ratio(numerator, denominator) that returns a tuple of the simplified numerator and denominator of numerator/denominator, using Fraction.

from fractions import Fraction def simplify_ratio(numerator: int, denominator: int) -> tuple[int, int]: f = Fraction(numerator, denominator) return f.numerator, f.denominator print(simplify_ratio(48, 18)) (8, 3) -- Fraction automatically reduces 48/18 to lowest terms (dividing both by their GCD, 6), giving 8/3.

What does this print?

1 True -- adding Fraction(1, 3) three times gives exactly 3/3, which reduces to 1/1, printed as '1'; the equality with the int 1 holds exactly, unlike the equivalent float computation with 0.1 three times.

Write a function decimal_to_fraction(text) that converts a terminating decimal string like '0.125' into a Fraction in lowest terms, without using limit_denominator.

from fractions import Fraction def decimal_to_fraction(text: str) -> Fraction: return Fraction(text) print(decimal_to_fraction("0.125")) print(decimal_to_fraction("2.5"))

1/8, then 5/2 -- Fraction's string constructor parses a terminating decimal string directly into its exact reduced rational form without needing float or limit_denominator at all.

from fractions import Fraction
total = Fraction(0)
for _ in range(3):
    total += Fraction(1, 3)
print(total, total == 1)