Python Round: Problems and Solutions

python roundYou probably learned how to round numbers in middle school, maybe even in grade school. If you want to round 4.85 to 1 decimal place, you should have 4.9, right? Not if you are dealing with computers. If you use Python to round 4.85 to 1 decimal place, you will get 4.8 as a result. This is really a computer problem, not a Python problem, but we will show you how to cope with this quirk and others if you are using Python. Get a background in Python with this course for beginners.

Some Background: Don’t Get Sunk by Floating Point Numbers

As mentioned, this problem is really a computer problem. A computer does not work in the number system we use, base 10. Instead, computers work in base 2. A computer can store integer numbers exactly, but numbers with decimal places cannot be represented exactly in base 2 on the computer. Numbers with decimal places are called floating point numbers in computer terms. You can get a review of floating point and other data types with this course. Let’s look at an example in the Python shell:

>>> round(4.85,1)

4.85

This result is wrong, according to the definition of the round() function. Halfway values are supposed to be rounded away from zero, so the response should have been 4.9. We can use the decimal module to view the real value that is stored for a floating point number. The decimal module has been available since Python 2.4.

>>> from decimal import Decimal

>>> Decimal(4.85)

Decimal (‘4.8499999999999996447286321199499070644378662109375’)

Notice the case sensitivity here. “decimal” with a lowercase d is the name of the entire module. It contains a constructor called Decimal with an uppercase D. Decimal “constructs” a number that is an equivalent true decimal number, not a floating point number. It shows us the value of the floating point number that is actually stored in computer memory.

The floating point number 4.85 is stored as a number slightly less than 4.85, as shown. When that number is rounded to 1 decimal place, it is rounded down to 4.8 and displayed. This problem is common to most computer processors due to how they work with floating point numbers.

This problem can also be seen when adding the number 0.2 to itself 10 times:

>>> sum = 0

>>> for i in range (10):

sum += 0.2 # hit return/enter twice to get back to the prompt

>>> sum

1.9999999999999998

However, in this case, if you use round(), the result will be what you expect

>>> round(sum,2)

2.0

A Solution: The Decimal Module

The decimal module was developed to implement the decimal arithmetic that we learned in school. It stores true base 10 decimal numbers. The cost is a little more processing time. Let’s look at an example where we are using dollar amounts. These are rounded to 2 decimal places, and in the case of a 5, the result is rounded away from zero, so 1.555 becomes 1.56 and -1.555 becomes -1.56. Suppose as a result of a calculation, we get the amount 6.22 which must be divided by 4:

>>> cost = 6.22

>>> cost #we echo the variable to check its value

6.22

>>> qcost = cost / 4

>>> qcost

1.555

>>> round(qcost,2)

1.55

Oops, there’s our floating point problem! Let’s bring in the decimal module.

>>> from decimal import * #we import all of the module, not just Decimal as before

>>> dcost = Decimal(cost)

>>> dcost

Decimal(‘6.21999999999999975131004248396493494510650634765625’)

Hey! How come it didn’t make a 2 place decimal? The answer is that Decimal numbers are constructed from string variables. We have to make a string out of cost, then convert to Decimal:

>>> cost = 6.22

>>> dcost = Decimal(str(cost))

>>> dcost

Decimal(‘6.22’)

>>> qdcost = dcost/4

>>> qdcost

Decimal(‘1.555’)

>>> round(qdcost,2)

Decimal(‘1.56’)

Yes! Let’s check another example:

>>> cost2 = 5.78

>>> dcost2 = Decimal(str(cost2))

>>> dcost2

Decimal(‘5.78’)

>>> qdcost2 = dcost2 /4

>>> qdcost2

Decimal(‘1.445’)

>>> round(qdcost2,2)

Decimal(‘1.44’)

Shoot! Now what? To see what is happening here, we must investigate the decimal module a bit more. The decimal module has a set of parameters called the context. Luckily, we imported all of the module, so we can see the parameters with the getcontext() method:

>>> getcontext()

Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[InvalidOperation, Inexact, FloatOperation, Rounded], traps=[InvalidOperation, DivisionByZero, Overflow])

The parameter we need stands out in uppercase: the rounding option. ROUND_HALF_EVEN is defined as follows:

If the value to be rounded is 5 then the preceding digit is examined. Even values cause the result to be rounded down and odd digits cause the result to be rounded up.

That’s the problem: In the second example the preceding digit was 4, which is even, so the result was rounded down. This option is used in science, where always rounding the mid value up could introduce a slight bias. More values would be rounded up than rounded down on average, so slightly higher values might result from a lot of calculations that are rounded. Here are the other choices for rounding with the decimal module:

ROUND_CEILING: Always round upwards towards infinity.
ROUND_DOWN: Always round toward zero.
ROUND_FLOOR: Always round down towards negative infinity.
ROUND_HALF_DOWN: Rounds away from zero if the last significant digit is greater than or equal to 5, otherwise toward zero.
ROUND_HALF_UP: Like ROUND_HALF_DOWN except if the last significant digit is 5 the value is rounded away from zero.
ROUND_UP: Round away from zero.
ROUND_05UP: Round away from zero if the last digit is 0 or 5, otherwise towards zero.

For money, we want the ROUND_HALF_UP option. We can set this with the getcontext() method:

>>> getcontext().rounding=ROUND_HALF_UP

>>> cost2 = 5.78

>>> dcost2 = Decimal(str(cost2))

>>> dcost2

Decimal(‘5.78’)

>>> qdcost2 = dcost2 /4

>>> qdcost2

Decimal(‘1.445’)

>>> round(qdcost2,2)

Decimal(‘1.45’)

Yes! But what about this “Decimal(‘1.45’)” thing? We want our user to see a final cost of 1.45. If we print() the answer, that problem is solved:

>>> rqdcost = round(qdcost2,2)

>>> print(rqdcost)

1.45

The moral here is don’t rely on the echo in the console to show you what will happen in the final program. Use print().

Other Solutions

If you are programming extensively with financial calculations, the python-money package and the py-moneyed packages are available. They provide classes for currency and money that use the decimal module. The py-moneyed package is more recent. Once the package has been added to your system, Money variables can be assigned and used without fear of floating point problems:

>>> from moneyed.classes import Money

>>> my_price = Money(amount =’21.4′, currency=’USD’)

If you are not using monetary data, but you need to specify how numbers are rounded, you can try some methods from the math module. This is included with Python and can be imported. This course has a section on math functions. The relevant methods are:

math.ceil(x): Return the ceiling of x, the smallest integer greater than or equal to x.

math.floor(x): Return the floor of x, the largest integer less than or equal to x.

math.trunc(x): Return the Real value x truncated to an integer.

>>> import math

>>> math.ceil (1.234)

2

>>> math.ceil (-1.234)

-1

>>> math.floor(1.234)

1

>>> math.floor(-1.234)

-2

>>> math.trunc(1.234)1

>>> math.trunc(-1.234)

-1

This should give you a good introduction to the perils of Python rounding, and when you’re ready, you can move your Python skills to the next level with this course.