1. Code
  2. Python

Mathematical Modules in Python: Decimal and Fractions

Scroll to top
This post is part of a series called Mathematical Modules in Python.
Mathematical Modules in Python: Random
Mathematical Modules in Python: Statistics

Even the most basic mathematical operations can sometimes give an erroneous result. This happens due to limitations in storing the exact value of some numbers. You can overcome these limitations by using the decimal module in Python. Similarly, neither the math nor the cmath module that we learned about in our last tutorial can help us in doing fraction-based arithmetic. However, the fractions module in Python does exactly that.

In this tutorial, you will learn about both these modules and the different functions they make available.

Using the Decimal Module

Task Typical Functions  
Creating a Decimal Number Decimal() constructor  
Using Context to Set Rounding and Precision getcontext().prec, getcontext().rounding  
Math Operations on Decimals sqrt(), exp(), log()  
Working with Context in the Decimal Module setcontext(), localcontext()  

Using the Fractions Module

Task Typical Code  
Creating Fractions Fraction() constructor  
Arithmetic With Fractions +, -, *, / operators  
Numerator and Denominator Functions limit_denominator() function, numerator, denominator properties  
Fractions and the Math Module math.sqrt(), math.floor()  

Why Do We Need a Decimal Module?

You are probably wondering why we need a module to do basic arithmetic with decimal numbers when we can already do the same using floats.

Before I answer this question, I want you to take a guess about the output value if you type 0.1 + 0.2 in the Python console. If you guessed that the output should be 0.3, you will be surprised when you check out the actual result, which is 0.30000000000000004. You can try another calculation, like 0.05 + 0.1, and you will get 0.15000000000000002.

To understand what's going on here, try to represent 1/3 in decimal form, and you will notice that the number is actually non-terminating in base 10. Similarly, some numbers like 0.1 or 1/10 are non-terminating in base 2. Since these numbers still need to be represented somehow, a few approximations are made while storing them, which results in those errors.

The number 0.30000000000000004 is actually very close to 0.3, so we can get away with this approximation most of the time. Unfortunately, this approximation is not going to cut it when you are simulating a satellite launch or dealing with money. Another problem with these approximations is that the errors keep piling up.

To get precise results like the ones we are used to dealing with when doing calculations by hand, we need something that supports fast, correctly rounded, decimal floating-point arithmetic, and the decimal module does exactly that.

Using the Decimal Module

Before using the module, you need to import it. After that, you can create decimals from integers, strings, floats, or tuples.

Creating a Decimal Number

When the decimal is constructed from an integer or a float, there is an exact conversion of the value of that number. Take a look at the examples below to see what I mean:

1
from decimal import Decimal
2
3
Decimal(121)
4
# returns Decimal('121')

5
6
Decimal(0.05)
7
# returns Decimal('0.05000000000000000277555756')

8
9
Decimal('0.05')
10
# returns Decimal('0.05')

11
12
Decimal((0, (8, 3, 2, 4), -3))
13
# returns Decimal('8.324')

14
15
Decimal((1, (8, 3, 2, 4), -1))
16
# returns Decimal('-832.4')

As you can see, the value of Decimal(0.05) is slightly different from Decimal('0.05'). This means that when you add 0.05 and 0.1, you should use decimal.Decimal('0.05') and decimal.Decimal('0.1') to construct the decimals.

1
from decimal import Decimal
2
3
Decimal('0.05') + Decimal('0.1')
4
# returns Decimal('0.15')

5
6
Decimal(0.05) + Decimal(0.1)
7
# returns Decimal('0.1500000000000000083266726847')

Using Context to Set Rounding and Precision

Now that you can perform various operations on decimals, you might want to control the precision or rounding for those operations. This can be done by using the getcontext() function. This function allows you to get as well as set the value of the precision and rounding options, among other things.

Please keep in mind that both rounding and precision come into play only during arithmetic operations and not while you are creating the decimals themselves.

1
import decimal
2
from decimal import Decimal, getcontext
3
4
Decimal(1) / Decimal(13)
5
# returns Decimal('0.07692307692307692307692307692')

6
7
getcontext().prec = 10
8
9
Decimal(0.03)
10
# returns Decimal('0.02999999999999999888977697537')

11
12
Decimal(1) / Decimal(7)
13
# returns Decimal('0.1428571429')

14
15
getcontext().rounding = decimal.ROUND_DOWN
16
17
Decimal(1) / Decimal(7)
18
# returns Decimal('0.1428571428')

19

Math Operations on Decimals

You can also use some of the mathematical functions like sqrt(), exp(), and log() with decimals. Here are a few examples:

1
import decimal
2
from decimal import Decimal, getcontext
3
4
Decimal(2).sqrt()
5
# returns Decimal('1.414213562373095048801688724')

6
7
getcontext().prec = 4
8
9
Decimal('2').sqrt()
10
# returns Decimal('1.414')

11
12
Decimal('2000').log10()
13
# returns Decimal('3.301')

Working With Context in the Decimal Module

We briefly touched upon the concept of context in the previous section when we used the getcontext() function. The context objects in Python's decimal module are used to determine a lot of things like the precision, rounding rules, and exception raising behavior while performing arithmetic calculations.

You can get and set the current context for calculations using the getcontext() and setcontext() functions. Using the localcontext() function alongside the with statement allows you to temporarily change the context for calculations.

There are three built-in contexts in the module that you can use for your calculations. The BasicContext sets precision to nine and the rounding algorithm to ROUND_HALF_UP. The ExtendedContext also keeps precision at nine but sets the rounding algorithm to ROUND_HALF_EVEN. Finally, the DefaultContext sets the precision to 28 but keeps ROUND_HALF_EVEN as its rounding algorithm. Another difference among these contexts is the exception-raising behavior. No exceptions are raised with ExtendedContext. Three exceptions are present in the DefaultContext related to numerical overflow, invalid operation, and division by zero. Almost all exceptions are enabled for BasicContext.

This makes BasicContext ideal for debugging and ExtendedContext ideal for situations where you don't want to halt program execution. As you might have guessed, the DefaultContext is used as the default context for the calculations.

Here is an example of using different contexts to get different results for a simple division:

1
import decimal
2
from decimal import ROUND_DOWN, ROUND_UP, Decimal as D
3
4
dec_a = D('0.153')
5
dec_b = D('0.231')
6
zero = D('0')
7
8
print("No Context (Using Default):  ", dec_a/dec_b)
9
# No Context (Using Default):   0.6623376623376623376623376623

10
11
decimal.setcontext(decimal.BasicContext)
12
print("Basic Context: ", dec_a/dec_b)
13
# Basic Context:  0.662337662

14
15
decimal.setcontext(decimal.ExtendedContext)
16
print("Extended Context: ", dec_a/dec_b)
17
# Extended Context:  0.662337662

18
print("Extended Context: ", dec_b/zero)
19
# Extended Context:  Infinity

20
21
decimal.setcontext(decimal.DefaultContext)
22
print("Default Context: ", dec_a/dec_b)
23
# Default Context:  0.6623376623376623376623376623

24
25
with decimal.localcontext() as l_ctx:
26
    l_ctx.prec = 5
27
    l_ctx.rounding = ROUND_UP
28
29
    print("Local Context: ", dec_a/dec_b)
30
    # Local Context:  0.66234

Besides noticing the difference in precision and rounding algorithm for different contexts, you have probably also observed that a division by 0 under ExtendedContext did not raise an exception but output the result as Infinity.

A lot of functions in the decimal module also accept a context object as an argument for performing their calculations. This way, you can avoid constantly setting the context or precision values for computation.

