# Numeric Data Types

There are six fundamental numeric datatypes used in Python. The int class (whole number), bool class (True or False), the float class (number with a decimal point) and the complex class (number with imaginary component $j=\sqrt{-1}$) are all in builtins. The float class is displayed as decimal but under the hood is encoded in binary. This results in recurring rounding problems making floating point arithmetric slightly different from traditional arithmetric. The Decimal class is contained in the decimal standard module is higher precision and behaves like traditional numbers which use the decimal system. The Fraction class is contained in the fractions standard module.

## int class

An integer is a whole number.

### Initialization Signature

The init signature of the int class can be viewed by inputting the class name with open parenthesis and pressing shift ⇧ and tab ↹:

The init signature is normally used when cast an integer from a string such as:

int('65')

The default base is 10. For a binary or hexadecimal string this needs to be specified:

int('0b1000001', base=2)
int('0x41', base=16)

Without quotation marks, these are all recognised as an integer:

65
0b1000001
0x41

The default way to instantiate an integer is to use a decimal as it uses a base 10 by default. The number can be assigned to an object name:

num1 = 65

Recall assigning to an object name can be conceptualised as adding a label to the integer object. This label can be used to reference the integer object. The Variable Inspector may be opened by right clicking blank space and selecting Open Variable Inspector:

The datatype is int as expected:

### Identifiers

Inputting num1. and pressing tab ↹ will display a list of identifiers:

The integer 65 is designed for interoperability with the Fraction class and can be conceptualised as the fraction instance:

$$\left(\frac{65}{1}\right)$$

The numerator will match the value of the integer in this case 65 and the denominator will be 0. The associated method as_integer_ratio returns these two attributes and returns the fraction as a tuple:

num1.numerator
num1.denominator
num1.as_integer_ratio()

The integer 65 is designed for interoperability with the complex class and can be conceptualised as the complex number:

$$65+0j$$

where $j = \sqrt{-1}$.

The real attribute reads the real value will match the value of the integer in this case 65 and the imag will be 0. The associated method conjugate takes these two attributes and inverts the sign of the imag attribute, because this is 0 the complex conjugate matches the original integer:

num1.real
num1.imag
num1.complex()

The remaining identifiers are for interoperability with the bytes class. Details about bytes and encoding were given in the previous tutorial. The binary representation of the integer which can be viewed using:

bin(num1)

The method bit_count returns the number of ones which in this case is 2 and the method bit_length returns the bit length (the number of digits past the 0b prefix):

num1.bit_count()
num1.bit_length()

The to_bytes method can cast the integer to a bytes instance. By default a length of 1 byte is used with a byteorder that is big (big endian) and signed is False meaning the bytes are unsigned:

This behaviour will therefore only work for a positive integer between 0:256 as these are the limits for a 8 bit signed integer. The defaults will behave similarly to the chr function which returns a Unicode value:

num1.to_bytes()
chr(num1)

If a byte length of 3 bytes is selected:

num1.to_bytes(length=3)

The escape sequence for \x00 will display as this is a non-printable character. The letter 'A' has an escape sequence of \x41 but displays as 'A' as it is readible. This can be seen by using the bytes method hex:

num1.to_bytes(length=3).hex()

A Unicode character can be examined that occupies two bytes:

num2 = 949
chr(num2)
letter2 = num2.to_bytes(length=2, byteorder='big')
letter2.decode(encoding='UTF-16-BE')

The from_bytes class method is an alternative constructor which can be used to instantiate an integer from a bytes object. For example:

int.from_bytes(letter2)

### Data Model Identifiers

The data model identifiers can be viewed by inputting an instance name followed by a dot ., two underscores __ and a tab ↹:

In the previous tutorial, the data model identifiers were discussed in detail for the str class. Some analogy can be seen between these two classes as they are both subclasses of object.

The data model dir controls the behaviour of builtins function dir and looks at the directory of the object returning a list of strings corresponding to the names of identifiers:

import pprint
pprint.pprint(dir(num1), compact=True)

The repr and str data model identifiers give the formal string representation and the informal string representation specifying the behaviour of the repr function and the str class respectively repr and str are in builtins:

repr(num1)
str(num1)

For the int class, the formal and informal representations are identical. Recall conventionally that print uses the informal str representation:

print(num1)

And the cell output prints the formal representation:

print(repr(num1))
num1

The data model identifier format_ is typically used when an integer variable is placed in a formatted string. This was examined in detail when the string class was examined however to recap:

f'The number is {num1 :d}'
f'The number is {num1 :03d}'
f'The number is {num1 :+04d}'

The data model identifier class is a method wrapper to the builtins class type which displays the class type of the object.

type(num1)

This displays int as expected.

The data model identifier doc is the document string for a string instance. It is more commonly used with the ? which includes some other information from the data model identifiers type, str, and doc:

? num1

The data model identifier index means that an int can be used for indexing:

'hello'
b'hello'

The data model identifier hash means that an int is hashable. Recall that a hashable value is permissible as a key in a dictionary or mapping.

num_dict = {1: 'one', 2: 'two', 3: 'three'}
num_dict

Keys in the dictionary are normally strings but can be integers aswell. Note this dictionary has the numeric keys and these differ from the numeric index in other collections like a list. The numeric keys above for example lack the key 0.

getitem is an collection data model identifier. It is used when:

num1

This is setup to raise a TypeError as an int is not subscriptable.

The data model identifier sizeof displays the memory an object occupies in bytes:

import sys
sys.getsizeof(num1)

The getattribute, setattr and delattr data model identifiers are used to get, set and delete attributes. getattribute is used when:

num1.real

setattr is used when:

num1.real = 66

Notice that this is not supported and the method is setup to invoke an AttributeError.

delattr is used when:

del num1.real

This is also not supported and the method is setup to invoke an AttributeError.

The init data model method is called when instantiating an integer.

When the new Python object is created, the new data model method is called. This creates the new instance which is given the label or object name and then the initialization signature init is called to initialize the instance with the unique numeric data.

### Unitary Data Model Identifiers

The unitary data model identifiers allow use of a mathematical operator on a unitary instance.

pos is a function wrapper for the + operator:

num1
+ num1

This doesn't change the sign of the integer.

