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
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
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
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)
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)
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
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)
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)
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
decimal.Decimalunless you need performance and know that imprecision is acceptable, in which case use
- 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
decimal.Decimalin 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.