---
jupytext:
  formats: content_md///md:myst,content///ipynb
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.13.8
kernelspec:
  display_name: Python 3 (ipykernel)
  language: python
  name: python3
---

(python:variables:types)=
# Types

Here we introduce two more very fundamental object types, discuss type conversion and introduce the Python-specific concept of immutability.

## More Types

In the [](python:crash) we met several data or object types (classes):
* integers
* floats
* booleans
* lists

We also met strings, which will be discussed in more detail later on. Python ships with some more data types. Next to complex numbers, which we do not consider here, and several list-like types Python knows two very special data types:
* None type
* NotImplemented type

Both types can represent only one value: `None` and `NotImplemented`, respectively. Thus, they can be considered as constants. But since in Python everything is an object, constants are objects, too. Objects have a type (class). Thus, there is a None type and a NotImplemented type.

```{code-cell} ipython3
print(type(None))
print(type(NotImplemented))
```

Existence of `NotImplemented` will be justified soon. Typically it's used as return value of functions to signal the caller that some expected functionality is not available.

The value `None` is used whenever we want to express that a name is not tied to an object. In that case we simply tie the name to *the* `None` object. We write 'the' because the Python interpreter creates only one object of None type. Such an object can hold only one and the same value. So there is no reason to create several different `None` objects.

```{code-cell} ipython3
a = None
b = None
print(id(a))
print(id(b))
```

```{code-cell} ipython3
a = 'Some string'
b = 'Some string'
print(id(a))
print(id(b))
```

In the second code block two string objects are created although both hold the same value. For both `None` values in the first code block only one object is created. If you play around with this you may find, that for short strings and small integers Python behaves like for `None`. This issue will be discussed in detail soon.

```{hint}
`None` is a Python keyword like `if` or `else` or `import`. It is used to refer to the object of None type. But the memory occupied by this object does not neccessarily contain a string 'None' or something similar. In fact, this object does not contain something useful. Its mileage is its existence, which allows to tie (temporarily unused) names to it. Same holds for `NotImplemented`.

We already met this concept when introducing boolean values. `True` and `False` are Python keywords, too. They are used to refer to two different objects of type `bool`. But these objects do not contain a string 'True' or 'False' or something similar. Instead, a `bool` object stores an integer value: 1 for the `True` object and 0 for the `False` object. How to represent `None`, `True` and so on in memory depends on the concrete implementation of the Python interpreter and is not specified by the Python programming language.
```

## Type Casting

Type casting means change of data type. An integer could be casted to a floating point number, for example. Python does not have a mechanism for type casting. Instead, dunder methods can be implemented to work with objects of different types.

A very prominent dunder method for handling different object types is the `__init__` method, which is called after creating a new object. Its main purpose is to fill the new object with data. For Python standard types like `int`, `float`, `bool` the `__init__` method accepts several different data types as argument. 

We've already applied the function `int`, which creates an `int` object, to strings. Thus, we have seen that the `__init__` method of `int` objects accepts strings as argument and tries to convert them to an integer value. The other way round, `str` for creating string objects accepts integer arguments.

```{code-cell} ipython3
a = '123'    # a string
b = int(a)
print(type(b))
print(b)
```

```{code-cell} ipython3
a = 123    # an integer
b = str(a)
print(type(b))
print(b)
```

```{code-cell} ipython3
a = 2    # an integer
b = float(a)
print(type(b))
print(b)
```

Data may get lost due to type casting. The Python interpreter will **not** complain about possible data loss.

```{code-cell} ipython3
a = 2.34    # a float
b = int(a)
print(type(b))
print(b)
```

```{hint}
It's good coding style to use explicit type casting instead of relying on implicit conversions whenever this increases readability.

A counter example is `1.23 * 56`, where the integer `56` is converted to float implicitely. Explicit casting would decrease readability: `1.23 * float(56)`.
```

```{note}
If you define a custom object type, it depends on your implementation of the type's `__init__` method what data types can be cast to your type.
```

## Casting to Booleans

Casting to `bool` maps 0, empty strings and similar values to `False`, all other values to `True`.

```{code-cell} ipython3
print(bool(None))
print(bool(0))
print(bool(123))
print(bool(''))
print(bool('hello'))
```

If we use non-boolean values where booleans are expected, Python implicitly casts to bool:

```{code-cell} ipython3
if not '':
    print('cumbersome condition satisfied')
```