neg is a function wrapper for the – operator:

num1
- num1

This changes the sign of the integer.

If numeric instances are not assigned to instance names, the dot . syntax to access an identifier cannot be used as the dot . is confused with the decimal point . used for the floating point number float. This is why the vast majority of identifiers for numeric values are data model identifiers that are function wrappers for numeric operators and why the use of numeric operators is preferential:

abs is a function wrapper for the builtins function abs:

abs(num1)
abs(- num1)

Notice that both of these are positive, the signs have been stripped.

ceil, floor and trunc are function wrappers for the functions math.ceil, math.floor and math.trunc. These functions are designed to cast a non-integer number into an integer. When the number is already an integer, the result is unchanged:

num1
import math
math.ceil(num1)
math.floor(num1)
math.trunc(num1)

round is a function wrapper for the function round which by default rounds to an integer. When the number is already an integer, the result is unchanged:

round(num1)

int is a function wrapper for the int class and defines how to cast a number to an int. When the number is already an integer, the result is unchanged:

num1
int(num1)

bool is a function wrapper for the bool class and defines how to cast a number to a bool. Any int that is non-zero will map to a boolean value of True, zero will map to False:

num1
bool(num1)
num2 = 0
bool(num2)
num3 = -1
bool(num3)

float is a function wrapper for the float class and defines how to cast the number to a float. Notice the subtle difference in the output, a decimal point is now included:

num1
float(num1)

### Binary Data Model Identifiers

Binary data model methods require two numeric instances:

num1 = 65
num2 = 4

If the docstring of the add binary data model method is examined, the numeric instance the data model method is being called from is referred to as self meaning this instance and the other instance is referred to as value:

The data model identifier add is a function wrapper for the + operator performing numeric addition:

num1 = 65
num2 = 4
num1 + num2

num2 + num1

The operation above is commutative and both instances are of the same int class so the result is the same. When the operator is used between different class types, there can be subtle differences.

The add data model identifier is also a function wrapper for the inplace addition += operator:

num1 = 65
num2 = 4
num1 += num2

This is shorthand for:

num1 = 65
num2 = 4
num1 = num1 + num2

Recall an int instance is immutable and therefore was hashable. Inplace addition i.e. addition and then reassignment should not be confused with mutability.

Inplace addition carries out the addition operation on the right first using the original instance of num1 creating a new numeric instance. The instance name num1 which recall can be conceptualised as a label originally pointed to the old instance. After the inplace addition it is now pointing to the new instance. i.e. is reassigned to the new instance. If the old instance has no other instance names or labels it is removed by Pythons garbage collecion.

mul is a function wrapper for the * operator:

num1 = 65
num2 = 4
num1 * num2

The * operator in the int class performs numeric multiplication as seen above. In the str class the * operator is defined to allow string replication with an int.

str1 = 'hello'

If the following is used, the mul method from the str class is used 'hello' is the string instance self and num1 is the int instance value. String replication occurs:

num1 = 3
str1 = 'hello'
str1 * num1

When the following is carried out, the mul method from the int class is used num1 is the int instance self and 'hello' is the string instance value. As mul is setup in an integer for multiplication, the first operation fails:

num1 = 3
str1 = 'hello'
num1 * str1

Behind the scenes the reverse multiplication rmul is attempted. The rmul sees that 'hello' is a string and then calls the mul method of the string class, effectivelty computing:

'hello' * num1

mul is also a function wrapper for the assignment multiplication operator *=.

sub is a function wrapper for the subtraction operator – which carries out numeric subtraction:

num1 = 65
num2 = 4
num1 - num2

sub is also a function wrapper for the assignment subtraction operator -=. There is also the associated reverse subtraction rsub.

pow is a function wrapper for the power operator ** which raises self to the power of the value:

num1 = 65
num2 = 4
num1 ** num2

Recall this is equivalent to:

65 * 65 * 65 * 65

pow is also a function wrapper for the assignment power operator **=. There is also the associated reverse subtraction rpow.

floordiv is a function wrapper for the floor division // operator which performs floor division also known as integer division. The associated modulo is a function wrapper for the modulo operator % which calculates the modulo. divmod is a fucntion wrapper to the function divmod which returns the floor division and modulo as components of a tuple. These are usually used with positive itneger values:

num1 = 65
num2 = 4
num1 // num2
num1 % num2
divmod(num1, num2)

floordiv and mod also wrap to the assignment floor division operator //= and the assignment mudulo operator %= respectively. There is also the associated reverse versions rfloordiv, rmodulo and rdivmod.

truediv is a function wrapper to the float division / operator which performs float division. The result will always be a float. Notice the inclusion of the decimal point:

num1 = 65
num2 = 4
num1 / num2
num1 / 1

truediv also wraps to the assignment floor division operator /=. There is also the associated reverse float divide rtruediv.

The six comparison data model identifiers equals eq, not equals __ne, less than or equal to le, less than lt, greater than or equal to ge and greater than gt are function wrappers to the 6 comparison operators ==, !=, <=, <, >= and > respectively.

num1 = 65
num2 = 4
num1 > num2
num1 > num1
num1 == num1
num1 >= num1
num1 != num1

and, or and xor are function wrappers to the and operator &, or operator |, xor operator ^ respectively. These are normally associated with boolean values recall any integer value that is non-zero is True and an integer equal to zero is False.

& is True only if both conditions are True:

True & True
True & False
False & False

| is True if one or both conditions are True:

True | True
True | False
False | False

^ is True if both conditions are different:

True ^ True
True ^ False
False ^ False

and, or and xor also wrap to the assignment and operator =&, assignment or operator =| and assignment xor operator ^= respectively. There is also the associated reverse versions rand, ror, rxor.

lshift is a function wrapper to the binary left shift operator << and rshift is a function wrapper to the binary right shift operator >>. These operate at the byte level. The bin function can be used to examine the change.

Notice that trailing zeros of the specified number have been added to the end shifting the existing byte sequence to the left:

num1 = 65
bin(num1)
bin(num1 << 1)
num1 << 1
bin(num1)
bin(num1 << 2)

Notice the specified number of digits on the right have been stripped:

