Introduction
Data types are a fundamental concept in programming. They form the foundational elements of code. Among them, numeric data types are especially important because they enable programmers to perform a wide range of mathematical operations, from simple arithmetic to complex scientific calculations.
In Python, this foundation is not only robust but also very versatile. The language offers a diverse set of numeric types, each designed to address specific mathematical needs.
In the following sections we will explore, in some detail, the most commonly used numeric data types in Python.
Integers
Of all numeric data types in Python, integers are the most straightforward. Because of Python's dynamic nature, creating integers is as simple as:
>>> a = 3
This simple line of code not only assigns the value '3' to a but also defines the internal type of the variable.
You can confirm it by using the type
function:
>>> a
3
>>> type(a)
<class 'int'>
Unsurprisingly, we can create positive or negative integers.
>>> x = 12
>>> y = -50
>>> z = 0
But, unlike some other programming languages, Python's integers have arbitrary precision. This means they can represent very large (or very small) numbers without any loss of precision.
>>> a = 1234567890123456789012345678901234567890
>>> a + 1
1234567890123456789012345678901234567891
Python also allows you to represent integers in different bases using prefixes.
>>> 0b1010 // Binary
10
>>> 0o0234 // Octal
156
>>> 0xFF // Hexadecimal
255
And you can use bitwise operators for performing operations on individual bits:
>>> x = 5
>>> y = 3
>>> bin(x), bin(y)
('0b0101', '0b0011')
>>> bitwise_and = x & y # 0b0001 (1 in decimal)
>>> bitwise_or = x | y # 0b0111 (7 in decimal)
>>> bitwise_xor = x ^ y # 0b0110 (6 in decimal)
>>> bitwise_not_x = ~x # -6 (in decimal)
>>> left_shift = x << 1 # 0b1010 (10 in decimal)
>>> right_shift = x >> 1 # 0b0010 (2 in decimal)
Integers in Python are immutable. This means that their value cannot be changed after they are created. Any operation that seems to modify an integer actually creates a new integer (the id function returns the unique identity of x).
>>> x = 500
>>> id(x)
4331970512
>>> x = x + 1
>>> id(x)
4331970992
One curiosity in CPython is that integer variables within the range -5 to 256 share the same id. This is for performance reasons as small numbers are commonly used in many programs.
>>> a = 10
>>> b = 10
>>> id(a) == id(b) == id(10)
True
Tip: sys.int_info
In reality, there are some limits on how big (or small) your computer can handle int
numbers.
In the sys
module you can find the int_info
tuple
which provides us with the limits of the platform.
>>> import sys
>>> sys.int_info
>>> sys.int_info
sys.int_info(bits_per_digit=30, sizeof_digit=4,
default_max_str_digits=4300, str_digits_check_threshold=640)
In my case Python cannot represent numbers with more than 4300 digits.
>>> pow(10, 4299)
1000....0 # Many digits omitted for brevity!
>>> pow(10, 4300)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Exceeds the limit (4300 digits) for integer string conversion;
use sys.set_int_max_str_digits() to increase the limit
Curiosity: CPython's implementation details
Let's take a look at how CPython efficiently handles small integers in its 'cache'. To start, notice the following numbers defined in pycore_global_objects.h:
#define _PY_NSMALLPOSINTS 257
#define _PY_NSMALLNEGINTS 5
Those numbers are used in longobject.c to determine if a number is considered a "small integer" (within -5 to 256):
#define IS_SMALL_INT(ival)
(-_PY_NSMALLNEGINTS <= (ival) && (ival) < _PY_NSMALLPOSINTS)
Finally, the get_small_int()
function returns a pre-defined number from a static list (as pointer to PyObject
):
static PyObject *
get_small_int(sdigit ival)
{
assert(IS_SMALL_INT(ival));
return (PyObject *)&_PyLong_SMALL_INTS[_PY_NSMALLNEGINTS + ival];
}
Basically, this function works as a cache, where small integers are already "precomputed" for fast access. It is used in several places in the C code, particularly when building new numbers, such as in PyLong_FromLong():
PyObject *
PyLong_FromLong(long ival)
{
...
/* Handle small and medium cases. */
if (IS_SMALL_INT(ival)) {
return get_small_int((sdigit)ival);
}
if (-(long)PyLong_MASK <= ival && ival <= (long)PyLong_MASK) {
return _PyLong_FromMedium((sdigit)ival);
}
...
}
The basic idea is that if a number is "small", it retrieves a pre-defined PyObject from the _PyLong_SMALL_INTS array. Otherwise, it will build it using the _PyLong_FromMedium() function or will do something else. Either way it will do some computations and memory allocations, which takes some time.
Floating-point
The second numeric data type worth talking about in Python are floating-point numbers. Floating-point numbers are numbers that have a decimal point or are expressed in scientific notation. They are represented using the float type.
>>> x = 3.14
>>> y = 1.5e6 # 1500000.0
>>> type(x)
<class 'float'>
>>> type(y)
<class 'float'>
Floating-point numbers can be converted to integer, and integers converted to floating-point. The conversion from floating-point to integer effectively truncates numbers.
>>> float(1)
1.0
>>> int(1.7)
1
You can also round a number to the nearest integer, or to a specific number of decimal digits:
>>> pi = 3.141592653589793
>>> round(pi)
3
>>> round(pi, 4)
3.1415
Unlike integers, due to the way floating-point numbers are stored in binary, there may be precision issues:
>>> 0.1 + 0.2
0.30000000000000004
Comparing floating-point numbers for equality can be tricky, therefore it's recommended to use tolerances to account for small variations:
>>> x = 0.1 + 0.2
>>> y = 0.3
>>> x == y # x is 0.30000000000000004, y is 0.3
False
>>> abs(x - y) < 0.00001 # are they within a tolerance of 1e-5?
True
You can also compare floating-point numbers for equality using the isclose
function in the math
module.
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
There are special values that may be useful for handling extreme cases or errors in numerical computations:
>>> infinity = float('inf')
>>> not_a_number = float('nan')
>>> infinity > 1e100
True
Tip: the decimal module
In cases where precision is critical, you can use the decimal module, which provides more control over precision.
>>> from decimal import Decimal
>>> a = Decimal('0.1')
>>> b = Decimal('0.2')
>>> c = Decimal('0.3')
>>> a + b == c
True
Another tip: sys.float_info
Similarly to integers, there are limits on how your computer can handle float
numbers.
The float_info
tuple in the sys
module
provides us with the limits of the platform.
>>> import sys
>>> sys.float_info
sys.float_info(max=1.7976931348623157e+308, max_exp=1024,
max_10_exp=308, min=2.2250738585072014e-308, min_exp=-1021,
min_10_exp=-307, dig=15, mant_dig=53, epsilon=2.220446049250313e-16,
radix=2, rounds=1)
>>> 1.7976931348623157e+308 * 1
1.7976931348623157e+308
>>> 1.7976931348623157e+308 * 2
inf
In my own case, the maximum float number it can represent is around 1.7976931348623157e+308.
Curiosity: CPython's implementation details
There are many interesting things to explore on floatobject.c, which is the C source code file in CPython that deals with floating-point numbers. There are lots of good examples there, but this is how CPython handles the sum of two floats:
static PyObject *
float_add(PyObject *v, PyObject *w)
{
double a, b;
CONVERT_TO_DOUBLE(v, a);
CONVERT_TO_DOUBLE(w, b);
a = a + b;
return PyFloat_FromDouble(a);
}
Basically it extracts the double
values of both PyObjects and returns a new PyObject.
The float_add
function also shows the reason for the immutability of Python's floats, as the sum of two floats
always returns a new PyObject
.
>>> x = 3.14
>>> id(x)
4346874480
>>> x += 1.0
>>> id(x)
4346873936
Complex Numbers
Complex numbers are mathematical entities that extend the real numbers to include a new quantity, denoted as "i" or "j", which represents the square root of -1. They have profound applications in diverse domains such as electrical engineering (in AC circuit analysis), signal processing, quantum mechanics, control theory, and more.
In Python, a complex number is represented as a + bj
where a
is the real part and b
the imaginary part.
For example, 2 + 3j
represents a complex number with a real part of 2 and an imaginary part of 3.
We can also use the complex(real, imag)
function to create complex numbers:
>>> z1 = 2 + 3j
>>> z2 = complex(4, -2)
>>> z1
(2+3j)
>>> z2
(4-2j)
>>> type(z1)
<class 'complex'>
Similarly to other numeric types, complex numbers support the standard arithmetic operations:
>>> z1 = 2 + 3j
>>> z2 = 4 - 2j
>>> z1 + z2
(6+1j)
>>> z1 - z2
(-2+5j)
>>> z1 * z2
(14+8j)
>>> z1 / z2
(0.1+0.8j)
If needed, we can extract the real and imaginary parts:
>>> z = 2 + 3j
>>> z.real
2.0
>>> z.imag
3.0
The conjugate of a complex number a + bj
is a - bj
.
It can be obtained using the conjugate()
method:
>>> z = 2 + 3j
>>> z.conjugate()
(2-3j)
We can also obtain the absolute value (or magnitude) of a complex number.
The absolute value of a complex number a + bj
is sqrt(a^2 + b^2)
and can be obtained with the abs
function:
>>> z = 3 + 4j
>>> abs(z)
5.0
Finally, we can also obtain the phase angle of a complex number in radians.
It can be calculated using the phase()
function in the cmath module.
>>> import cmath
>>> z = 2 + 3j
>>> cmath.phase(z)
0.982793723247329
Tip: the cmath
module
Besides the phase()
function, the cmath module provides access to
other mathematical functions for complex numbers.
>>> import cmath
>>> z = 2 + 3j
>>> cmath.polar(z)
(3.605551275463989, 0.982793723247329)
>>> cmath.exp(z)
(-7.315110094901103+1.0427436562359045j)
>>> cmath.log10(z)
(0.5569716761534184+0.42682189085546657j)
...
Curiosity: CPython's implementation details
Operations on complex objects are implemented on complexobject.c. There are lots of good examples there, but this is how CPython handles the sum of two complex numbers:
Py_complex
_Py_c_sum(Py_complex a, Py_complex b)
{
Py_complex r;
r.real = a.real + b.real;
r.imag = a.imag + b.imag;
return r;
}
Quite easy to understand, hopefully!
Interesting numeric modules
In this section we will explore briefly some numeric and mathematical modules in Python. You can find these and others in the Python documentation.
The math
module
The math
module is one of the most used mathematical modules in Python.
It provides access to mathematical functions defined by the C standard.
This is a very extensive module, so I'll just show a couple of simple things you can do with it.
You can round numbers up or down:
>>> math.pi
3.141592653589793
>>> math.ceil(math.pi)
4
>>> math.floor(math.pi)
3
Compute factorials and the greatest common divisor:
>>> math.factorial(4)
24
>>> math.gcd(10,6)
2
Do permutations and combinations:
>>> math.comb(20,6)
38760
>>> math.perm(20,6)
27907200
Compute power, logarithms and use trigonometric functions:
>>> math.pow(10,3)
1000.0
>>> math.sqrt(4)
2.0
>>> math.log10(5)
0.6989700043360189
>>> math.sin(1)
0.8414709848078965
And do angular conversions:
>>> angle = 90
>>> math.radians(angle)
1.5707963267948966
>>> math.degrees(1.5707963267948966)
90.0
The decimal
module
The decimal
module provides a way to perform precise arithmetic with decimal numbers.
This is particularly important in financial and scientific applications where accuracy is crucial.
The decimal
module is extensively documented, and has many examples, so I'll just show some simple things that can be done with it.
The most import class in this module is the Decimal
:
>>> from decimal import Decimal
>>> d1 = Decimal('0.1')
>>> d2 = Decimal('0.2')
>>> type(d1)
<class 'decimal.Decimal'>
As shown before, it has better precision than float
:
>>> 0.1 + 0.2 == 0.3 # Float
False
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
True
We can control many aspects of a Decimal
number, like, for instance, the precision:
>>> from decimal import getcontext
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1,
clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
>>> Decimal(2).sqrt()
Decimal('1.414213562373095048801688724')
>>> getcontext().prec = 2
>>> Decimal(2).sqrt()
Decimal('1.4')
We can use arithmetic functions on decimals:
>>> Decimal('0.1') + Decimal('0.2')
Decimal('0.3')
>>> Decimal('0.1') - Decimal('0.2')
Decimal('-0.1')
>>> Decimal('0.1') * Decimal('0.2')
Decimal('0.02')
>>> Decimal('0.1') / Decimal('0.2') # Regular division
Decimal('0.5')
>>> Decimal('10') // Decimal('3') # Integer division
Decimal('3')
And similarly to regular numbers, we can also use other mathematical functions:
>>> d = Decimal(math.pi)
>>> d.log10()
Decimal('0.4971498726941338374217231246')
>>> d.sqrt()
Decimal('1.772453850905515992751519103')
The fractions
module
The fractions
module provides support for rational number
arithmetic, and can be built by passing a numerator and denominator:
>>> from fractions import Fraction
>>> f = Fraction(1, 3)
>>> f.numerator
1
>>> f.denominator
3
There are other ways of building fractions, although using float
and Decimal
may lose precision:
>>> Fraction('1/3')
Fraction(1, 3)
>>> Fraction(1/3)
Fraction(6004799503160661, 18014398509481984)
>>> Fraction(Decimal('1') / Decimal('3'))
Fraction(3333333333333333333333333333, 10000000000000000000000000000)
Similar to other numbers, you can do basic arithmetic with fractions:
>>> f1 = Fraction(1, 3)
>>> f2 = Fraction(1, 6)
>>> f1 + f2
Fraction(1, 2)
>>> f1 - f2
Fraction(1, 6)
>>> f1 * f2
Fraction(1, 18)
>>> f1 / f2
Fraction(2, 1)
>>> f1 // f2 # Integer division
2
And compare fractions if needed:
>>> f1 = Fraction(1, 3)
>>> f2 = Fraction(1, 6)
>>> f1 > f2
True
And we finish this article here. Hope you learned something from it, keep exploring and have fun with Python!