When working with decimal numbers in Python, we usually turn to floats. Floats serve us well for most purposes, such as simple divisions, but they do have limitations that can become incredibly problematic for certain use cases. They're simply not precise enough. However, floats aren't our only option, and today we're going to be taking a look at the `decimal` module, and why you might use it.

## A Quick Look at Binary

Before we can properly understand the problem with floats, we need to do a quick review of how numbers are represented in binary.

The number system that we use in everyday life is a base-10 number system, also known as a decimal number system. We use ten unique numerals in various combinations to represent all numbers. Binary, on the other hand, is a base-2 number system and only uses two unique numerals: generally 0 and 1. When numbers are stored in our computers, they are stored in this binary format.

A binary number might look something like this: 10101101, which happens to be 173.

So how do we get 173 out of 10101101?

Binary works in powers of 2, so the rightmost 1 in 10101101, represents 1 x 2⁰. We then take a step to the left, where we find a 0. This 0 represents 0 x 2¹, which is 0 x 2. Another step to the left and we find another 1, this time representing 1 x 2², which is 4. Every step to the left, the power increases by 1.

In total we have something that looks like this:

(1 × 2⁷) + (0 × 2⁶) + (1 × 2⁵) + (0 × 2⁴) + (1 × 2³) + (1 × 2²) + (0 × 2¹) + (1 × 2⁰)

Which is:

(1 x 128) + (0 x 64) + (1 x 32) + (0 x 16) + (1 x 8) + (1 x 4) + (0 x 2) + (1 x 1)

If we were to add all this up, we'd get 173. As you can see, binary representations of numbers tend to be a great deal longer than decimal representations, but we can ultimately represent any integer in this way.

### Fractions in Binary

So, we've done a quick refresher on how integers can be represented in binary, but what about fractions? As it turns out, it works much the same way, just with negative powers.

For example, 2⁻¹ is ½, and 2⁻² is ¼, which means we can now represent 0.75, 0.5, and 0.25. Using progressively greater negative powers we can represent all manner of decimal numbers.

However, just as there are numbers we can't represent with a finite number of decimal numerals (e.g. ⅓), there are also numbers we can't represent in binary. For example, the number 0.1 has no finite binary representation.

## Floats in Python

So what happens when we write `0.1` in Python? Let's take a look:

``````print(f"{0.1:.20f}")  # 0.10000000000000000555
``````

For those of you not familiar with the syntax above, the `:.20f` is a way of telling Python we want 20 digits after the decimal point for this float. We have a post you can look at below:

As we can see, we don't actually get `0.1`: we get a close approximation of `0.1`. Unfortunately, sometimes a close approximation just isn't good enough.

This is particularly prevalent when performing comparisons:

``````a = 10
b = a / 77
c = b * 77

if a != c:
print("Things got weird...")

# Things got weird...
``````

If we print `a` and `c`, we can see what happened:

``````print(f"{a:.20f}")  # 10.00000000000000000000
print(f"{c:.20f}")  #  9.99999999999999822364
``````

This approximation error can get hugely compounded over a series of operations, meaning we can actually end up with quite significant differences between numbers that should be identical.

You can read more about these issues in the Python documentation: https://docs.python.org/3/tutorial/floatingpoint.html

## Enter `decimal`

As mentioned at the start of this post, Python does have another way of dealing with decimal numbers, which is the `decimal` module. Unlike floats, the `Decimal` objects defined in the `decimal` module are not prone to this loss of precision, because they don't rely on binary fractions.

## Creating `Decimal` objects

Before we dive into `Decimal` objects, let's look at how to defined them. There are actually several ways to do this.

### Using an integer

The first way to create a `Decimal` object us to use an integer. In this case, we simply pass the integer in as an argument to the `Decimal` constructor:

``````import decimal

x = decimal.Decimal(34)  # 34
``````

We can now use `x` just like any other number, and we'll get a `Decimal` object back when performing mathematical operations:

``````x = decimal.Decimal(34)

print(x + 7)         # 41
print(type(x + 7))   # <class 'decimal.Decimal'>