num1 = 65
bin(num1)
bin(num1 >> 1)
num1 >> 1
bin(num1)
bin(num1 >> 2)

lshift and rshift also wrap to the assignment binary left shift operator <<= and assignment binary right shift operator >>= respectively. There is the associated reverse versions rlshift and rrshift.

### PEDMAS

PEDMAS is an abbreviation for order of operator precedence:

D and M are of equal precedence.

A and S are of equal precedence.

Compare the expression:

-3 ** 2

With:

(-3) ** 2

The first expression has E and S. E occurs before S so the 3 is brought to the power of 2 and then the negative operator is applied.

The second expression has P, E and S. P occurs first and within the parenthesis, the operation S is carried out. Once the operation in parenthesis is carried out, the final operation E is carried out.

## bool class

A boolean is a True or False value.

When the method resolution order for the int class is examined:

int.mro()

The output list displays int and then object. This means the int (like everything else in Python is an object). The method resolution order means Python will first look for a method defined in the int class (blueprint) and then if it can't find the method there, take a second look in the object class (blueprint).

When the method resolution order for the bool class is examined:

bool.mro()

The output list displays bool, int and then object. The method resolution order means Python will first look for a method defined in the bool class (blueprint), then secondly look for a method in the int class (blueprint) and finally if it can't find the method there, take a third look in the object class (blueprint). The only major modification to the bool class is a restriction to only two possible values False and True. Otherwise it behaves identically to the int class as most its methods are taken directly from the int class unmodified (i.e. accessed directly from the int blueprint).

Recall that each class has the data model identifier dict which gives a dictionary of identifiers. Notice only a handful of data model identifiers are shown in the bool class:

bool.__dict__

This is because most of the data model identifiers and other identifiers are inherited directly from the int base class:

int.__dict__

Some other identifiers are object superclass:

### Initialization Signature

The init signature of the bool class can be viewed by inputting the class name with open parenthesis and pressing shift ⇧ and tab ↹:

The init signature states that the builtins False and True are the only two instances of the class bool and clarifies that the class bool is a subclass of the class int, and cannot be subclassed.

Recall casting an integer to a bool gives True for any non-zero integer and False for zero:

bool(0)
bool(1)
bool(-4)

Typically the inbuilts False and True are used directly but they can be assigned to object names:

False
True

### Identifiers

A list of identifiers can be found by inputting one of these object names followed by a dot . and tab ↹:

These methods are the same as their counterparts in the int class because this is a subclass and the methods are taken directly from the int classes blueprint.

### Data Model Identifiers

The data model identifiers can be viewed using False.__ followed by a tab ↹"

Most of these behave identically to the int class as they were taken directly from the int base class. The formal string data model identifier repr has been updated to display strings of the two builtin identifiers 'False' and 'True' opposed to '0' and '1'. When only repr the formal string identifier is updated, the informal string identifier str is automatically updated to use the same representation:

repr(False)
str(False)

Notice the difference in syntax highlighting from the builtin bool instance False and the string 'False'. The inbuilt instance is case sensitive and if false is referenced, Python will look for an object false which won't have been created resulting in a NameError.

If the comparison operator is equal == is used:

False == 0
True == 1
True == 2

Numerically it can be seen that False is the same as 0 and True is the same as 1.

When boolean values are used with most of the mathematical operators, they take on these numeric values. This can be seen by use of the basic positive and negative unitary operators. Recall for a unitary + and unitary -, the pos and neg data model identifiers are invoked and a change can be seen in the result when + is used:

+ True
- True

For most mathematical operations, it is more common to use the integers directly. However it is common to use boolean values with the and operator &, or operator |, xor operators ^ as these operators return a boolean value. This use case was explored when these methods were examined earlier.

## float class

A float is number that has an incomplete unit. This incomplete unit is normally expressed using a decimal point.

When the method resolution order for the float class is examined:

float.mro()

This class inherits directly from object and is not a subclass of the int class.

### Initization Signature

The init signature of the float class can be viewed by inputting the class name with open parenthesis and pressing shift ⇧ and tab ↹:

The initialization signature is only typically used when casting an existing number or a string of a number to a floating point number. For example:

float(10)
float('10')

Notice the difference in the syntax highlighting for the input arguments which distinguish the numerical input argument from the text input argument. Notice the output in all cases includes the postfix .0. The . in this case indicates a decimal point. This should not be confused with the other use of the . which is used to access identifiers from an object.

Every day items are not measured in quantised units and the decimal point means it is possible to include an incomplete quantity. For example a human may have a height of 1.5 metres:

1.5

Floating point numbers are particularly common when the item size is extremely small or extremely large with respect to the unit of measurement. For example, the radius of a hydrogen atom is 0.000000000053 metres:

0.000000000053

Because this number is so small, it becomes difficult to transcribe and the output uses scientific notation.

The 0th position is the unit value which is to the left hand side of the decimal point:

5e0

The 1st position is the tens, which is one to the left hand side of the unit value:

5e1

The negative 1st position is the 10ths which is one to the right hand side of the unit. The decimal point itself is not counted as a numeric digit when using scientific notation:

5e-1

In the example above the first non-zero digit for the radius of the hydrogen atom was 5 which was at the 11th digit to the right hand side to the decimal point. This is why the power was -11.

In scientific notation the mantissa is always expressed with the unit value occupied and the power uses is always an integer.

Scientific notation is typically used for very small and very large numbers to prevent transcription errors from leading or trailing zeros respectively. The radius of the sun is expressed as 696340 km where k means to the power of 3. This means the radius of the sun in metre is:

696340e3

Or with a proper mantissa as:

6.96340e8

Python will display scientific notation for numbers with an exponent less than -5 and greater than 16. This behaviour can be seen using:

for i in range(-12, 25, 2):
print(float(5**i))

### Identifiers

The . is used as a decimal point for numeric data and therefore it is not possible to access identifiers from a number unless the number has an object name and the . is placed after the object name.

For example if the instance is assigned to an object name num1:

num1 = 0.5

Then pressing num1 followed by a dot . and tab ↹ displays the identifiers:

Although the float class is not a subclass of the int class, its identifiers are setup to be consistent with the int class.