For historical reasons boolean values internally are integers (0 or 1). This sometimes yields unexpected (but well-defined) results. An example is the comparison of integers to `True`.

```{code-cell} ipython3
a = 3

if a:
    print('first if')
else:
    print('first else')

if a == True:
    print('second if')
else:
    print('second else')
```

The first condition is equivalent to `bool(3)`, which yields `True`, whereas the second is equivalent to `3 == 1`, yielding `False`. See [PEP 285](https://peps.python.org/pep-0285/#resolved-issues) for some discussion of that behavior (PEP 285 introduced `bool` to Python).

## Immutability

Objects in Python can be either *mutable* or *immutable*. Mutable objects allow modifying the value they hold. Immutable objects do not allow changing their values. Objects of simple type like `int`, `float`, `bool`, `str` are immutable whereas lists and most others are mutable.

Understanding the concept of (im)mutability is fundamental for Python programming. Even if the source code suggests that an immutable object gets modified, a new object is created all the time:

```{code-cell} ipython3
a = 1
print(id(a))

a = a + 1
print(id(a))
```

This code snipped first creates an integer object holding the value 1 and then ties the name `a` to it. In line 3, sloppily speaking, `a` is increased by one. More precisely, a new integer object is created, holding the result 2 of the computation, and the name `a` is tied to this new object.

Mutable objects behave as expected:

```{code-cell} ipython3
a = [1, 2, 3]
print(id(a))

a[0] = 4
print(id(a))
```

Immutability of some data types allows the Python interpreter for more efficient operation and for code optimization during execution. We will discuss some of those efficiency related features later on.

Always be aware of (im)mutability of your data. The following two code samples show fundamentally different behavior:

```{code-cell} ipython3
a = 1    # immutable integer
b = a

a = a + 1
print(a, b)
```

Increasing `a` does not touch `b`, because the integer object `a` and `b` refer to is immutable. Increasing `a` creates a new object. Then `a` is tied to the new object and `b` still refers to the original one.

```{code-cell} ipython3
a = [1, 2, 3]    # mutable list
b = a

a[0] = 4
print(a, b)
```

Modifying `a` also modifies `b`, because `a` and `b` refer to the same mutable object.

## Getting the Type

Although rarely needed, we mention the built-in function `isinstance`. It takes an object and a type as parameters and returns `True` if the object is of the given type.

```{code-cell} ipython3
print(isinstance(8, int))
print(isinstance(8, str))
print(isinstance(8.0, float))
```

## Useful Dunder Functions for Custom Types

There are a bunch of dunder functions one should implement when creating custom types (cf. [](python:crash:everything-object:custom-types)):
* `__str__` is called by the Python interpreter to get a text representation of an object. For instance, it's called by `print` and whenever one tries to convert an object to string via `str(...)`.
* `__repr__` is simlar to `__str__` but should return a more informative string representation. In the best case, it returns the Python code to recreate the object. See [Python's documentation](https://docs.python.org/3/reference/datamodel.html#object.__repr__) for details.
* `__bool__` is called whenever an object has to be cast to `bool`.
* `__len__` is called by the built-in function `len` to determine an object's length. This is useful for list-like objects.

## Types are Objects

Since everything in Python is an object, types are objects, too. Thus, types may provide member variables and methods in addition to the corresponding objects' member variables and methods. In some programming languages members of a type are called *static members*.

Member variables of types occur for instance if constants have to be defined (almost always for convenience):

```{code-cell} ipython3
class ColorPair:
    
    red = (1, 0, 0)
    green = (0, 1, 0)
    blue = (0, 0, 1)
    yellow = (1, 1, 0)
    cyan = (0, 1, 1)
    magenta = (1, 0, 1)
    
    def __init__(self, color1, color2):
        self.color1 = color1
        self.color2 = color2

        
my_pair = ColorPair(ColorPair.red, ColorPair.yellow)
```

Member functions of types are rarely used. One usecase are very flexible contructors for complex types, which do not fit into the `__init__` method due to many different variants of possible arguments. Often such constructors are named `from_...` and corresponding object creation code looks like
```python
my_object = SomeComplexType.from_other_type(arg1, arg2, arg3)
```
In such cases the `from_...` methods return a new object of corresponding type, that is, they implicitly call the `__init__` method.

Defining methods for types requires advanced syntax contructs we do not discuss here.
