Execute Program

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.2
    Result:
  • >
    0.1 + 0.2 == 0.3
    Result:
    FalsePass Icon
  • 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 decimal module. Like floats, Decimal objects 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 more Decimals. When we need to see a decimal's value, we can call str(some_decimal) to turn it into a string.

  • >
    from decimal import Decimal

    three_tenths = Decimal("0.1") + Decimal("0.2")
    str(three_tenths)
    Result:
    '0.3'Pass Icon
  • It's also possible to call Decimal(some_float), but that's usually a bad idea. Decimal("0.1") gives us 0.1, but Decimal(0.1) gives us a slightly different number because the float was already imprecise before we passed it to Decimal.

  • >
    from decimal import Decimal

    str(Decimal("0.1"))
    Result:
  • >
    from decimal import Decimal

    str(Decimal(0.1))
    Result:
  • >
    from decimal import Decimal

    Decimal("0.1") == Decimal(0.1)
    Result:
    FalsePass Icon
  • 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 Decimal

    Decimal("20.0") == 20.0
    Result:
  • >
    from decimal import Decimal

    Decimal("0.7") == 0.7
    Result:
  • 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 Decimal

    Decimal("20") * 0.1
    Result:
    TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float'Pass Icon
  • 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 Decimal

    Decimal("2") * 2 == 4 == Decimal("4")
    Result:
    TruePass Icon
  • 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 Decimal

    result = Decimal("0.3") + Decimal("0.05") - Decimal("0.05")
    str(result)
    Result:
    '0.30'Pass Icon
  • 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 Decimal

    str(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, Decimal objects are forced to round.

  • For example, 1/3 is 0.3333333333..., which can't be represented exactly as a decimal.

  • >
    from decimal import Decimal

    one_third = Decimal(1) / Decimal(3)
    should_be_one = one_third * Decimal(3)
    str(should_be_one)
    Result:
  • >
    from decimal import Decimal

    Decimal(1) == 1
    Result:
    TruePass Icon
  • >
    from decimal import Decimal

    one = Decimal(1)
    (one / 3) * 3 == 1
    Result:
    FalsePass Icon
  • 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 use Decimals? 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 time module to benchmark the two operations.)

  • >
    from time import time

    value = 0.99999
    result = value

    start = time()
    while result > 0.000001:
    result *= value
    time() - start
    Result:
  • >
    from decimal import Decimal
    from time import time

    value = Decimal("0.99999")
    result = value

    start = time()
    while result > 0.000001:
    result *= value
    time() - start
    Result:
  • 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_catalog dictionary 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 calling Decimal(some_float) is still imprecise.

    from decimal import Decimal

    price_catalog = {
    "ham": Decimal("3.30"),
    "tomatoes": Decimal("1.10"),
    }
    orders = [("ham", 3), ("tomatoes", 2)]
    total = 0
    for product, amount in orders:
    total += price_catalog[product] * amount
    str(total)
    Goal:
    "12.10"
    Yours:
    "12.10"Pass Icon
  • 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.