The real, imag and conjugate methods are present in the int class and the float class as both classes are setup to be compatible with complex numbers. The real component is once again going to be the same as the original value and the imag component is going to be zero. The conjugate which returns the real component and switches the sign of the zero imag component is going to be identical to the existing instance:

The float class is also setup to be compatible with the Fractions class, it does not have the attributes numerator or denominator as the values of these have to be calculated, opposed to being merely read off like in the case of the integer class. It does however have the method as_integer_ratio which calculates these and displays them in a tuple:

num1 = 0.5
num1.as_integer_ratio()

Sometimes the results may be unexpected…

num2 = 0.1
num2.as_integer_ratio()

A float is displayed using the 10 decimal characters but under the hood is stored using a finite number of binary bits. The binary system only uses 2 characters and recurring rounding errors are quite prevalent.

Recurring rounding errors occur in decimal also. In decimal a recurring rounding error occurs with the concept of one third which is easy to represent as a fraction:

$$\left(\frac{1}{3}\right)$$

But cannot be represented cleanly as a decimal, essentially the same recurring operation occurs on and on and on … forever.

$$0.3333333333\cdots$$

In real life the number will be written down using a finite number of characters:

$$0.3333333333$$

The concept of one third plus one third plus one third equalling unity is simple as a fraction:

$$\left(\frac{1}{3}\right) + \left(\frac{1}{3}\right) + \left(\frac{1}{3}\right)$$

However in decimal there will be a recurring rounding error for each third:

$$0.3333333333 + 0.3333333333 + 0.3333333333$$

As a result a very small proportion will be lost and the result will be just shy of unity:

$$0.9999999999$$

The hex method will convert a float instance to a hexadecimal string, more details can be seen by examining the docstring by inputting the method name with open parenthesis and pressing shift ⇧ and tab ↹:

The format is best seen by an example:

num1 = 0.5
num1.hex()

This gives '0x1.0000000000000p-1'"

The general form is:

'[sign] [0x] integer [. fraction] [p exponent]'

[sign] the sign is not shown so it is implied to be positive.

[0x] is a constant prefix denoting a hexadecimal number.

integer [. fraction] are in hexadecimal. To convert to decimal powers of 16 need to be used.

The unit 1 and $1*16^{0}=1$

The first value past the decimal point is 0 and $0*16^{-1}=0$

The second value past the decimal point is 0 and $0*16^{-2}=0$

The third value past the decimal point is 0 and $0*16^{-3}=0$

The fourth value past the decimal point is 0 and $0*16^{-4}=0$

$\vdots$

Combining these together gives $1.00000$ in decimal.

[p exponent] indicates 2 to the power of a decimal exponent. In this example p-1 means:

$2^{-1}=0.5$

Combining the above:

$+1*0.5$

which is the original value:

$0.5$

Let's look at a more complicated example:

num2 = 0.12
num2.hex()

This gives '0x1.eb851eb851eb8p-4'"

The general form is:

'[sign] [0x] integer [. fraction] [p exponent]'

[sign] the sign is not shown so it is implied to be positive.

[0x] is a constant prefix denoting a hexadecimal number.

integer [. fraction] are in hexadecimal. To convert to decimal powers of 16 need to be used.

The unit 1 and $1*16^{0}=1$

The first value past the decimal point is e which is $14*16^{-1}=0.875$

The second value past the decimal point is b and $11*16^{-2}=0.04296875$

The third value past the decimal point is 8 and $8*16^{-3}=0.001953125$

The fourth value past the decimal point is 5 and $5*16^{-4}=0.0000762939453125$

$\vdots$

Combining the 5 values above gives $1.9199981689453125$ in decimal.

[p exponent] indicates 2 to the power of a decimal exponent. In this example p-4 means:

$2^{-4}=0.0625$

Combining the above:

$+1.9199981689453125*0.0625$

this is close to the original value:

$0.11999988555908203$

((1 * 16 ** 0)
+ (14 * 16 ** -1)
+ (11 * 16 ** -2)
+ (8 * 16 ** -3)
+ (5 * 16 ** -4)) * (2 ** -4)

In the calculation above only the 4 most significant components of the fraction were used. A closer approximation will be made if all 14 are used. If the result '0x1.eb851eb851eb8p-4' is examined, notice that eb851 is recurring which means there will be at least some rounding error when using a finite number of digits.

The alternative constructor fromhex is a class method that is used to create a new float instance from a hexadecimal string. It can be used with the strings above:

float.fromhex('0x1.0000000000000p-1')
float.fromhex('0x1.eb851eb851eb8p-4')

### Data Model Identifiers

To view the data model identifiers, a float instance name can be input followed by a dot ., two underscores __ and a tab ↹:

Most of the numeric identifiers are available and the float class and the int class are setup to be consistent with one another.

The string identfiers repr and str are setup for the formal and informal string repreentations, which in the case of the float class match. The decimal point is always included in the representation. Scientific notation will display for numbers with an exponent less than -5 and greater than 16.

num1 = 1.5
num2 = 0.000000000053
num3 = 6.96340e8
repr(num1)
str(num1)
repr(num2)
str(num2)
repr(num3)
str(num3)

The float class has the hash data model identifier but lacks the index data model identifier.

This means the dictionary can be used as a key in a mapping:

num1
hash(num1)
num_dict = {1.5: 'one and a half', 2.5: 'two and a half'}
num_dict[1.5]

But it does not make sense to try and index an ordered collection using a floating point number, there is an ambiguity whena floating point number of 1.5 is used for example and a TypeError displays:

### Unitary Data Model Identifiers

Casting a float to an integer using the int init signature wuill truncate the non-integer component of the number:

num1 = 1.5
int(num1)

Recall that the ceil, floor and trunc are function wrappers for math.ceil, math.floor and math.trunc respectively. These methods are designed to cast a non-integer number into an integer. The subtle differences between these methods can be seen with positive and a negative number:

import math
num1 = 1.5

The closest two integers to 1.5 are 1 and 2. The lower number 1 is known as the floor and the higher number 2 is known as the ceiling. Truncating the number just removes the non-integer value andis identical to using the init signature of the int class.

math.floor(num1)
math.ceil(num1)
math.trunc(num1)
num2 = -1.5
math.floor(num2)
math.ceil(num2)
math.trunc(num2)

round is a function wrapper for the builtins function round. The docstring of the round function can be examined in more detail by inputting it with open parentheis and pressing shift ⇧ and tab ↹:

The round function by default rounds to the nearest integer however the keyword argument ndigits can be used to specify the number of digits after the decimal point to round to. For example:

num1 = 1.234
round(num1)
round(num1, ndigits=2)

### Binary Data Model Identifiers

Most of the binary data model identifiers are consistent and configured to work seamlessly between the integer and float classes. An integer is automatically cast to a floating point number when used in a calculation with a binary operator and a float.

num1 = 1
num2 = 1.5
num1 + num2

Explicitly this is the same as:

float(num1) + num2

Some unexpected behaviour occurs with floating point numbers primarally due to the fact that they are under the hood stored in binary and there are recursive rounding errors:

num1 = 0.1
num2 = 0.2
num1 + num2

Particular care needs to be taken to ccount for rounding when using conditional operators:

round(num1 + num2, ndigits=6) == num3

## complex class

A complex number has a real and imaginary component. The imaginary component is a result of the square root of a negative number being undefined with only real components. The symbol $j$ is used to denote this imaginary component.

$$j=\sqrt{-1}$$

The algebra of complex numbers is similar to the algebra of vectors which have seperate $x$ and $y$ components and therefore the real axis is often visualised as $x$ and the imaginary access is visualised as $y$. In a complex number however any square term involving $j$ becomes real, taking on the original definition from above:

$j^{2}=-1$

When the method resolution order for the float class is examined, it is seen to be independent of the int class and float classes. As seen earlier however, these classes are configured to be consistent with each other to ensure compatibility:

complex.mro()

### Initialization Signature

The init signature of the complex class can be viewed by inputting the class name with open parenthesis and pressing shift ⇧ and tab ↹:

The init signaturehas two keyword arguments real and imag which each have a default value of 0.

It is more common to use the shorthand notation to initiate a complex class as seen in the output:

num1 = 2+1j
num1

### Identifiers

For example if the instance is assigned to an object name num1:

num1 = 2+1j

Then pressing num1 followed by a dot . and tab ↹ displays the identifiers:

The real attribute will read off the real component of the complex number and the imag component will read of the imaginary component, which is now non-zero:

num1 = 2+1j
num1.real
num1.imag

The complex conjugate can be calculated by using the conjugate method:

num1 = 2+1j
num1.conjugate()

Notice the real component remains unchanged but the sign of the imaginary component is flipped. The consequence of this will be explored in a bit more detail in a moment.

### Data Model Identifiers

To view the data model identifiers, the name of an instance can be input followed by a dot ., two underscores __ and a tab ↹:

Notice that there are substantially less of the mathematical data model identifiers defined. If a data model identifier is not defined, its corresponding operator cannot be used. For example there is no floordiv or mod meaning // and % cannot be used with a complex number:

There is no round so the round function cannot be used.

Likewise there is no floor, ceil or trunc so the math functions math.floor, math.ceil and math.trunc cannot be used.

For casting to other data types only complex and bool are defined. Using the complex initialization signature will instantiate the same complex number. Using the bool initialization signature will result in a True value for any non-zero real or imaginary component.

num1 = 2+1j
bool(num1)
num2 = 0+1j
bool(num2)
num3 = 0+0j
bool(num3)

A TypeError will display if attempting to cast to an int or a float:

### Unitary Data Model Identifiers

The supported unitary data model identifiers will operate on both the real and imaginary component of the complex number. For example:

num1 = 2-1j
+num1
-num1
abs(num1)

### Binary Data Model Operators

The binary operators will carry out the mathematical operation for there real components and imaginary components, treating the real and imaginary components as seperate variables. This can be seen with addition and subtraction:

$$(2+1j)+(3-2j)=(2+3)+(1-2)j=5-1j$$

num1 = 2+1j
num2 = 3-2j
num1 + num2

$$(2+1j)-(3-2j)=(2-3)+(1+2)j=-1+3j$$

num1 = 2+1j
num2 = 3-2j
num1 - num2

In multiplication, the individual terms are algebraically calculated:

$$(2+1j)\ast(3-2j)=2\ast3+1j\ast3+2j\ast-2j+1j\ast-2j=6+3j-4j-2j^{2}=6-j-2j^{2}$$

However taking the original definition of $j^{2}=-1$, this can be simplified:

$$6-j-2j^{2}=6-j-2\ast-1=6-j+2=8-j$$

num1 = 2+1j
num2 = 3-2j
num1 * num2

Now some properties of the complex conjugate can be explored. When a number is added to its complex conjugate, the real component doubles and the imaginary components cancel out. When a number is subtracted by its complex conjugate its real term cancels and its imaginary component doubles.

$$(2+1j)+(2-1j)=(4)+(1-1)j=4$$

$$(2+1j)-(2-1j)=(2-2)+(1+1)j=2j$$

Multiplication of a number by its complex conjugate yields the square magnitude of a complex number. This is real as the imaginary components cancel and $j^{2}=-1$:

$$(2+1j)\ast(2-1j)=2\ast2+1j\ast2+2\ast-1j+1j\ast1j=4+2j-2j-1j^{2}=4-j^{2}=5$$

num1 = 2+1j
num2 = num1.conjugate()
num2
num1 + num2
num1 - num2
num1 * num2

Float division of a complex number uses the complex conjugate of the denominator. This is multiplied to the top and bottom:

$$\frac{\left(2+1j\right)}{\left(2-2j\right)}=\frac{\left(2+1j\right)}{\left(2-2j\right)}\ast\frac{\left(2+2j\right)}{\left(2+2j\right)}=\frac{\left(2+1j\right)\ast\left(2+2j\right)}{\left(2-2j\right)\ast\left(2+2j\right)}$$

This means the denominator can be evaluated down to a real number:

$$\frac{\left(2+1j\right)\ast\left(2+2j\right)}{\left(4-4j+4j-4j^2\right)}=\frac{\left(2+1j\right)\ast\left(2+2j\right)}{8}$$

The numerator can then be evaluated to get the result:

$$\frac{4+2j+4j+2j^{2}}{8}=\frac{2+6j}{8}=\frac{1+3j}{4}=0.25+0.75j$$

num1 = 2+1j
num2 = 2-2j
num1 / num2

The equal to operator and the not equal to operator are setup for the complex class. These check if both the real components are equal and if both the iamginary components are equal:

num1 = 2+1j
num2 = 3-3j
num1 == num2
num1 != num2

The other four conditional operators will give a TypeError not supported. This is because there are two seperate sets of numbers to compare. The real component of num2 is higher but the imaginary component of num1 is higher, making their comparison ambiguous:

## Decimal class

Floating point arithmetic differs from the decimal arithmetic one is acustomed to being brought up with the decimal system. The recursive rounding errors seen with floating point numbers are because they are under the hood stored as a binary number. Python has a Decimal class which is more precise than the floating point numbers and used in physics or finicial application when more accurcy is required. The down side of using the Decimal class is it must be imported.

### Importing Decimal

The Decimal class (PascalCase) is found within decimal module. The decimal module can be imported using:

import decimal

Inputting decimal. and pressing tab ↹ will display a list of identifiers:

Most of the identifers are related to the context. To examine the context, the getcontext function can be used:

The context has a precision prec which has a default value of 28. The identifier MAX_PREC specifies the maximum precision possible on a supercomputer, although using this value will likely exceed your computer memory.

The context has rounding which has a default value of ROUND_HALF_EVEN. The other possible rounding modes are all other identifiers ROUND_05UP, ROUND_CEILING, ROUND_DOWN, ROUND_FLOOR, ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_HALF_UP, ROUND_UP.

The context has Emin and Emax which are the minimum and maximum values of the exponent. The identifiers MIN_EMIN and MAX_EMAX specifies the minimum and maximum values for the exponent possible on a supercomputer, once again using these values will likely exceed your computer memory. The context has capitals which is assigned to a boolean value. If capitals is enabled E will display in the exponent, otherwise e will display in the exponent.

The Etiny is a value equal to Emin – prec + 1. There is a corresponding minimum possible value MIN_ETINY possible on a supercomputer, once again using this value will likely exceed your computer memory.

Otherwise there are flags and traps. A flag will flag up a warning but proceed with an operation while a trap will raise an error, halting any subsequent Python code execution. The signals available for flags and traps are Clamped, DecimalException, DivisionByZero, Inexact, InvalidOperation, Overflow, Rounderd, Subnormal, Underflow, FloatOperation. Some other signals are available for traps DivisionImpossible, DivisionUndefined, InvalidContext, ConversionSyntax.

Identifiers can be accessed from the getcontext function by inputting decimal.getcontext(). and pressing tab ↹ :

The statements can be accessed, essentially as attributes and assigned to new values.

It is also possible to instantiate one of the classes Context, BasicContext, DefaultContext and ExtendedContext to use with the function setcontext. localcontext is an option to use a local context within a context manager, typically a with code block.

The main item of interest is the Decimal class, there is alsp the DecimalTuple which is a named tuple representation.

This guide will use the default context and therefore just import the Decimal class directly.

from decimal import Decimal

### Initialization Signature

Once imported the docstring of the Decimal class can be viewed by inputting it with open parenthesis and pressing shift ⇧ and tab ↹:

The initialization signature has a keyword input argument which is normally a string:

num1 = Decimal(value='1')
num1

Value can also be an integer. Notice that the representation defaults to a string:

num1 = Decimal(value=1)
num1

When the value is not an integer, it should be supplied as a string:

num1 = Decimal(value='0.1')
num1

Notice that if the value is supplied as a floating point number, the limited precision and recursive rounding errors due to float binary encoding carry over to the conversion:

num1 = Decimal(value=0.1)
num1

For this reason it is preferable to use a string to instantiate a decimal opposed to another number. There are some more details about this in the alternative constructor from_float which is a class method. Its docstring can be viewed by inputting the method with open parenthesis followed with a shift ⇧ and tab ↹:

Using this alternative constructor, gives a similar result to providing a floating point number:

num1 = Decimal.from_float(0.1)
num1

It is also possible to instantiate using a tuple of integers. This has the form: (sign, (digits), power) where the sign is 0 for positive values and 1 for negative values. The digits are each of the digits in the mantissa and the power is the power of the decimal exponent.

num1 = Decimal(value=(0, (1,), -1))
num1

If using the tuple notation, it is often easier to use the named tuple class DecimalTuple, which adds names to the fields in the tuple and therefore makes the code more readible:

from decimal import DecimalTuple
dectuple1 = DecimalTuple(sign=0, digits=(1,), exponent=(-1))
dectuple1
num1 = Decimal(value=dectuple1)
num1

Or more directly:

num1 = Decimal(value=DecimalTuple(sign=0, digits=(1,), exponent=(-1)))
num1

### Identifiers

For example if the instance is assigned to an object name num1:

num1 = Decimal(value='0.1')

Then pressing num1 followed by a dot . and tab ↹ displays the identifiers:

There is a large number of identifiers available for the Decimal class. These identifiers include equivalents to mathematical functions that are compartmentalised into a seperate math module for the the other numeric datatypes. These will be covered in the math module tutorial once some other basics have been established.

The Decimal class is setup, like the other number types for compatibility with the complex class.

num1 = Decimal(value='0.1')
num1.real
num1.imag
num1.conjugate()

Although there is some compatibility, the complex class only operates with floating point numbers and any decimal instances are essentially cast to floating point approximations. This can be seen if the following is attempted:

num1 = Decimal(value='0.1')
num2 = Decimal(value='0.2')
num3 = complex(real=num1, imag=num2)
num4 = complex(real=num2, imag=num1)
num3 + num4

There is also some compatibility with the Fraction class:

num1 = Decimal(value='0.1')
num1.as_integer_ratio()

The as_tuple method and to_eng_string will display the decimal as DecimalTuple and engineering string respectively:

num1 = Decimal(value='1.2345e8')
num1.as_tuple()

The engineering generally uses three digits and then a power that is a multiple of three.

num1.to_eng_string()

In engineering and scientific applciations typically the power and the unit are combined into a unit with a prefix. The above number if it was a length for example would be taken as 123.45 megametres or 123.45 Mm.

### Data Model Identifiers

To view the data model identifiers, the directory function dir can be used:

num1 = Decimal(value='0.1')
pprint.pprint(dir(num1), compact=True)

Differences between the formal string representation and informal string representation can be seen by comparing the output from the repr and str functions respectively:

num1 = Decimal(value='0.1')
repr(num1)
str(num1)

The data model identifiers in the Decimal class behave similarly to their counterparts in the float class. The recursive rounding issues prevalent due to binary encoding are not prevalent:

num1 = Decimal(value='0.1')
num2 = Decimal(value='0.2')
num1 + num2
0.1 + 0.2
num1 = Decimal(value='0.5')
num2 = Decimal(value='0.4')
num1 - num2
0.5 - 0.4
num1 = Decimal(value='0.1')
num2 = Decimal(value='0.2')
num3 = Decimal(value='0.3')
num3 == num1 + num2

The recursive rounding issues prevalent due to decimal encoding are however prevalent:

num1 = Decimal(value='1')
num2 = Decimal(value='3')
onethird = num1 / num2
onethird + onethird + onethird
onethird * 3

Care needs to be taken with comparison operators when decimal recursive rounding issues occur:

threethird = onethird + onethird + onethird
threethird
num1 == threethird
num1 > onethird + onethird + onethird

### Traps

The traps are configured, by default to match those of the float class:

1.0 / 0.0
num1 = Decimal(value='1.0')
num2 = Decimal(value='0.0')
num1 / num2

This behaviour is defined in the context trap. To view the context, import the following:

from decimal import Decimal, getcontext, DivisionByZero

Then use the function getcontext:

If getcontext() is input followed by a dot . and tab ↹ a number of identifiers can be seen:

The traps statement and clear_traps function can be used:

getcontext().traps
getcontext().clear_traps()
getcontext().traps

Notice that the traps statement displays a dictionary. A dictionary has key value pairs. In this case the keys are the error classes and the values are boolean True or False. When the traps are cleared these all have a value of False. Now the following works:

num1 / num2

Notice that the returned Decimal instance incorporates a string of 'Infinity'.

The traps dicitionary can be indexed into with one of the error class signals and assigned a boolean value of True. This enables the trap and the error class will display when division by zero is attempted:

getcontext().traps[DivisionByZero] = True
num1 / num2

Other traps can be enabled or disabled in the context in a similar manner.

## Fraction class

The Fraction class is used for fractions.

### Importing Fraction

The Fraction class (PascalCase and singular) is found within fractions module (snake_case and plural). The fractions module can be imported using:

import fractions

A list of identifiers can be seen by inputting fractions followed by a dot . and tab ↹:

Most of the identifiers are modules or dependencies. The main identifier of interest is the Fractions class which is typically imported directly:

from fractions import Fraction

### Initialization Signature

The init signature of the Fraction class can be viewed by inputting the class name with open parenthesis and pressing shift ⇧ and tab ↹:

A Fraction is normally created with two integers. These can be supplied using the numerator and denominator keyword arguments. The default value for the numerator is 0 and for the denominator is None whch in this case is a value of 1 effectively creating a fraction that is an integer.

The following fraction can be created:

$\text{num1}=\frac{3}{8}$

num1 = Fraction(numerator=3, denominator=8)
num1
num1 = Fraction('3/8')
num1

It is also possible to instantiate a Fraction using a Decimal or float instances for the numerator.

num1 = Fraction(numerator=Decimal('0.2'))
num1

Note the recursive rounding issues for floating point numbers will be carried over into the fraction:

num1 = Fraction(numerator=0.2)
num1

A string of a number which incorporates a decimal point, will be assumed to be a Decimal:

num1 = Fraction(numerator='0.2')
num1

The method resolution order for the Fraction class can be examined using:

fractions.Fraction.mro()

Unlike the other data types examined so far which are written in C. The fractions module is available as a physical script file which can be examined:

### Identifiers

For example if the instance is assigned to an object name num1:

num1 = Fraction(numerator=3, denominator=8)
num1

Then pressing num1 followed by a dot . and tab ↹ displays the identifiers:

The Fraction class has limited interoperability with the complex class and can be conceptualised as the complex number:

$$\frac{3}{8}+0j$$

where $j = \sqrt{-1}$.

The real attribute reads the real value will match the value of the fraction in this case £\frac{3}{8}\$ and the imag will be 0. The associated method conjugate takes these two attributes and inverts the sign of the imag attribute, because this is 0 the complex conjugate matches the original Fraction:

num1 = Fraction(numerator=3, denominator=8)
num1
num1.real
num1.imag
num1.conjugate()

The complex class converts all components made with Fractions into a component of a floating point number:

num1 = Fraction(numerator=3, denominator=8)
num1
num2 = Fraction(numerator=5, denominator=8)
num2
complex(real=num1, imag=num2)

The numerator and the denominator attributes are accessible. The associated method as_integer_ratio returns these two attributes as a tuple:

num1 = Fraction(numerator=3, denominator=8)
num1
num1.numerator
num1.denominator
num1.as_integer_ratio()

from_decimal and from_float are class methods which can be used to construct a new instance from a Decimal or float. These behave similarly to the initialization signature:

num1 = Fraction.from_decimal(Decimal('0.1'))
num1
num2 = Fraction.from_float(0.1)
num2

Notice that num2 has a huge numerator and huge denominator and this is due to the recirsvie rounding errors of the floating point number. The method limit_denominator can be used to limit the denominator and can be thought of a Fraction equivalent of rounding:

num2 = Fraction.from_float(0.1)
num2
num2.limit_denominator(max_denominator=1000)

### Data Model Identifiers

To view the data model identifiers, a Fraction instance name can be input followed by a dot ., two underscores __ and a tab ↹:

There are also some identifiers beginning with a single underscore. These are used internally by the equivalent data model identifiers with the same name.

Recall that the init data model method is called when instantiating a Fraction. When the new Python object is created, the new data model method is called. This creates the new instance which is given the label or object name and then the initialization signature init is called to initialize the instance with the unique numeric data. The getattribute data model identifier is used when an attribute is accessed.

The Python functions repr and str use the data model identifiers repr and str. Differences between the formal string representation and informal string representation can be seen by comparing the output from the repr and str functions respectively:

