Introduction#
Here’s something interesting. In Python if you create two variables that are equal to 256, they will be assigned to the same Python object.
>>> a = 256
>>> b = 256
>>> a is b
TrueBut if you create two variables that are equal to 512, they may not share the same object.
>>> a = 512
>>> b = 512
>>> a is b
FalseIn fact, for any integer from -5 to 256, a is b will return True. But outside this range, it may return False.
Why does this happen?
Let’s look at another example.
>>> a = []
>>> b = a
>>> a.append(1)
>>> a
[1]After we append 1 to a, what would be the value of b?
>>> b
[1]It’s not an empty list anymore. How does changing one variable also change the other?
>>> a = "Python In Production"
>>> b = a
>>> a = a.replace("In", "Before")
>>> a
'Python Before Production'
>>> b
'Python In Production'And why didn’t changing a affect b this time?
Today, we will talk about the object model of Python, which will help you understand one of the most fundamental concepts of the language and avoid common pitfalls in production.
Object Model#
In Python everything is an object, and every object has three things. An identity, a type, and a value.
>>> a = 2026
>>> id(a)
1234567890
>>> hex(id(a))
'0x499602d2'
>>> type(a)
<class 'int'>The identity of an object stays the same throughout its lifetime. You can think of it as the object’s memory address. The type tells us which class the object belongs to, which determines the operations that can be performed on it and the possible values it can have.
Object Creation and Variable Assignment#
a = 2026When Python interprets a = 2026, it creates an integer object in memory with the value 2026 and binds the name a to it. Think of this assignment as slapping the label a on the object 2026.
So, it is important to understand that the variable a is not the object itself. When we call id(a) or type(a), Python evaluates the name a to retrieve the object it refers to, and then applies id() or type() to that object.
Value and Identity Comparison#
When Python interprets b = 2026, this time it creates a new integer object in memory with the value 2026 and assigns the name b to it.
>>> b = 2026
>>> a == b
True
>>> a is b
FalseThe objects that a and b are assigned to have the same value, which we check with ==. But they are two different objects, meaning that they occupy different memory locations. We use is to compare identities, which is equivalent to comparing id(a) and id(b).
>>> id(a) == id(b)
FalseObject Creation Optimizations#
Creating a Python object is expensive both in terms of compute and memory usage. Python has different strategies for optimizing this. One of them is to cache small integers. If you reference an integer with a value between -5 and 256, Python returns the same shared object.
Simple Mental Model#
But generally, these are implementation details that evolve and may change in future versions. So, for your mental model, when Python creates an object, think of it as occupying a new memory location.
Name Binding#
When we assign b = a, Python does not create a new object in memory. It just creates a new name b that is bound to the same object that a is bound to. This is called name binding or aliasing.
>>> a = 2026
>>> b = a
>>> a == b
True
>>> a is b
TrueHere, a and b have the same value, but unlike before, they also have the same identity.
Mutating Objects#
What about when we perform an operation on an object? Does Python change the value of the object in place without allocating new memory, or does it create a new one?
In order to answer this, we need to ask ourselves two questions.
First, is the object mutable or immutable? The value of a mutable object can be changed after it is created. For example, lists are mutable. The value of an immutable object cannot be changed after it is created. For example, integers and strings are immutable. So, to mutate an object in place, you need a mutable object to start with.
Second, does the operation actually mutate the object in place or create a new one? Even on mutable objects, not every operation mutates in place, though most do. The easiest way to guess this is to figure out whether the operation is returning None or an object. If it returns None, it is likely mutating the object in place. If it returns an object, it is possibly creating a new one. There are also some exceptions to this rule, but it is a good starting point.
Let’s apply everything we saw to the example from the introduction.
>>> a = []
>>> b = a
>>> a.append(1)
>>> a
[1]
>>> b
[1]In the first line, we create a new list object in memory, which is mutable, and bind the name a to it. In the second line, we bind the name b to the same list object that a is bound to. In the third line, we call the append() method on a. The append() method mutates the list in place, which we can guess from the fact that append() returns None. Because b is also bound to the same list object as a, when we check the value of b, we see that it has also changed.
>>> a = "Python In Production"
>>> b = a
>>> a = a.replace("In", "Before")
>>> a
'Python Before Production'
>>> b
'Python In Production'Strings, however, are immutable in Python, so the replace() method creates a new string object with the replaced value in memory and returns it, which we can guess from the fact that we reassign the name a to the result of a.replace(). Because b is still bound to the original string object, when we check the value of b, we see that it has not changed.
We don’t usually reason step by step about our code like we just did, because after a while it becomes second nature. Until we’re comfortable with the object model, this kind of reasoning helps us understand the behavior of our code that might otherwise seem surprising.
Recap#
In this chapter, we talked about the object model of Python. We learned that everything in Python is an object, and every object has an identity, a type, and a value. We also learned about object creation and variable assignment, value and identity comparison, object creation optimizations, name binding, and mutating objects.
Understanding the object model is crucial for writing efficient and less error-prone code in Python, especially in production environments. In the next chapter, we will dive deeper into Python data types.
