Python for Programmers: Decimals
Welcome to the Decimals lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
We've seen integers (
int) and floating point numbers (float). These are sufficient for most of our needs, but both types have limitations.Integers' limitation is that they're always whole numbers. Floats have two limitations. First, they have an upper limit. Second, as we saw in an earlier lesson, they're imprecise.
>
0.1 + 0.2Result:
>
0.1 + 0.2 == 0.3Result:
False
In some situations, imprecision is fine. For example, if we draw a video game character 0.00000000000000004 pixels away from where it should be, that's OK.
In other situations, imprecision is unacceptable. If we're working on billing code, it's crucial that we charge our customers exactly the right amount!
Fortunately, Python gives us an alternative that addresses floating point imprecision: the
decimalmodule. Like floats,Decimalobjects have a decimal point. But unlike floats,Decimals have unlimited precision and no maximum value.We can build
Decimals from strings or from numbers, like ints and floats. Then we can perform mathematical operations (like addition), which return moreDecimals. When we need to see a decimal's value, we can callstr(some_decimal)to turn it into a string.>
from decimal import Decimalthree_tenths = Decimal("0.1") + Decimal("0.2")str(three_tenths)Result:
'0.3'
It's also possible to call
Decimal(some_float), but that's usually a bad idea.Decimal("0.1")gives us 0.1, butDecimal(0.1)gives us a slightly different number because the float was already imprecise before we passed it toDecimal.>
from decimal import Decimalstr(Decimal("0.1"))Result:
>
from decimal import Decimalstr(Decimal(0.1))Result:
>
from decimal import DecimalDecimal("0.1") == Decimal(0.1)Result:
False
There's another, related problem: some specific floating point numbers are precise, but others aren't. If we only check numbers that happen to be precise, then we may not notice the precision problems.
>
from decimal import DecimalDecimal("20.0") == 20.0Result:
>
from decimal import DecimalDecimal("0.7") == 0.7Result:
Here's a simple rule: when precision matters, don't create
Decimals from floats!Fortunately, because mixing decimals and floats is unsafe, Python tries to help us prevent it. When using mathematical operators like
*, mixing decimals and floats is an error.>
from decimal import DecimalDecimal("20") * 0.1Result:
TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float'
What about integers? Because integers are precise, it's always safe to pass an integer to
Decimal(). For the same reason, it's also safe to mix decimals and integers.>
from decimal import DecimalDecimal("2") * 2 == 4 == Decimal("4")Result:
True
So far, decimals seem better in every way! But they do involve trade-offs. For example, sometimes decimals show more decimal places than we'd expect.
In the next example, we get a decimal value that's equal to 0.3. But when we convert it into a string, we get
"0.30".>
from decimal import Decimalresult = Decimal("0.3") + Decimal("0.05") - Decimal("0.05")str(result)Result:
'0.30'
0.30 is equal to 0.3, so this result is still correct. The final result has two decimal places because one of the values in the calculation is
0.05, with two decimal places. The extra decimal place may be desirable or not, depending on our use case.>
from decimal import Decimalstr(Decimal("0.001") * 1000)Result:
While
Decimals are "more precise" for a lot of common math, they do have limits. If we do a calculation that has no exact decimal representation,Decimalobjects are forced to round.For example, 1/3 is 0.3333333333..., which can't be represented exactly as a decimal.
>
from decimal import Decimalone_third = Decimal(1) / Decimal(3)should_be_one = one_third * Decimal(3)str(should_be_one)Result:
>
from decimal import DecimalDecimal(1) == 1Result:
True
>
from decimal import Decimalone = Decimal(1)(one / 3) * 3 == 1Result:
False
The same problem exists for irrational numbers, like pi (3.14159...) or e (2.71828...).
You might wonder: if
Decimals are more precise than floats, why don't we always useDecimals? The biggest reason is performance. CPUs have built-in floating point units, which make floating point numbers very fast. But no CPU has built-in support for Python's decimal module. Even if they did, decimals with unlimited precision could never be as fast as floats with limited precision.We can see this by writing a quick benchmark. We'll write a loop with floats, then write the same loop with decimals. (The next example uses the built-in Python
timemodule to benchmark the two operations.)>
from time import timevalue = 0.99999result = valuestart = time()while result > 0.000001:result *= valuetime() - startResult:
>
from decimal import Decimalfrom time import timevalue = Decimal("0.99999")result = valuestart = time()while result > 0.000001:result *= valuetime() - startResult:
The decimal version took 651 ms, vs. only 64 ms for the float version, a difference of about 10x.
(Unlike most of our code examples, we hard-coded the results above based on the times that we got when running the code. If you try this test, you may get shorter or longer times depending on your computer's hardware, operating system, and Python version.)
Here's a code problem:
The
price_catalogdictionary below stores its prices as floating point numbers. This is a very bad idea, because floating point numbers aren't precise! Before changing the code, try running it. You'll see that the answer is slightly incorrect due to floating point imprecision.Modify the dictionary to store the prices as decimals instead. Decimals are always precise, so the amounts will be correct.
Two things to remember: you'll need to import
Decimal, and simply callingDecimal(some_float)is still imprecise.from decimal import Decimalprice_catalog = {"ham": Decimal("3.30"),"tomatoes": Decimal("1.10"),}orders = [("ham", 3), ("tomatoes", 2)]total = 0for product, amount in orders:total += price_catalog[product] * amountstr(total)- Goal:
"12.10"
- Yours:
"12.10"
There's no universal number type that works perfectly in every situation; each type has its own trade-offs! Integers are fast with no upper limit, but can only represent whole numbers. Floats are fast and can represent decimal values, but they have an upper limit and are imprecise. Decimals have no upper limit and can represent any decimal number to arbitrary precision, but they're significantly slower than ints or floats, and still can't represent some numbers like 1/3 or pi.