num1 = Fraction(numerator=3, denominator=8)
repr(num1)
str(num1)

Recall that the Python function dir uses the data model identifier dir to display the directory of the object. The Python type function uses the data model identifier class:

type(num1)

The data model identifier doc is the document string for a string instance. It is more commonly used with the ? which includes some other information from the data model identifiers type, str, and doc:

? num1

The format data model identifier is typically used by a formatted string:

f'The fraction is {num1}.'

The copy.copy and copy.deepcopy functions use the data model identifiers copy and deepcopy:

from copy import copy, deepcopy
num2 = copy(num1)
num2
num3 = copy(num1)
num3

### Unitary Data Model Identifiers

The unitary operators +, – and the function abs use the data model identifiers pos, neg and __abs__respectively:

num1 = Fraction(numerator=3, denominator=8)
+num1
-num1
abs(-num1)

Casting to int, bool, float or complex use the data model identifiers int, bool, float or complex. When these are used the fraction is essentially handled intermediately as float instance and then cast into the other data types:

num1 = Fraction(numerator=11, denominator=8)
num1
float(num1)
int(num1)
bool(num1)
complex(num1)

The math.trunc, math.floor, math.ceil and round use the data model identifiers trunc, floor, math and round respectively. When these are used the fraction is once again essentially handled intermediately as float instance and then handled like the float instance owuld be:

num1 = Fraction(numerator=11, denominator=8)
num1
float(num1)
math.trunc(num1)
math.floor(num1)
math.ceil(num1)
round(num1, ndigits=2)

### Binary Data Model Identifiers

The binary data model methods require two numeric instances, for example:

$$\text{num1} = \frac{3}{4}$$

$$\text{num2} = \frac{1}{8}$$

These can be instantiated using:

num1 = Fraction(numerator=3, denominator=4)
num2 = Fraction(numerator=1, denominator=8)

It is easier to thnk of the * operator first which uses the mul data model identifier. Multiplication of the fractions is the product of the numerators divided by the product of the denominators:

$$\text{num1} \ast \text{num2} = \frac{3}{4} \ast \frac{1}{8} = \frac{3 \ast 1}{4 \ast 8} = \frac{3}{32}$$

This can be calculated using:

num1 * num2

Float division can be thought of multiplication of the inverse:

$$\text{num1} / \text{num2} = \frac{3}{4} / \frac{1}{8} = \frac{3}{4} \ast \frac{8}{1} = \frac{3 \ast 4}{8 \ast 1} = \frac{6}{1} = 6$$

$$\text{num2} / \text{num1} = \frac{1}{8} / \frac{3}{4} = \frac{1}{8} \ast \frac{4}{3} = \frac{1 \ast 4}{8 \ast 3} = \frac{1}{6}$$

This can be calculated using the / operator which uses the truediv data model identifier:

num2 / num1

The floor division // and the modulus % operators use the floordiv and mod data model identifiers. For Fractions they are calculated using the following:

$$\text{num2} // \text{num1} = \frac{1}{8} // \frac{3}{4} = \frac{1 \ast 4 // 3 \ast 2}{8 \ast 4} = \frac{4 // 6}{32} = \frac{0}{32} = 0$$

$$\text{num2} \text{﹪} \text{num1} = \frac{1}{8} \text{﹪} \frac{3}{4} = \frac{1 \ast 4 \text{﹪} 3 \ast 2}{8 \ast 4} = \frac{4 \text{﹪} 6}{36} = \frac{4}{36} = \frac{1}{8}$$

num2 // num1
num2 % num1

The divmod function returns these two values as a tuple and uses the divmod data model identifiers:

divmod(num2, num1)

The ** operator can be used to calculate the power of a Fraction instance to another Fraction instance. For simplicity the two numbers will be used:

$$\text{num1} = \frac{3}{4}$$

$$\text{num2} = \frac{1}{8}$$

num1 = Fraction(numerator=1, denominator=4)
num2 = Fraction(numerator=3, denominator=2)

$$\text{num1} ** \text{num2} = \frac{1}{4} ** \frac{3}{2} = \frac{1 ** \frac{3}{2}}{4 ** \frac{3}{2}} = \frac{1 ** {3} ** \frac{1}{2}}{4 ** 3 ** \frac{1}{2}} = \frac{1 ** \frac{1}{2}}{64 ** \frac{1}{2}} = \frac{1}{8} = 0.125$$

num1 ** num2

Addition and subtraction require a common denominator. For example:

$$\text{num1} = \frac{3}{4} = \frac{6}{8}$$

$$\text{num2} = \frac{1}{8}$$

The Fractions can be expressed as:

num1 = Fraction(numerator=3, denominator=4)
num1
num2 = Fraction(numerator=1, denominator=8)
num2

Sometimes for convenience the _normalize keyword input argument can be assigned to False:

num1 = Fraction(numerator=6, denominator=8, _normalize=False)
num1

With a common denominator, addition and subtraction are straightforward:

$$\text{num1} + \text{num2} = \frac{6}{8} + \frac{1}{8} = \frac{6 + 1}{8} = \frac{7}{8}$$

$$\text{num1} – \text{num2} = \frac{6}{8} – \frac{1}{8} = \frac{6 – 1}{8} = \frac{5}{8}$$

num1 + num2
num1 - num2

The six equality operators equals to ==, not equals to !=, less than <, less than or equal to <=, greater than > and greater than or equal to >= use the data model methods eq, ne, lt, le, gt and ge respectively. These operate in a similar manner to the int and float classes. The Fraction instances can be conceptualised as floats when using these operators however as the components of the Fraction i.e. the numerator and denominator are integers, these comparisons are without the recursive rounding errors. For example, the following numbers can be created:

num1 = Fraction(numerator=1, denominator=10)
num1
num2 = Fraction(numerator=2, denominator=10)
num2
num3 = Fraction(numerator=3, denominator=10)
num3

And the comparisons can be made. Notice the two sides of the is equal to operator == are equivalent in the case of the Fraction class:

num3 == num1 + num2
num3 != num1 + num2

This is not the case when every numeric value is cast to a float, as seen earlier:

float(num3) == float(num1) + float(num2)