Here we focus on the 'data type' of numbers, of which there are two main ones we are concerning ourselves with: `int`

and `float`

A data type is (binary) **data** that is structured and has rules of its access and use applied to it by the programming language you are using (Python). A bunch of binary data in memory might be interpreted by Python as a string for characters, or a decimal number, or an integer. It might also represent the data for a function definition! Only when you follow Python's rules for data types can you give this blob of binary bits structured existence and use.

For example, the following values are stored in the variables `a`

and `b`

look the same to a human, but are fundamental different to Python.

`a`

refers to a numerical literal 22. It is interperted as a valid number that can be used for arithmetic expressions.`b`

is a string of characters '2' and '2'. Mathematically they are meaningless.In the cell below, I attempt to add

`a + b`

and it fails. This is expected! The string characters is not a mathmetically capable data type!

In [1]:

```
a = 22
```

In [2]:

```
b = "22"
```

In [3]:

```
a + b
```

`int`

and `float`

values¶Integer (`int`

) and decimal (`float`

) numbers are compatible data types in Python. While they are not at all stored the same underneath, they are mathematically compatible. What happens if you perform math using an `int`

and a `float`

(mixed data types)? In this case, it usually defaults to the data type that gives the value the most information (which is a `float`

.) A fraction part of a number is more informative data than just a whole number.

In the example below, we add `a`

to the literal `float`

of `0.6767`

. The resulting value from this addition is a new `float`

value of `22.6767`

. This result cannot be stored as an integer, obviously.

In [4]:

```
a + 0.6767
```

Out[4]:

`type`

-ing¶If you want to know *exactly* what data type a value is, we can use the `type()`

function. Give it anything (an identifier, a literal) and `type()`

explains to you what data type that value is.

In [5]:

```
type(a)
```

Out[5]:

In [6]:

```
type(b)
```

Out[6]:

In [7]:

```
type(0.6767)
```

Out[7]:

In [8]:

```
print(type(a))
```

Below are examples of the basic arithmetic operations. Note the data type returned from the evaluations.

In [11]:

```
val = 3.0 + 4.0
print(val)
```

In [12]:

```
3 + 4
```

Out[12]:

In [13]:

```
3 * 4
```

Out[13]:

In [14]:

```
10.0/3.0
```

Out[14]:

In [15]:

```
10//3
```

Out[15]:

The evaluation of `result`

, below, from the use of the "integer division" operator, `/`

, is a whole number: `3.0`

despite being whole it's value represented as a `float`

data type instead of an `integer`

data type. Some functions, like `range()`

only operate on integers. What if I wanted to use `result`

in my call to `range()`

? Well, we can call the function `int()`

. It's a special function that is representative of the `int`

data type. This function *makes* new integers! We use it to our advantage here by making a new `int`

from the `float`

value of `3.0`

. In the end, `result`

now stores `3`

, not `3.0`

.

In [57]:

```
result = 10.0//3.0
```

In [58]:

```
result = int(result)
```

In [59]:

```
result
```

Out[59]:

In [60]:

```
for n in range(result):
print(n)
```

You have a full breadth of numerical comparison operators (equality, inequality, greater than, less than, etc), logical operators (`and`

, `or`

, `not`

) that can be nicely intermixed with your regular arithmetic operators to make some complex expressions.

In [26]:

```
c = 75667
```

In [27]:

```
a > c
```

Out[27]:

In [28]:

```
a == c
```

Out[28]:

In [29]:

```
a != c
```

Out[29]:

In [30]:

```
a >= c
```

Out[30]:

In [31]:

```
c >= a
```

Out[31]:

In [35]:

```
c <= (a ** 10)
```

Out[35]:

In [36]:

```
(a != 6) and (c > 3)
```

Out[36]:

In [37]:

```
(a != 6)
```

Out[37]:

In [38]:

```
(c > 3)
```

Out[38]:

In [41]:

```
(17 / 4) * 4 == 17
```

Out[41]:

In [42]:

```
17.0 == 17
```

Out[42]:

Data in other languages are 'dumb', 'passive'. They exist as just a bundle of bits 'typed' as the data they are supposed to be (`int`

, `float`

, like in the C language); however any actions on the data are performed externally on that data and it might violate the rules of the data type (C can do crazy things).

