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 Crash Course 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.

print(type(None))
print(type(NotImplemented))
<class 'NoneType'>
<class 'NotImplementedType'>

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.

a = None
b = None
print(id(a))
print(id(b))
94287211210560
94287211210560
a = 'Some string'
b = 'Some string'
print(id(a))
print(id(b))
139871271436464
139871271437360

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.

a = '123'    # a string
b = int(a)
print(type(b))
print(b)
<class 'int'>
123
a = 123    # an integer
b = str(a)
print(type(b))
print(b)
<class 'str'>
123
a = 2    # an integer
b = float(a)
print(type(b))
print(b)
<class 'float'>
2.0

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

a = 2.34    # a float
b = int(a)
print(type(b))
print(b)
<class 'int'>
2

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.

print(bool(None))
print(bool(0))
print(bool(123))
print(bool(''))
print(bool('hello'))
False
False
True
False
True

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

if not '':
    print('cumbersome condition satisfied')
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.

a = 3

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

if a == True:
    print('second if')
else:
    print('second else')
first if
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 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:

a = 1
print(id(a))

a = a + 1
print(id(a))
139871335317744
139871335317776

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:

a = [1, 2, 3]
print(id(a))

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

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:

a = 1    # immutable integer
b = a

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

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.

a = [1, 2, 3]    # mutable list
b = a

a[0] = 4
print(a, b)
[4, 2, 3] [4, 2, 3]

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.

print(isinstance(8, int))
print(isinstance(8, str))
print(isinstance(8.0, float))
True
False
True

Useful Dunder Functions for Custom Types#

There are a bunch of dunder functions one should implement when creating custom types (cf. Custom Object 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 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):

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

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.