Types
Contents
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 byprint
and whenever one tries to convert an object to string viastr(...)
.__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 tobool
.__len__
is called by the built-in functionlen
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.