Python: A Snake That Loves Objects
An explanation of objects in object-oriented programming
Almost everything is an object in Python! In this piece we are going to talk about those objects.
First, what is an object? According to webopedia an object is “a self-contained entity that consists of both data and procedures to manipulate the data”. To draw a parallel with areal-world object, take a coffee machine: it has materials which represent the data (i.e. coffee beans), and it has functionalities which represent the procedures to manipulate the data (i.e. grinding the beans). Variables, class instances, functions, methods, and many other things are objects in Python.
Now let’s dive into the wonders of how the Python language treats everything as an object.
id and type
Every object in Python has an id, a type and a value.
The id of an object is its identity — its address in memory. According to geeksforgeeks, “this identity has to be unique and constant for this object during its lifetime”. In other words, when an object is created it gets an identity, or an address in memory, and when the object is deleted it loses this identity. For programmers who have never dealt with low-level languages, this concept may be new, but the fact is that everything in a computer is stored somewhere in the memory of the machine. The id of an object can be retrieved with function id().
Let’s take an example with an integer a of value 9.
>>> a = 9
In this case the id of the variable
10105344. Of course, this will depend on your machine and where the variable is saved by the Python interpreter.
The type of an object is, well…its type. To check the type of an object, we can pass it to the
type() function. Let’s look at our
Here we see that the function doesn’t just return
int, as expected, but
class ‘int’. In Python, there are no built in data types, as there are in C or other programming languages. The types are actually classes and each variable is an instance of the corresponding class. So in our case,
a is an instance of the
Mutable objects in Python are objects that can be modified. For example, lists, sets, and dictionaries are mutable. Let’s see what that means:
We created a first list of ints,
l1, and then assigned
l1 to a new list
l2. We then appended an element to
l1. By now
l1 should look like
[1, 2, 3, 4]. But what about
l2? When we print it, we see that it has been updated too, even though we assigned
l2 before modifying
l1. What we did in the second line is called aliasing. We created an alias of
l2, but both those names point to the same object. We can verify this by using the
id() function we mentioned earlier, or the
l2 have the same identity — they’re pointing to the same object.
isoperator is different from the
==operator. The former evaluates if two names point to the same object in memory and the latter evaluates if they have the same value.
If we want to make a copy of a list without modifying the original, we need to clone it. This can be done with the slicing method:
The cloning worked and
l2 is now referencing to a different object than
l1, as their ids are different.
Numbers, strings and tuples are immutable objects in Python. That means that they can’t be modified in place. Let’s take an example with ints:
We see now that if
b and we modify
b will remain unmodified. That is the meaning of immutable.
How Python treats mutable and immutable objects differently
We’ve seen that you can’t modify immutable objects after assignation, but you can do it with mutable objects.
So what goes on under the hood? With the example of our lists
l2, Python will reassign all the elements of the list to add a new element to it. This is not as efficient in terms of memory and time. Immutable objects, on the other hand, are quicker because the whole object is not reassigned every time we try to modify it — Python will just create a new object with the updated value.
How arguments are passed to functions and what that implies for mutable and immutable objects
The way Python stores variables in memory is interesting. For people familiar with the C programming language, it works a bit like pointers. Variables in Python store the address (or the id) of the object they reference.
That being said, an argument is passed by reference to a function. That means we don’t actually pass objects to functions, we pass references to them. In general, a function works this way: it has a name and either takes arguments or doesn’t, executes a block of statements, and exits. So when we pass arguments to it shows different behaviors depending on the mutability of the object referenced by the argument.
If we pass a mutable object to a function, and modify its value inside the function block, the new value of that object is accessible from outside the scope of the function. To illustrate:
l1 after passing it to our function
foo(), which appends a number to it, and then we see the updated list.
Now, if we try that with an immutable object, the modification brought to the argument inside the function will not be accessible from outside its scope
We can see that the string
s1 has been modified inside the function, but still holds the old value outside the scope of
Other tricky object magic and exceptions
Preallocated range of ints
We said that strings are immutable and that two variables with the same value point to the same object. In fact, that’s not always the case. Let’s take a look at an example:
Wait…what? In both cases
b have the same value — why aren’t they the same object in the second case? For one simple reason: when an instruction is run, there is already an allocated array of ints ranging from -5 (defined by the macro
NSMALLNEGINTS in the source code of Python) and 257 (
NSMALLPOSINTS). The most used ints in Python are in that range, so the interpreter already allocates them at the start. Creating objects in those ranges does not require an actual creation of object. That’s why all ints and characters (characters are just numbers) in that range are actually the same object and the condition
b evaluates to True. The ASCII value of the space character not being in that range, new objects are created for
b and they don’t point to the same object. Let’s see this with int values:
b have the same value, but they aren’t the same object because they are not inside the array of ints provided by Python — fascinating!
Difference between a += b and a = a + b
We could think that using the operation
a += b and
a = a + b are the same, from a programming standpoint. But under the hood, they are very different operations. The += sign is called a compound assignment operator. It both assigns and operates on a variable, so it does what we call an in place addition. Since everything in Python is an object and stems from a class, these operators are actually methods from the class of the data they are used on. the += in turn calls the method
__iadd__, and if that doesn’t work it will try to use
__add__ instead. This can lead to unexpected behavior that is implementation dependent. The + operator only calls
__add__. In both cases, depending on the object they operate on, we can see both the creation of a new object or the modification of an existing object.
Remember how we said tuples are immutable? Well, it’s true, but if they contain mutable objects we can change the value of these objects. Let’s take an example:
We successfully modified the tuple! We did this by modifying one of its mutable values, which was also updated in its alias. We need to be careful though — always make sure that tuples put in sets and dictionaries only contain immutable types.