skip to content

What does fractions.Fraction.limit_denominator do, and when is Fraction the right numeric type?

level: middleimportance: nice to knowfreq 16%

answer

  1. Exact ratios rather than decimal digits
  2. Numerator and denominator, always reduced
  3. The closest ratio under a size cap
  4. Recovering one tenth from a float
  5. Continued fractions behind the search

basics

~10 s

limit_denominator returns the closest Fraction whose denominator is at most the limit you give, so Fraction(0.1).limit_denominator(1000) is 1/10. Fraction stores an exact numerator and denominator, which suits ratios that division must not degrade.

solid answer

~40 s

`fractions.Fraction` holds an exact rational — a numerator and a denominator kept in lowest terms — so division stays exact: `Fraction(1, 3) + Fraction(1, 6)` is exactly `Fraction(1, 2)`, which neither float nor Decimal can manage. `limit_denominator(max_denominator)` returns the closest Fraction whose denominator does not exceed the limit, which is how you recover a human-meant ratio from a float: `Fraction(0.1)` is the float's exact binary value, a monstrous ratio, while `Fraction(0.1).limit_denominator(1000)` is `1/10`. Reach for Fraction when the domain is genuinely ratios — aspect ratios, gear or sample-rate conversions, exact probabilities, unit conversions chained without loss. Do not reach for it for money: denominators grow as you accumulate, arithmetic is slower, and there is no scale or rounding policy, which is exactly what `Decimal` provides.

code

pycon · 9 lines
pycon
>>> from fractions import Fraction
>>> Fraction(0.1)
Fraction(3602879701896397, 36028797018963968)
>>> Fraction(0.1).limit_denominator(1000)
Fraction(1, 10)
>>> Fraction("3.141592653589793").limit_denominator(100)
Fraction(311, 99)
>>> Fraction(1, 3) + Fraction(1, 6)
Fraction(1, 2)

go deeper

for a junior

Know that fractions.Fraction keeps a numerator and denominator instead of digits, that it is reduced on construction, and that limit_denominator finds the nearest simple ratio to a messy value.

for a middle

Explain why Fraction(0.1) is a huge ratio while Fraction("0.1") is one tenth, what limit_denominator searches for, and how Fraction, Decimal and float divide up the work between them.

for a senior

Show judgement about cost: denominator growth over long accumulations, where an exact ratio actually buys correctness, and the boundary where you convert to Decimal or float and make the loss explicit.

for a principal

Own the numeric-type policy for a codebase — which representation each domain quantity uses, where conversions are permitted, and how those choices survive serialisation between services.