print(x // 7)        # 4
print(type(x // 7))  # <class 'decimal.Decimal'>
``````

### Using a string

Perhaps a little surprisingly, one of the easiest ways to make a `Decimal` object with fractional components is using a string. We just need to pass a string representation of the number to `Decimal` and it'll take care of the rest:

``````x = decimal.Decimal("0.1")

print(x)            # 0.1
print(f"{x:.20f}")  # 0.10000000000000000000
``````

As we can see, printing `x` to 20 decimal places here gives us 19 zeroes: we don't end up with some random 5s at the end like we did when using float.

If you need a precise decimal representation of a number, using strings to create your `Decimal` objects is a very simple way to achieve this.

### Using a float

It's also possible to create a `Decimal` object from a float, but I'd generally advise against doing this. The example below should make it clear as to why:

``````x = decimal.Decimal(0.1)
print(x)  # 0.1000000000000000055511151231257827021181583404541015625
``````

Converting straight from a float causes our `Decimal` object to inherit all of the imprecision that we were trying to avoid in the first place. There may be cases where you want to preserve this imprecision for some reason, but most of the time, that's not the case.

### Using a tuple

Perhaps the most complicated method of creating a `Decimal` object is using a tuple, and this method gives us some insight into how `Decimal` objects work under the hood.

The tuple we provide to `Decimal` has three parts. The first element in the tuple is either the number `0` or `1`, and this represents the sign of the number (whether it's positive or negative). A zero in this first position indicates a positive number, while a one represents a negative number.

The second item in the tuple is another tuple, and this contains all of the digits in the resulting number. For example, the number `1.3042` has the digits `(1, 3, 0, 4, 2)`.

The third and final item in the tuple is an exponent. This tells the `Decimal` object how many places we need to shift the digits around the decimal point. An exponent of `-3` is going to cause the digits to shift 3 spaces to the right, giving us `13.042`, while an exponent of `2` is going to give us `1304200`.

A complete example for the number `-13.042` looks like this:

``````x = decimal.Decimal((1, (1, 3, 0, 4, 2), -3))
print(x)  # -13.042
``````

A really important thing to note here is that we do need the brackets around the outer tuple, as `Decimal` expects this tuple as a single argument. Removing the outer brackets will cause an error.

While relatively complicated, the tuple syntax is best suited to creating `Decimal` objects programmatically.

## The `Context` object

One of the cool things about the `decimal` module is that it allows us to define the behaviour of `Decimal` objects in various ways. We can specify degrees of precision, limits for exponents, and even rounding rules.

In order to do this, we're generally going to be working with the `getcontext` function, which allows us to see and modify the `Context` object for the current thread.

Let's take a look at what a default `Context` object looks like:

``````import decimal

print(decimal.getcontext())
# Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
``````

As you can see, we have all kinds of properties that we can set to change how our `Decimal` objects work.

For example, we might decide we want 10 significant figures worth of precision, which we'd set like this:

``````import decimal

decimal.getcontext().prec = 10
x = decimal.Decimal(1) / decimal.Decimal(7)

print(x)  # 0.1428571429
``````

Note that this new precision is only relevant during mathematical operations. We can define a `Decimal` object with arbitrary precision, even if we set a lower level of precision using `getcontext`.

``````import decimal

decimal.getcontext().prec = 10

x = decimal.Decimal("1.045753453457657657")
y = decimal.Decimal(7)

print(x)      # 1.045753453457657657
print(y)      # 7
print(x / y)  # 0.1493933505
``````

As I mentioned earlier, we can also define how rounding works for `Decimal` objects, which can be very useful. By default, Python uses bankers' rounding, which differs somewhat from the kind of rounding we learn about in school.

We talk about bankers' rounding here if you're not familiar with it: https://blog.tecladocode.com/rounding-in-python/

Using `getcontext` we might change Python's rounding behaviour to always round decimals ending in `5` away from `0`, which is how we round in everyday life:

``````import decimal

decimal.getcontext().prec = 1

x = decimal.Decimal(5)
y = decimal.Decimal(2)

print(x / y)  # 2

decimal.getcontext().rounding = decimal.ROUND_HALF_UP

print(x / y)  # 3
``````

A number of different rounding behaviours are defined in the `decimal` module using constants. You can read more about the options available here: https://docs.python.org/3.7/library/decimal.html#module-decimal

## Some useful `decimal` methods

The `decimal` module comes with a number of handy methods for working with `Decimal` objects. Here are a few that you might want to check out.

### `sqrt`

The `sqrt` method allows us to get the square root of any `Decimal` with the designated level of precision.

``````import decimal

decimal.getcontext().prec = 15
x = decimal.Decimal(2).sqrt()

print(x)  # 1.41421356237310
``````

### `quantize`

The `quantize` method is used to change a `Decimal` object to a new exponent. For example, say we have the number `1.41421356` and we want to change this number to match the pattern `1.000`, we could write the following:

``````import decimal

x = decimal.Decimal("1.41421356")
y = x.quantize(decimal.Decimal("1.000"))

print(y)  # 1.414
``````

One thing to be aware of with the `quantize` method is that it will raise an `InvalidOperation` exception if the final values exceeds the defined level of precision.

``````import decimal

decimal.getcontext().prec = 1

x = decimal.Decimal("1.41421356")
y = x.quantize(decimal.Decimal("1.000"))

# decimal.InvalidOperation: [<class 'decimal.InvalidOperation'>]
``````

### `as_tuple`

The `as_tuple` method gives us a tuple representation of the `Decimal` object, just like when we create one using the tuple syntax.

``````import decimal

x = decimal.Decimal("1.41421356")
print(x.as_tuple())

# DecimalTuple(sign=0, digits=(1, 4, 1, 4, 2, 1, 3, 5, 6), exponent=-8)
``````

There are many other methods available for `Decimal` objects, and I'd recommend taking a look at this section of the documentation to learn more: https://docs.python.org/3.7/library/decimal.html#decimal-objects

## So, should we just use `decimal`?

Not necessarily. While the `decimal` module is certainly very neat, and it has the benefit of absolute precision, this precision comes at a cost. In this case, the cost is twofold.

Firstly, using the `decimal` module is much harder than using floats. This much is obvious as soon as we look at the `DecimalTuple` object above, and we also have things like contexts to consider on top of this. You simply need to know more in order to even make use of the module.

The second issue is perhaps more pressing, however, and that's down to performance. `decimal` operations are likely to be around 3 times slower than operations using floats, so if you're dealing with a performance sensitive part of your application, where absolute precision isn't important, you might want to stay clear of `decimal`. This is particularly true if you're still working with Python 2, as the operations are then likely to be a couple of hundred times slower!

The `decimal` module is a great tool to have in our toolkit, but we should always be aware of whether we truly need it for a particular application.

## Wrapping up

That's it for this post on floats and decimals. There's definitely a lot more to learn, so once again, take a look at the documentation! The `decimal` module documentation can be a little intimidating, but hopefully what we've talked about here makes you well prepared to dive right in.

If you've been enjoying our posts, and you want to take your Python to the next level, be sure to check out our Complete Python Course.