3.14 Using Type Decimal for Monetary Amounts

In this section, we introduce Decimal capabilities for precise monetary calculations. If you enter banking or other fields that require the accuracy provided by type Decimal, you should investigate Decimal’s capabilities in depth.

For most scientific and other mathematical applications that use numbers with decimal points, Python’s built-in floating-point numbers work well. For example, when we speak of a “normal” body temperature of 98.6, we do not need to be precise to a large number of digits. When we view the temperature on a thermometer and read it as 98.6, the actual value may be 98.5999473210643. The point here is that calling this number 98.6 is adequate for most body-temperature applications.

Floating-point values are stored in binary format (we introduced binary in the first chapter and discuss it in depth in the online “Number Systems” appendix). Some floating-point values are represented only approximately when they’re converted to binary. For example, consider the variable amount with the dollars-and-cents value 112.31. If you display amount, it appears to have the exact value you assigned to it:

In [1]: amount = 112.31

In [2]: print(amount)
112.31

However, if you print amount with 20 digits of precision to the right of the decimal point, you can see that the actual floating-point value in memory is not exactly 112.31—it’s only an approximation:

In [3]: print(f'{amount:.20f}')
112.31000000000000227374

Many applications require precise representation of numbers with decimal points. Institutions like banks that deal with millions or even billions of transactions per day have to tie out their transactions “to the penny.” Floating-point numbers can represent some but not all monetary amounts with to-the-penny precision.

The Python Standard Library2 provides many predefined capabilities you can use in your Python code to avoid “reinventing the wheel.” For monetary calculations and other applications that require precise representation and manipulation of numbers with decimal points, the Python Standard Library provides type Decimal, which uses a special coding scheme to solve the problem of to-the-penny precision. That scheme requires additional memory to hold the numbers and additional processing time to perform calculations but provides the to-the-penny precision required for monetary calculations. Banks also have to deal with other issues such as using a fair rounding algorithm when they’re calculating daily interest on accounts. Type Decimal offers such capabilities.3

Importing Type Decimal from the decimal Module

We’ve used several built-in types—int (for integers, like 10), float (for floating-point numbers, like 7.5) and str (for strings like 'Python'). The Decimal type is not built into Python. Rather, it’s part of the Python Standard Library, which is divided into modules—groups of related capabilities. The decimal module defines type Decimal and its capabilities.

To use capabilities from a module, you must first import the entire module, as in

import decimal

and refer to the Decimal type as decimal.Decimal, or you must indicate a specific capability to import using from…import, as we do here:

In [4]: from decimal import Decimal

This imports only the type Decimal from the decimal module so that you can use it in your code. We’ll discuss other import forms beginning in the next chapter.

Creating Decimals

You typically create a Decimal from a string:

In [5]: principal = Decimal('1000.00')

In [6]: principal
Out[6]: Decimal('1000.00')

In [7]: rate = Decimal('0.05')

In [8]: rate
Out[8]: Decimal('0.05')

We’ll soon use the variables principal and rate in a compound-interest calculation.

Decimal Arithmetic

Decimals support the standard arithmetic operators +, -, *, /, //, ** and %, as well as the corresponding augmented assignments:

In [9]: x = Decimal('10.5')

In [10]: y = Decimal('2')

In [11]: x + y
Out[11]: Decimal('12.5')

In [12]: x // y
Out[12]: Decimal('5')

In [13]: x += y

In [14]: x
Out[14]: Decimal('12.5')

You may perform arithmetic between Decimals and integers, but not between Decimals and floating-point numbers.

Compound-Interest Problem Requirements Statement

Let’s compute compound interest using the Decimal type for precise monetary calculations. Consider the following requirements statement:

  • A person invests $1000 in a savings account yielding 5% interest. Assuming that the person leaves all interest on deposit in the account, calculate and display the amount of money in the account at the end of each year for 10 years. Use the following formula for determining these amounts:

    a = p(1 + r)n

    where

    p is the original amount invested (i.e., the principal),

    r is the annual interest rate,

    n is the number of years and

    a is the amount on deposit at the end of the nth year.

Calculating Compound Interest

To solve this problem, let’s use variables principal and rate that we defined in snippets [5] and [7], and a for statement that performs the interest calculation for each of the 10 years the money remains on deposit. For each year, the loop displays a formatted string containing the year number and the amount on deposit at the end of that year:

In [15]: for year in range(1, 11):
   ...: amount = principal * (1 + rate) ** year
   ...: print(f'{year:>2}{amount:>10.2f}')
   ...:
 1   1050.00
 2   1102.50
 3   1157.62
 4   1215.51
 5   1276.28
 6   1340.10
 7   1407.10
 8   1477.46
 9   1551.33
10   1628.89

The algebraic expression (1 + r) n from the requirements statement is written as

(1 + rate) ** year

where variable rate represents r and variable year represents n.

Formatting the Year and Amount on Deposit

The statement

print(f'{year:>2}{amount:>10.2f}')

uses an f-string with two placeholders to format the loop’s output.

The placeholder

{year:>2}

uses the format specifier >2 to indicate that year’s value should be right aligned (>) in a field of width 2—the field width specifies the number of character positions to use when displaying the value. For the single-digit year values 19, the format specifier >2 displays a space character followed by the value, thus right aligning the years in the first column. The following diagram shows the numbers 1 and 10 each formatted in a field width of 2:

A diagram shows the numbers 1 and 10 each formatted in a field width of 2. There is a leading space before the 1.

You can left align values with <.

The format specifier 10.2f in the placeholder

{amount:>10.2f}

formats amount as a floating-point number (f) right aligned (>) in a field width of 10 with a decimal point and two digits to the right of the decimal point (.2). Formatting the amounts this way aligns their decimal points vertically, as is typical with monetary amounts. In the 10 character positions, the three rightmost characters are the number’s decimal point followed by the two digits to its right. The remaining seven character positions are the leading spaces and the digits to the decimal point’s left. In this example, all the dollar amounts have four digits to the left of the decimal point, so each number is formatted with three leading spaces. The following diagram shows the formatting for the value 1050.00:

A diagram shows the formatting for the value 1050.00. The field width is 10. There are 3 leading spaces to the left then 1050 decimal point 2 digits to the right of the decimal point are 0 0.

tick mark Self Check

  1. (Fill-In) A field width specifies the ___________ to use when displaying a value.
    Answer: number of character positions.

  2. (IPython Session) Assume that the tax on a restaurant bill is 6.25% and that the bill amount is $37.45. Use type Decimal to calculate the bill total, then print the result with two digits to the right of the decimal point.
    Answer:

    In [1]: from decimal import Decimal
    
    In [2]: print(f"{Decimal('37.45') * Decimal('1.0625'):.2f}")
    39.79