Decimal Still Rounds Like a Float: Check How the Value Was Constructed
By ON Lab · Sources checked November 8, 2026
Converting a float to Decimal preserves that float’s actual value, including its binary approximation. If your intended input is exact decimal text, construct Decimal from that text before arithmetic. Then choose a rounding mode explicitly. The stored value and the tie-breaking rule are separate decisions.
Inspect what reaches the rounding operation
A historical CPython question compared decimal tie rounding with the surprising result of round(2.675, 2). Our fixture separates those two effects without using real prices or prescribing any financial rounding policy.
from decimal import Decimal, ROUND_HALF_EVEN
value = 2.675
print(repr(value))
print(Decimal(value))
print(Decimal("2.675"))
print(round(value, 2))
print(Decimal(value).quantize(Decimal("0.01"), rounding=ROUND_HALF_EVEN))
print(Decimal("2.675").quantize(Decimal("0.01"), rounding=ROUND_HALF_EVEN))On Python 3.9.6 and 3.13.18, the float displayed as 2.675, but Decimal(value) exposed 2.67499999999999982236431605997495353221893310546875. Both the rounded float and the Decimal constructed from it produced 2.67. The Decimal constructed directly from "2.675" produced 2.68 under the same half-even quantization.
The conversion was not failing to preserve the float: it preserved exactly the value it received. A shorter display representation had hidden the distinction. Switching numeric classes after an earlier conversion cannot recover an input value that is no longer available.
Exact decimal ties still need a policy
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP
step = Decimal("0.01")
for text in ["2.675", "2.665", "-2.665"]:
value = Decimal(text)
print(text,
value.quantize(step, rounding=ROUND_HALF_EVEN),
value.quantize(step, rounding=ROUND_HALF_UP))| Exact input text | Half even | Half up |
|---|---|---|
| 2.675 | 2.68 | 2.68 |
| 2.665 | 2.66 | 2.67 |
| -2.665 | -2.66 | -2.67 |
These were observed on both runtimes. The last row is a useful boundary: half up is not shorthand for always moving a value toward positive infinity. Use the documented rule’s name and test negative as well as positive inputs.
Keep decimal constants decimal too
If decimal arithmetic is the intended contract, construct constants such as the quantization step from strings as well. Building a Decimal from a float expression such as 10 ** -2 imports that expression’s float approximation. A Decimal elsewhere in the expression does not retroactively make every operand exact.
For the same reason, serializing an already rounded or approximated float to text is a different provenance from retaining the original decimal input. Decimal(str(value)) expresses the float’s chosen display text; it does not establish what a user originally supplied. Preserve the original input representation when that distinction matters.
Do not use a tiny adjustment as a universal fix
Adding an arbitrary epsilon changes the value before rounding. Its effect depends on magnitude, sign and the required rule; it is not a general correction for representation. Likewise, formatting to two decimal places changes the displayed string and does not define the application’s arithmetic or storage type.
Decide whether the task needs approximate measurement calculations, exact decimal quantities, or integer units. Each has a contract. This guide demonstrates numeric behavior; it does not choose accounting, tax, currency or regulatory rounding requirements.
Verify the input path and the result together
- Record whether the value began as text, an integer unit count or a float.
- Inspect a float with an exact conversion when diagnosing representation.
- Specify the decimal step and tie rule when the contract requires them.
- Test values just below, at and above a boundary, including negatives.
- Check serialized output and the value read back by the actual consumer.
If quantize raises InvalidOperation for large or high-precision inputs, inspect the decimal context and required precision. Increasing precision blindly is not a substitute for an input-domain decision. The small fixture here is not a stress test of every context, exponent, NaN or infinity.
The official standard-library documentation and original report are free alternatives. This guide’s added checks separate input construction, exact decimal ties, negative behavior and output display. No paid numeric package or live transaction is involved.
Method: current Python 3.13 built-in, Decimal and floating-point documentation plus a historical CPython report. Four input rows were compared on Python 3.9.6 and 3.13.18/macOS. No actual monetary amount, financial policy, production calculation or all-precision context was tested.