Numeric types in Python
By John Lekberg on August 29, 2020.
In this week's post, you will learn about Python's 6 different numeric types, and the numeric tower exposed by the numbers module.
Python's numeric types
Python has 4 built-in numeric types:
- bool represents the integers 1 and 0. (Why is
bool
numeric? See PEP 285 for details.) - int represents the integers ℤ using arbitrary-precision integer arithmetic.
- float represents the real numbers ℝ using machine-level double precision floating point numbers.
- complex represents the complex numbers ℂ using a pair of
float
objects.
The standard library adds 2 more numeric types:
- fractions.Fraction
represents the rational numbers
ℚ using a pair of
int
objects. - decimal.Decimal represents the real numbers ℝ using arbitrary precision decimal floating point numbers.
It's useful to have these different numeric types for different use cases. E.g.
rounding error occurs when adding float
objects, but not when adding
Fraction
objects:
.1 + .2 == .3
False
.1 + .2
0.30000000000000004
from fractions import Fraction as F F("0.1") + F("0.2") == F("0.3")
True
F("0.1") + F("0.2")
Fraction(3, 10)
But what happens when different numeric types interact?
-
Sometimes it succeeds:
F("0.1") + .1
0.2
-
Sometimes it fails:
from decimal import Decimal as D D("0.1") + .1
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'
So, what are the rules that control how different numeric types interact? In Python, these rules are known as the "numeric tower".
Python's numeric tower
Python's numeric tower is exposed by the numbers module. (See PEP 3141 for details.)
numbers
exposes 5 abstract base classes:
These classes form a hierarchy:
from numbers import Number, Complex, Real, Rational, Integral issubclass(Complex, Number)
True
issubclass(Real, Complex)
True
issubclass(Rational, Real)
True
issubclass(Integral, Rational)
True
If you want to know how the numeric types fit into this hierarchy, it's most useful to pair each numeric type with the most specific class in the numeric tower:
Numeric tower | Numeric types |
---|---|
Number | Decimal *
|
Complex | complex
|
Real | float
|
Rational | Fraction
|
Integral | int , bool
|
* Although
Decimal
is "in" the numeric tower (it subclassesNumber
), it doesn't follow the implicit conversion rules used by the other numeric types.Decimal
interacts withint
:from decimal import Decimal as D D(3) + 4
Decimal('7')
But,
Decimal
does not interact withcomplex
,float
, orFraction
:D(3) + complex(4)
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'complex'
D(3) + float(4)
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'
from fractions import Fraction as F D(3) + F(4)
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'Fraction'
As a result, I don't consider
Decimal
to be a part of the numeric tower.
Making these pairings will allow us to better understand what happens when different numeric types interact.
What happens when different numeric types interact?
When two different numeric types interact, the type of the result is the more general type. E.g.
-
int
(Integral
) plusfloat
(Real
) isfloat
.type(int() + float())
float
-
Fraction
(Rational
) pluscomplex
(Complex
) iscomplex
:type(Fraction() + complex())
complex
However, this can cause problems with float
and complex
objects, because of
the limitations of their representation of the real ℝ (and complex ℂ) numbers:
-
int
can represent any integer ℤ as long as there is enough memory. E.g. 21,000,0002 ** 1_000_000
99006562292958982506979236163019 03250733624241787567332866396114 53170948330948610305461455123464 83914824315070345837238835106589 [...] 74619678367983172072707268320759 17571004282091924202288726677490 52301871236104888403162747109376
(NOTE: digits omitted for clarity.)
But
float
andcomplex
can't represent numbers this big, and interactions raise an exception:float(2 ** 1_000_000)
OverflowError: int too large to convert to float
complex(2 ** 1_000_000)
OverflowError: int too large to convert to float
(2 ** 1_000_000) + 1.0
OverflowError: int too large to convert to float
-
Fraction
objects can represent any rational number ℚ as long as there is enough memory. E.g. 2-1,000,000Fraction(1, 2 ** 1_000_000)
Fraction(1, 99006562292958982506979236163019 03250733624241787567332866396114 53170948330948610305461455123464 83914824315070345837238835106589 [...] 74619678367983172072707268320759 17571004282091924202288726677490 52301871236104888403162747109376 )
(NOTE: digits omitted for clarity.)
But
float
andcomplex
can't represent numbers this small, and interactions produce 0:float(Fraction(1, 2 ** 1_000_000))
0.0
complex(Fraction(1, 2 ** 1_000_000))
0j
Fraction(1, 2 ** 1_000_000) + 1.0
1.0
In conclusion...
In this week's post, you learned about Python's 6 different numeric types, and
the numeric tower exposed by the numbers
module:
int
andFraction
objects can represent any integer ℤ or rational ℚ (as long as it fits into memory).float
andcomplex
use machine-level double precision numbers, likely the IEEE Standard for Floating-Point Arithmetic (IEEE 754).bool
is a numeric type for historical reasons (see PEP 285 for details).Decimal
uses decimal floating point numbers, following the "General Decimal Arithmetic" specification.- The numeric tower has abstract base classes
Number
,Complex
(ℂ),Real
(ℝ),Rational
(ℚ),Integral
(ℤ). bool
,int
,Fraction
,float
, andcomplex
are in the numeric tower and can interact with each other.Decimal
is technically "in" the numeric tower, but doesn't interact well with the other numeric types.
My challenge to you:
NumPy a popular 3rd-party library introduces more numeric types:
Read about the different numeric types and compare them to Python's standard 6 types.
Ask yourself questions, like:
If you enjoyed this week's post, share it with your friends and stay tuned for next week's post. See you then!
(If you spot any errors or typos on this post, contact me via my contact page.)