DEV Community

Tom Nijhof
Tom Nijhof

Posted on

The weird quirk with rounding in Python (and that is good)

When working with rounding in Python, you may have come across an unexpected quirk. Unlike most people who tend to round numbers ending in .5 up, Python operates differently. While it rounds 0.5 to 0, it rounds 1.5 to 2, following the international standard (IEEE 754). This may be seen as a feature or a bug, depending on how you view it.

The Python logo surrounded by different examples of rounding

It is a bug, for me!

If you encounter a bug when working with numbers ending in .5 I recommend utilizing the build library decimal. Within this library, there is a function that rounds all numbers ending with .5 up by using the ROUND_HALF_UP mode. This mode can be found in the documentation, along with other options for rounding. By incorporating this function into our code, we can ensure that any numbers ending in .5 are rounded correctly and avoid potential bugs.

I do NOT recommend using this unless it specifically causes a bug!

    from decimal import Decimal as D, ROUND_HALF_UP


    def round_nat(d):
        """
        Rounds all halves up
        """
        return D(d).quantize(D('1'), ROUND_HALF_UP)


    for x in range(1,100):
        d = x/10
        print(f"{d} rounds to {round_nat(d)}")
Enter fullscreen mode Exit fullscreen mode

It is a feature!

Rounding behavior in Python conforms to IEEE 754 rounding to nearest, ties to even. This is also known as bankers rounding or Dutch rounding. Guido van Rossum is not a banker but he is Dutch.
This ensures a fully random number is just as likely to round up as down. For example, let’s consider a range of numbers from 0 to 10 in increments of 0.1. If we calculate the average of these numbers, it comes out to 4.95. However, if we use the round_nat function (created in the previous chapter), we get an average of 5, due to the presence of 10 numbers ending in .5, which are all rounded up. This results in a slight bias towards a larger number. On the other hand, using native Python rounding eliminates this bias.

    all_num = [i/10 for i in range(0, 100)]
    print("average of all numbers:", sum(all_num)/len(all_num))
    all_rounded_num = [round(i) for i in all_num]
    print("average of all rounded numbers:", sum(all_rounded_num)/len(all_rounded_num))
    all_nat_rounded_num = [round_nat(i) for i in all_num]
    print("average of all natural rounded numbers:", sum(all_nat_rounded_num)/len(all_nat_rounded_num))
Enter fullscreen mode Exit fullscreen mode
    average of all numbers: 4.95
    average of all rounded numbers: 4.95
    average of all natural rounded numbers: 5
Enter fullscreen mode Exit fullscreen mode

Another way to understand it is by looking at the numbers between 0 and 1 with 1 decimal. We have 9 numbers in between, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9. To round them we need to remove 0.1 from 0.1, and add 0.1 to 0.9. Meaning their biases balance out.
The same for 0.2 and 0.8, 0.3 and 0.7, and 0.4 and 0.6; All numbers except 0.5. We do not have a counterbalance for 0.5 between 0 and 1.

To balance rounding 0.5 we balance it with 1.5. We remove 0.5 from 0.5 and add 0.5 to 1.5. Meaning that rounding is just as likely to increase as decrease a random number.

Conclusion

Rounding in Python may seem unusual at first glance, but it’s actually quite useful. Understanding how it works is essential as it can cause a program to behave differently than expected. While it may not have a significant impact in most cases, it’s important to be aware of this behavior to avoid any potential issues.

Top comments (1)

Collapse
 
sreno77 profile image
Scott Reno

Good info!