In Python (and other languages), data types are extended in concept and are based on "objects" (hence, "object-oriented" languages). The data is now 'smarter', more active. **Everything** used in Python is an object of one type or another. Every literal you type is evaluted into its proper data type (`str`

, `int`

, `float`

, `list`

, etc). Data is now not just the value(s) it represents but it also comes packaged with *actions*, the functions that **act** on that data.

In short, objects are data types that are packaged values **and** functions that operate on those values, in one nicely encapsulated entity. Every `int`

, for example, has the value and functions within that `int`

that operate on that one `int`

value. Every `int`

created has their own set of these functions meant for `int`

manipulation. The same goes for any data type. This enforces the correct actions that can be done the data.

For example, a lesser known but still valid operation to perform on an `int`

is to check its 'bit length', or the number of binary bits used to represent a base-10 integer.

We set `a`

to the `int`

value of `3`

. This is base-10 that we all know and love. To ask that `int`

how many binary bits (one's and zero's) are used to represent this number, we call the `bit_length()`

function on the `int`

object. It is a valid action only for `int`

s! It returns `2`

, which means that the binary string ($11$, or $2^1 + 2^0$, represents base-10 integer `3`

). OK, that's so important as the fact we used a function bundled with what we thought was just plain old integer data!

In [69]:

```
a = 3
a.bit_length()
```

Out[69]:

Even mundane operations like `a + 2`

do not externally act on the `int`

represented by `a`

but actually calls a special function within the `int`

object called `__add__`

(pronounced "dunder add"). See the two versions, below. No matter what, you are "asking" an object to perform work. If it doesn't support that action it will fail!

In [71]:

```
a + 2
```

Out[71]:

In [72]:

```
a.__add__(2)
```

Out[72]:

How do objects store all these bits of information about itself? The `int`

object needs to store its value and the functions it supports that operate on that value. These *attributes* are stored in a **namespace**. *Every* object in Python has a namespace and it's a 'bucket' that holds all the identifier attributes stored within an object.

The following is the namespace of our `int`

object, `a`

. We use the function `dir()`

:

In [73]:

```
print(dir(a))
```

All the "dunder names", or attributes with "" can be ignored for now. We will touch on them near the end of the course and only a select few. The functions you might use on an

`int`

would be near the end of that list.Each data type has its own namespace with its own attributes valid of that data type

You use the `.`

(dot) operator to access function or value attributes inside an object's namespace.

Modules are an excellent example of objects. You can *import* another Python code file into yours to use the code within it for something useful. The `math`

module is one such module that comes included with Python upon installation. We `import math`

to "bring in" the math module for use.

In [43]:

```
import math
```

The identifier `math`

is created and is now a variable that refers to a module object. The namespace of this of this module shows you all the items it has made available for use. The `math`

module comes with many math operations (`pow`

, `sqrt`

, `log`

) and even some `float`

constants like (`pi`

, or `e`

).

As mentioned above, the `.`

(dot) operator is used to access these attributes of any object, the module object included.

In [44]:

```
math
```

Out[44]:

In [45]:

```
dir(math)
```

Out[45]:

We use the dot operator to access `sqrt`

:

In [74]:

```
math.sqrt
```

Out[74]:

Opps, it's a function and I didn't properly run it! Instead, I asked Python to 'describe' that identifier and it explained to me that it's a function. OK, let's properly run it:

In [75]:

```
math.sqrt(100)
```

Out[75]:

Let access the constant for $\pi$!

In [78]:

```
math.pi + 10
```

Out[78]:

If you attempt to access an attribute that doesn't exist in an object's namespace, its as if you access a variable name that wasn't defined yet, but referred to as an `AttributeError`

in this context:

In [77]:

```
math.foobar
```

`import`

¶Namespaces should be kept clean. Import the module you need, wholesale or selectively import what you need into your "main" or top-level namespace. The following imports just `sqrt()`

into my main namespace. I don't need to access it using `math.sqrt`

anymore!

In [79]:

```
from math import sqrt
```

In [80]:

```
sqrt(100)
```

Out[80]:

What's my "main namespace" and what does it look like? Namespaces are hierarchial trees of objects. You start with a main namespace in which other objects with their own namespaces are stored. You can call `dir()`

without any parameters to see what this main namespace looks like:

In [53]:

```
dir()
```

Out[53]: