Python Numerics
9 min read

Python Numerics

Python Numerics

Following on from the basic info in Numerics, let's talk a little about how Python handles some of these types, along with what you might need to think about.

Integers

Once upon a time Python was a little confused when it came to integers, and had 2 integer types: int and long. The type of an integer value then depended on just how big the number was.

Fortunately, this was changed way back in 2001, when the 2 types were unified under PEP 287

If you don't know what PEPs are, they're basically just suggestions for improving Python. Some get done, some don't. We'll probably cover some of them in more detail in future posts.

So now you only have the one int type to worry about.

Floats

Floating point nummbers are a little notorious when it comes to programming, and Python is not alone in showing some, shall we say, unusual results sometimes.

To illustrate this, let's look at a specific example.

In [1]: 31.2 / 12
Out[1]: 2.6

Nothing strange there, right? You can verify this on a calculator if you like, or work it out by hand. Each time you should see that 31.2 divided by 12 gives you the answer 2.6.

So, what happens if we do this?

In [2]: 12 * 2.6

You'd expect the answer to be 31.2, right? After all, it's simply reversing the previous calculation.

Unfortunately, you'd be wrong. This is what you get:

In [2]: 12 * 2.6
Out[2]: 31.200000000000003

Huh? That can't be right, surely? Aren't computers supposed to be accurate? Even I - a lowly human with a squishy brain - can work that out on a scrap of paper and get the right answer. So why can't Python do it?

Don't be too harsh on poor old Python here. It's not the language that's to blame. This is related to how the computer itself stores floating point numbers. At this point, it's not worth going into too much detail about exactly what is going on; we can just say that it's really down to the fact that computers only store binary values. The important thing here is that you know this can happen, and take it into account if you need to use floating point numbers.

How can I avoid this strange behaviour?

Often, it's pretty simple. Very few people actually need much accuracy beyond 2 or 3 decimal places. Let's say you want to store the price of something in a variable, and it costs €3.99. You're worried that when somebody buys more than one of this item, something like this might happen:

In [3]: 3.99 * 5
Out[3]: 19.950000000000003

And, of course, you'd be right. That is exactly what would happen.

There's really no need to store this price as a floating point number though. Instead of storing your prices as Euros, store them as cents. This gets rid of the need for floating point numbers, and you can then store all prices as integers: a numeric type that is always going to give you the right answer when you multiply it out.

In [4]: 399 * 5
Out[4]: 1995

All you need to do then is remember to divide by 100 when it comes to displaying your prices as Euros, but any core system calcualtions will all be integer calculations.

In most cases, this approach will give you exactly what you need. If not, you could choose to round off any floating point errors (they will always be rather small, so rounding to eg. 2 decimal places will pretty much always give you the right answer). The problem with this approach is you rely on everybody remembering to round things off properly before feeding results into another calculation. Forget this a few times and a tiny floating point error could possibly become significant.

Python does provide another option though, if you really want to work with decimals.

Decimals

Python provides a decimal library to handle just this problem. It handles exactly the same type of numbers as the default float type, but without the problematic rounding issues.

Python provides a lot of built in functionality, but not all of it can just be used. Sometimes you need to import a bit of the standard library first. This is mainly to avoid having to load lots of functionality that you won't be using into memory.
This is done using the import statement. We will cover this in more detail in a future post.

This type is not used by default though, and needs to be imported and used very specifically.

In [5]: import decimal

In [6]: decimal.Decimal(3.99) * 5
Out[6]: Decimal('19.95000000000000106581410364')

"What gives?", you say? I said using the decimal library would handle this for me, and that looks even worse than the floating point answer!

Yes, true. The decimal library still uses floating points underneath. It doesn't really have a lot of choice - that is how computers work! The difference is: we can tell the computer how many significant figures to use.

In [7]: decimal.getcontext().prec = 2

In [8]: decimal.Decimal(3.99) * 5
Out[8]: Decimal('20')

Better? No? Ah, I see your confusion. That prec property that we are using to specify the precision is not the number of decimal places. It is the number of significant figures. You do need to be a little careful there.

In [9]: decimal.getcontext().prec = 4

In [10]: decimal.Decimal(3.99) * 5
Out[10]: Decimal('19.95')

That's better, right? Just be a little careful, and make sure you understand exactly what significant figures actually means.

In [11]: decimal.Decimal(3.99) * 35
Out[11]: Decimal('139.7')

Significant figures does not refer to the decimal precision. It refers to the number of digits in a number that have some meaning.

As soon as we go from a number in the 10s to a number in the 100s, our 4 significant digits suddenly only allows for one decimal place.

Examples of significant digits

28.35 has 4 significant digits - The 10s, the units, and 2 decimal places.
178.3 also has 4 significant digits - The 100s, the 10s, the units and 1 decimal place.
17.835, 178.35, 1783.5, and 17835 all have 5 significant digits.

Complex numbers

To understand the basics of complex numbers from a mathematical perspective, take a look at our post: Complex Numbers. All of what is described there applies to the handling of complex numbers in Python as well.

One difference in notation is that Python uses j instead of i to denote the imaginary part of a complex number. This is because Python follows the convention generally used within engineering, not mathematics. Beyond the letter used though, the difference is semantic, and you can safely ignore it. Just remember to define your complex numbers in Python as a + bj rather than a + bi.

One other minor thing to note when specifying a complex number, is that if you have a number such as a + j, you will need to specify the 1 in the imaginary part. It cannot be assumed as it might be in mathematics.

In [1]: a = 2 + j
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-9675d70e8cdf> in <module>
----> 1 a = 2 + j

NameError: name 'j' is not defined

The reason for this is made clear with the error message NameError: name 'j' is not defined - without prefixing the j with a numeric quantity, Python will treat it as a variable, and since j has not been defined previously, this causes a crash.

If j is already defined, this will instead produce a result that you probably didn't want.

In [2]: j = 12

In [3]: a = 1 + j

In [6]: a
Out[6]: 13

Python needs all variables to be defined before they are used!

Even with j defined already, specifying a complex number in the "correct" way, means that Python understands you.

In [4]: a = 2 + 1j

In [5]: a
Out[5]: (2+1j)

Absolute value / magnitude

In the list of operations below there is an abs() operation that is listed as absolute value / magnitude of a number. For any "simple" number - that is, any number with a purely numeric part - this is the positive value of that number, as you can see from the examples.

For complex numbers, it is the magnitude. This is a concept used when working with things like vectors and matrices. For the moment, we will not go into too much detail here (perhaps a future post on the mathematics of magnitude), but it is useful to know that the magnitude of a complex number a + bj is defined as:
$$
\sqrt{a^2 + b^2}
$$
You may think this looks a little familiar, and you'd be right.

Operations

So, what can we do with numbers? All the standard mathematical operations for starters, plus a couple you may be less familiar with.

Operation Description Alternative
+ Addition
- Subtraction
* Multiplication
** To the power of pow(a, b) == a ** b
/ Division
// Floored quotient divmod(a, b) == (a // b, a % b)
% Modulo (remainder)
abs() Absolute value / magnitude

There are some terms here that may be less than familiar.

a to the power of b means a multiplied by itself b times. pow(a, b) is simply an alternative way to write this in Python. It will provide exactly the same result as a ** b.

\begin{equation} 3 ** 4 == pow(3, 4) == \\ \underbrace{3 * 3 * 3 * 3}_\text{3 * itself 4 times} \end{equation}

Note the use of "double equals" signs here. This is because Python distinguishes between an assignment and an equality check.
To assign a value to a variable a single equal sign would be used (a = 4).
To check whether to values are equal, the double equals sign is used. This would usually be used in a conditional statement when checking a value, something else we will cover later (if a == 4:).

A floored quotient is the number of times one number divides entirely into another.

13 // 5 == 2 because 13 contains two whole "fives", with 3 left over.

The modulo is then the remainder, or "left over" amount when doing the same calculation.

13 % 5 == 3 becuase there is a remainder of 3 when dividing 13 by 5.

divmod is simply an easy way to combine both these calculations if you need them at the same time. It returns a tuple of 2 numbers: the floored quotient and the modulo.

divmod(13, 5) == (2, 3)

Don't worry about what a tuple is right now. We will cover this later. Just think of it as a group of values for the moment. It's a convenient way for Python to give us more than one value at a time.

Examples

Operation Integer Float Complex
a + b 1 + 6 => 7 1.3 + 2.6 => 3.9 (1 + 3j) + (3 - 2j) => (4 + 1j)
a - b 3 - 7 => -4 2.6 - 1.3 => 1.3 (1 + 3j) - (3 - 2j) => (-2 + 5j)
a * b 3 * 8 => 24 2.6 * 1.2 => 3.12 (1 + 3j) * (3 + 2j) => (-3 + 11j)
a / b 24 / 3 => 8 5.04 / 1.2 => 4.2 (-3 + 11j) / (3 + 2j) => (1+3j)
a ** b 3 ** 4 => 1.2 ** 2 => 1.44 (2 + 3j) * (3 + 4j) => (-6 + 17j)
a // b 11 // 3 => 3 6.2 // 1.2 => 5.0 Not supported for complex numbers
a % b 11 // 3 => 2 6.2 % 1.2 => 0.2 Not supported for complex numbers
abs(a) abs(-73) => 73 abs(-12.3) => 12.3 abs(3 + 4j) => 5.0

Converting numbers

When deciding what type a number (or any value, but numerics for this post) is, Python makes a "best guess".

In [1]: a = 7

In [2]: type(a)
Out[2]: int

In [3]: a = 7.1

In [4]: type(a)
Out[4]: float

This "best guess" is commonly known as "duck typing". If it walks like a duck, and quacks like a duck, assume it is a duck.

So what if I need to have this number as a different type? Well, there are simple built-in functions that allow you to convert them.

In [5]: b = 12

In [6]: c = float(b)

In [7]: c
Out[7]: 12.0

In [8]: type(c)
Out[8]: float

In [9]: c = complex(b)

In [10]: c
Out[10]: (12+0j)

In [11]: type(c)
Out[11]: complex

In [12]: b = 13.7

In [13]: c = int(b)

In [14]: c
Out[14]: 13

In [15]: type(c)
Out[15]: int

Note that converting a float to an integer will take only the integer component. It will not round up or down. This applies whether the number is positive or negative.

You can check the type of a variable or value in Python by calling type() on it.

The only issue you may come across is attempting to convert a complex number to a non-complex number, even if there is no imaginary part to it.

In [16]: c = (12.3 + 0j)

In [17]: c
Out[17]: (12.3+0j)

In [18]: type(c)
Out[18]: complex

In [19]: d = float(c)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-22-b91b5674e50b> in <module>
----> 1 d = float(c)

TypeError: can't convert complex to float

Even though 12.3 == 12.3 + 0j from a purely mathematical point of view, Python does not allow this. Instead, you can check the real and imaginary parts entirely seperately.

In [20]: c.real
Out[20]: 12.3

In [21]: c.imag
Out[21]: 0.0

This allows you to check whether the imaginary part is zero, and take only the non-complex (real) part of the number, if you need to.

Enjoying these posts? Subscribe for more