1
import decimal
2
from decimal import Decimal as D
3
4
5
print(D('22').sqrt(decimal.BasicContext))
6
# 4.69041576

7
8
print(D('22').sqrt(decimal.ExtendedContext))
9
# 4.69041576

10
11
print(D('22').sqrt(decimal.DefaultContext))
12
# 4.690415759823429554565630114

13
14
with decimal.localcontext() as l_ctx:
15
    l_ctx.prec = 5
16
17
    print(D('22').sqrt(l_ctx))
18
    # 4.6904

Using the Fractions Module

Sometimes, you might face situations where you need to perform various operations on fractions or the final result needs to be a fraction. The fractions module can be of great help in these cases.

Creating Fractions

The fractions module allows you to create a Fraction instance from numbers, floats, decimals, and even strings. Just like the decimal module, there are a few issues with this module as well when it comes to creating fractions from floats. Here are a few examples:

1
from fractions import Fraction
2
from decimal import Decimal
3
4
Fraction(11, 35)
5
# returns Fraction(11, 35)

6
7
Fraction(10, 18)
8
# returns Fraction(5, 9)

9
10
Fraction('8/25')
11
# returns Fraction(8, 25)

12
13
Fraction(1.13)
14
# returns Fraction(1272266894732165, 1125899906842624)

15
16
Fraction('1.13')
17
# returns Fraction(113, 100)

18
19
Fraction(Decimal('1.13'))
20
# returns Fraction(113, 100)

Arithmetic With Fractions

You can also perform simple mathematical operations like addition and subtraction on fractions, just like regular numbers.

1
from fractions import Fraction
2
3
Fraction(113, 100) + Fraction(25, 18)
4
# returns Fraction(2267, 900)

5
6
Fraction(18, 5) / Fraction(18, 10)
7
# returns Fraction(2, 1)

8
9
Fraction(18, 5) * Fraction(16, 19)
10
# returns Fraction(288, 95)

11
12
Fraction(18, 5) * Fraction(15, 36)
13
# returns Fraction(3, 2)

14
15
Fraction(12, 5) ** Fraction(12, 10)
16
# returns 2.8592589556010197

Numerator and Denominator Functions

The module also has a few important methods like limit_denominator(max_denominator) which will find and return a fraction closest in value to the given fraction whose denominator is at most max_denominator. You can also return the numerator of a given fraction in the lowest term by using the numerator property and the denominator by using the denominator property.

1
from fractions import Fraction
2
3
Fraction('3.14159265358979323846')
4
# returns Fraction(157079632679489661923, 50000000000000000000)

5
6
Fraction('3.14159265358979323846').limit_denominator(10000)
7
# returns Fraction(355, 113)

8
9
Fraction('3.14159265358979323846').limit_denominator(100)
10
# returns Fraction(311, 99)

11
12
Fraction('3.14159265358979323846').limit_denominator(10)
13
# returns Fraction(22, 7)

14
15
Fraction(125, 50).numerator
16
# returns 5

17
18
Fraction(125, 50).denominator
19
# returns 2

Fractions and the Math Module

You can also use this module with various functions in the math module to perform fraction-based calculations.

1
import math
2
from fractions import Fraction
3
4
math.sqrt(Fraction(25, 4))
5
# returns 2.5

6
7
math.sqrt(Fraction(28,3))
8
# returns 3.0550504633038935

9
10
math.floor(Fraction(3558, 1213))
11
# returns 2

12
13
Fraction(math.sin(math.pi/3))
14
# returns Fraction(3900231685776981, 4503599627370496)

15
16
Fraction(math.sin(math.pi/3)).limit_denominator(10)
17
# returns Fraction(6, 7)

Final Thoughts

These two modules should be sufficient to help you perform common operations on both decimals and fractions. As shown in the last section, you can use these modules along with the math module to calculate the value of all kinds of mathematical functions in the format you desire.

In the next tutorial of the series, you will learn about the random module in Python.

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.