There are two changes to rounding between Python 2 and Python 3 that programmers should watch out for when migrating. I realize that Python 3 has been out for ten years, but I also know there are still people using Python 2 so there’s a chance this info is still useful to someone.

# Ties away from zero vs. ties to nearest even

Python 2’s rounding behavior is “round to nearest, ties away from zero.” Python 3 uses “round to nearest, ties to nearest even,” sometimes called “banker’s rounding.” This change manifests in both the built-in `round()`

function and in `decimal.Decimal.quantize()`

.

I think this change is great. Ties-to-nearest-even is a great choice. But if you want the Python 2 behavior in Python 3 you’ll have to do some work. For `float`

it’s easiest to make your own function. See this example (starting after “if you need the Python 2 behavior”). For `Decimal`

you can specify `decimal.ROUND_HALF_UP`

when calling `quantize()`

.

I wish Python 3 `round()`

took a rounding scheme parameter because the alternatives are cumbersome. Also it’s hard to mimic `round()`

‘s behavior exactly, especially for positive/negative numbers and positive/negative values of `ndigits`

.

# round() does or does not cast Decimals to floats

This change is more subtle. Python 2 `round()`

always does floating-point arithmetic, even if you pass in a `Decimal`

. But Python 3 `round()`

delegates to the class of the value being passed in. This means `Decimals`

rounded with `round()`

are susceptible to floating-point imprecision in Python 2 but not in Python 3.

I think this change is great, too. Let me give you an example from Python 2 to explain why. First let’s round the `float`

1.006 to 2 decimal places:

`>>> round(1.006, 2)`

1.01

Makes sense. Now let’s try rounding 1.005 to 2 decimal places. We expect the same result because Python 2 rounds ties away from zero (“up” in this case, since the number is positive).

`>>> round(1.005, 2)`

1.0

That’s weird. What happened? It turns out 1.005 can’t be represented exactly as a floating-point number. You can see this if you convert 1.005 from `float`

to `Decimal`

:

`>>> decimal.Decimal(1.005)`

Decimal('1.00499999999999989341858963598497211933135986328125')

Oof. But we’re using floating-point numbers and they’re notorious for this, so we only have ourselves to blame. Conscientious programmers would avoid this problem by using `decimal.Decimal`

. Let’s try it:

`>>> round(decimal.Decimal('1.005'), 2)`

1.0

Oof—still wrong. This time because `round()`

converts to `float`

. That’s an easy mistake to make. Honestly I think it would have been better if Python 2 `round()`

raised an exception if called with a `Decimal`

, because implicitly converting to `float`

with the possibility of incorrect results due to floating-point imprecision is likely not what the programmer wanted.

So what’s the right way to round a `Decimal`

in Python 2? This awkward incantation:

`>>> decimal.Decimal('1.005').quantize(decimal.Decimal('0.01'), decimal.ROUND_HALF_UP)`

1.01

Yeah. Not great. It’s definitely an improvement being able to use `round()`

in Python 3 (provided you’re ok with ties-to-nearest-even, anyway).

# Guidelines for working with numbers with fractions

- Use
`decimal.Decimal`

unless you need performance and know that imprecision is acceptable, in which case use`float`

. - Decide what rounding scheme is appropriate for your application (ties away from zero, ties to nearest even, etc) and make sure your code is using it.
- Don’t use
`round()`

with`decimal.Decimal`

in Python 2.

# An unrelated complaint

This isn’t Python-specific, but I hate that floating-point numbers are a default. In the world we live in, where programmers may not have a computer science background and may not be familiar with the pitfalls of floating-point arithmetic, I think there is no reasonable default. It’s too easy for programmers to get behavior they don’t want. I like strict languages and I think we’d be better off if languages forced programmers to specify either floating-point or decimal.Decimal/BigDecimal/etc for every number that has a fraction.