# Rounding numbers in Python 2 and Python 3

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.

This entry was posted in Computers. Bookmark the permalink.