### What a Fraction is `fractions.Fraction` is Python's exact rational type. Internally it is a pair of ints, normalised on construction by dividing out the greatest common divisor and keeping the sign in the numerator, so `Fraction(6, 8)` is `Fraction(3, 4)` and the `numerator` and `denominator` attributes are always in lowest terms. Because both parts are Python ints, there is no width limit and no rounding: every addition, subtraction, multiplication and division of two Fractions is exact, and a Fraction never needs a precision setting because it has no digits to lose. That is a real capability neither of the other numeric types has. A float cannot hold one third at all, and a Decimal can hold it only to the current context's precision — one third is not a terminating decimal, so `Decimal(1) / Decimal(3)` is rounded. `Fraction(1, 3)` is one third, and `Fraction(1, 3) + Fraction(1, 6)` is exactly `Fraction(1, 2)`. ### Constructing one, and the float trap again The constructor accepts a pair of ints, a single int, a string, a `Decimal`, another Fraction, or a float. As with `decimal.Decimal`, the float conversion is *exact*, and exact means faithful to the binary value rather than to what you typed: ```pycon >>> from fractions import Fraction >>> Fraction(0.1) Fraction(3602879701896397, 36028797018963968) >>> Fraction("0.1") Fraction(1, 10) ``` The denominator there is 2 to the 55th — the tell-tale sign that a float went in. Constructing from a string, or from a Decimal, gives the ratio you meant. ### What limit_denominator computes `limit_denominator(max_denominator=1000000)` returns the closest Fraction to `self` whose denominator is at most `max_denominator`. It is a best-rational-approximation search, driven by the continued fraction expansion of the value, so it is not merely rounding the denominator down; nothing with a denominator inside the limit is closer to the original than what it returns: ```pycon >>> Fraction(0.1).limit_denominator(1000) Fraction(1, 10) >>> Fraction("3.141592653589793").limit_denominator(10) Fraction(22, 7) >>> Fraction("3.141592653589793").limit_denominator(100) Fraction(311, 99) ``` Two uses dominate. The first is *recovering intent*: a float arrived from somewhere, you believe it was a simple ratio, and you want that ratio back — a denominator cap of a few thousand cleans up binary noise without inventing structure. The second is *deliberate approximation*: you need a rational you can act on physically, such as a resampling ratio a pipeline can implement with small integer up- and down-factors, or a gear ratio buildable from real tooth counts. ### Choosing between Fraction, Decimal and float Use `Fraction` when exactness under *division* is the requirement and the numbers stay tame: exact probabilities, chained unit conversions, aspect and sampling ratios, symbolic-ish arithmetic in tests. Its cost is real — denominators grow as unlike fractions accumulate, so a long summation can end up with enormous ints and arithmetic that is far slower than either alternative, and there is no concept of scale or a rounding policy to control the growth. Use `Decimal` for money and for anything where a fixed number of decimal places and an explicit rounding rule are part of the contract. Use `float` when the values are measurements, the operations are transcendental, or speed matters and a relative error near 1e-16 is irrelevant. ### What the cap does when it bites hard A cap smaller than the value's own denominator forces a genuine approximation rather than an error: `Fraction(3, 4).limit_denominator(2)` returns `Fraction(1, 1)`, the nearest rational that fits. So the call always succeeds, and the burden is on you to choose a cap that matches the precision your domain actually has. If you need to know how far it moved, compare the result with the original — both are exact, so the difference is exact too, and a threshold on that difference is a sound guard before you accept an approximation. When all you want is the underlying pair without building a Fraction, `as_integer_ratio()` exists on `int`, `float`, `Decimal` and `Fraction` alike: `Fraction(1, 3).as_integer_ratio()` is `(1, 3)`, and `(0.75).as_integer_ratio()` is `(3, 4)`. ### Interoperation rules worth knowing Fraction mixes with `int` and stays exact — `Fraction(1, 3) + 1` is `Fraction(4, 3)`. Mixed with a `float` the result is a `float`: the numeric tower widens to the lossier type rather than promoting the float to an exact rational, so exactness disappears silently. Mixed with a `Decimal` it raises `TypeError`: the two exact types deliberately refuse to combine, because there is no answer that is exact in both worlds. Convert explicitly — `Fraction(Decimal("1.1"))` is `Fraction(11, 10)` — so the lossy step is visible in the code. Comparisons and hashing do work across the numeric tower: a Fraction compares equal to a float or a Decimal of the same value, and `hash(Fraction(1, 2)) == hash(0.5)`, which means the two can collide as dictionary keys. Finally, `Fraction` is registered in the `numbers` tower as a `Rational`, and both it and `float` expose `as_integer_ratio()`, which is the low-level way to get the same numerator/denominator pair without constructing a Fraction at all.

  • Why not use Fraction for money, given that it is exact?
    Money needs a fixed scale and a stated rounding rule, and Fraction has neither. It also grows: summing amounts with unlike denominators inflates the numerator and denominator, so long runs get slow and the values stop being human-readable. Decimal gives you two decimal places, a chosen rounding mode and a value that displays and stores as the amount itself.
  • What happens if you add a Fraction to a float, or to a Decimal?
    Adding a float returns a float — Python widens to the lossier type, so exactness is gone silently. Adding a Decimal raises TypeError, because the two exact types refuse to guess at a common representation. Convert deliberately, for example Fraction(Decimal("1.1")), so the conversion is visible where it happens.

saying these in an interview costs you the question

  • Thinks Fraction(0.1) gives one tenth
  • Proposes Fraction as the type for money
  • Believes limit_denominator just truncates the denominator
  • Ignores denominator growth over long summations
  • Expects Fraction and Decimal to add together
  • Assumes Fraction stores digits like Decimal does

context