Python Objects Part I: This is an Object, That is an Object — Everything is an Object!

Brennan D Baraban
15 min readJan 11, 2019

--

Thus far I’ve written about various features of the C programming language. Today, I will shift focus to a new language— Python. Python is a powerful, versatile tool, and this versatility stems from its object-oriented nature. Everything in Python is an object. And by everything, I mean everything.

Over my next four articles, I will be exploring what exactly this means. This series is intended to provide a thorough education in how objects are used to represent values in Python — a knowledge which is foundational to learning object-oriented programming. In today’s post, I will start from the top, with a basic introduction to what objects are in Python.

OBJECT TYPE

Source: https://imgflip.com/i/2r0nfg

An object in Python is simply a thing. I know, very academic. 😅 But that’s truly all there is to it! To describe it in slightly more technical terms (and I really mean slightly), an object represents a value a variable can refer to. For instance, in the following example, the variable x refers to an object representing the value 1.

>>> x = 1

All objects in Python are represented by a generalized structure used to describe and access a particular value’s information and methods. One such piece of information is an object’s type. To determine the type of an object, you can use the built-in method of the same name, type(). Building on the above example, the type of x, which refers to the value 1, is the numeric type int:

>>> x = 1
>>> type(x)
<class 'int'>

Alternatively, if x referred to the string "hello" (strings in Python are represented with either single or double quotes), it would be an instance of the sequence type str:

>>> x = "hello"
>>> type(x)
<class 'str'>

Do not worry about the class qualifier listed with each type — I’ll revisit classes later in this series. For now, simply note that classes are the earlier-referenced generalized structure used to represent objects.

You can read more on the set of all Python built-in types here. Just remember — no matter the type of a value in Python, it is an object!

OBJECT IDENTITY

Source: https://imgflip.com/i/2r0ngl

Of course, there is more to objects than just their type — for something to be an object in the first place, it has to exist somewhere in memory. To view the memory address of a particular object, you can use the built-in method id().

>>> x = 1
>>> id(x)
10105088

The id() method returns the identity of an object, an integer memory address guaranteed to be unique for the duration of a variable’s lifetime. You may be more familiar with reading memory addresses in hexadecimal format — to achieve this, you can combine the id() and hex() methods.

>>> x = 1
>>> hex(id(x))
'0x9a3100'

Note that specific addresses may vary from machine to machine.

OBJECT COMPARISON— WITH is & ==

You can compare the memory locations of any two objects in Python with the is operator — is returns True if two variables have the same identity (ie. they refer to the same object), and False otherwise.

>>> x = 89
>>> y = 98
>>> x is y
False

In the above example, x refers to the memory location of an int object 89, while y refers to the separate location of int object 98 — thus, the two variables have different identities, and x is y is False. In more Pythonic terms, x is not y:

>>> x is not y
True

Note that the value of an assignment expression is calculated before a new object is attached to a variable. The above is equivalent to using the value of x in an expression assignment for y:

>>> x = 89
>>> y = x + 9
>>> x is y
False

All the above exemplify integer objects, but we can achieve identical behavior with strings:

>>> a = "Holberton"
>>> b = "Holberton School"
>>> a is b
False
>>> a = "Holberton"
>>> b = a + "School"
>>> a is b
False

Note that you can even compare two variables referring to objects of different types — they’re both objects, after all!

>>> x = 89
>>> y = "hello"
>>> x is y
False

Finally, you may notice that the equivalence operator == often seems to display the same behavior as is:

>>> x = 89
>>> y = 89
>>> x == y
True

Do not confuse the two for each other, however. Where is compares the memory locations of two variables, == specifically compares their values.

OBJECT INSTANTIATION — IMMUTABLE OBJECTS

Source: https://imgflip.com/i/2r1rum

When going about instantiating a new object in memory, Python does not just do so willy-nilly. Rather, in the interest of efficiency, objects are created according to a particular process.

After being instructed to create an instance of an object, Python first checks its existing memory. If an instance of that object already exists, instead of allocating memory to create a new one, it will use the pre-existing value.

For instance, take a look at the following example comparing objects with equivalent identities:

>>> x = 89
>>> y = 89
>>> x is y
True

This may be initially off-putting, but recall the above. Here, in the first line, x is assigned to refer to an instance of the int object 89. In the second line, before creating a second instance of 89, Python first checks to see if that object already exists. It does, so in lieu of allocating new memory, y is simply assigned to refer to that same instance. This can be proven by comparing the two variables’ identities:

>>> hex(id(x))
'0x9a3c00'
>>> hex(id(y))
'0x9a3c00'

To put it another way, by assigning each of x and y to the same int object, it’s as if we had assigned x to y:

>>> x = 98
>>> y = x
>>> x is y
True
>>> hex(id(x))
'0x9a3c00'
>>> hex(id(y))
'0x9a3c00'

I must clarify, however, that this process of object instantiation is not universal. Rather, it is specific to immutable objects.

In addition to type and identity, objects are further classified by mutability. Think of immutability as a synonym for unchangeable. Any changes made to the features of an immutable object change the fundamental identity of the object itself.

An analogy is colors. The color blue is solely defined by just that — its color being blue. As soon as the color blue becomes a different color, say red, it is no longer the color blue anymore — it is now the color red.

Source: https://www.vispronet.com/blog/color-spectrum-rgb-cmyk-pantones/

Integer objects, and, in fact, all numeric types in Python, are immutable types. As immutable types, Python will always check if an int object already exists in memory before creating a new one. The same is true for string objects, another immutable type:

>>> a = "Holberton"
>>> b = "Holberton"
>>> a is b
True

In the above, there is only one instance of the string object Holberton; the variables a and b both refer to it. This concept is referred to as aliasing — an object is aliased when it has two or more names (in this case, a and b)

Since the value of immutable types cannot change without a change of identity, variables referring to them refer to separate objects as soon as their values are altered:

>>> x = 89
>>> y = x
>>> x is y
True
>>> y += 1
>>> x is y
False

Again, the same is true for str types — the relevant classification is the object’s mutability, not type:

>>> a = "Holberton"
>>> b = a
>>> a is b
True
>>> b += " School"
>>> a is b
False

OBJECT INSTANTIATION — MUTABLE OBJECTS

Source: https://imgflip.com/i/2r1s5s

Python tries to avoid instantiation of multiple immutable objects, since the value of an immutable object is directly tied to its identity. Instantiation of mutable objects, however, has no regard for duplicates.

Whereas immutability can be thought of as synonymous to unchangeable, mutability can be thought of as changeable — the features of a mutable object can change over time, while retaining the identity of the original object.

An analogy is humans. Your features vary over time — your hair will be longer tomorrow than it is right now, your hometown may be different a year from now than what it is today, why, even your name could be changed at some point in your life, but at the end of the day, your identity will remain unchanged — you will still be you.

Source: https://www.popsugar.com/celebrity/Brad-Pitt-Hottest-Pictures-33024536

One mutable type in Python is lists, another sequence type in Python which can hold a variable number of objects of any type. Lists are represented by brackets [], with each element separated by a comma:

>>> list_1 = [1, 2, 3]
>>> type(list_1)
<class 'list'>

When we compare two lists of different values, we compare two separate objects:

>>> list_1 = [1, 2, 3]
>>> list_2 = [4, 5, 6]
>>> list_1 is list_2
False

Nothing special here. When we compare two lists of equivalent values, however, we continue to compare two different objects:

>>> list_1 = [1, 2, 3]
>>> list_2 = [1, 2, 3]
>>> list_1 == list_2
True
>>> list_1 is list_2
False

I know, this is confusing, but remember that the relevant classification to the instantiation of an object is its mutability.

The identity of a mutable object is not tied to its value. Python must account for this ahead of time. Thus, when instructed to create an instance of a mutable object such as a list, Python will not even bother looking for existing instances of the same value — it will go ahead and allocate a new instance of the object immediately.

Instantiated and with its own unique identity, a mutable object can then be altered at will, and the identity will be retained across changes. To demonstrate this, I’ll use the Python list method append(), which adds objects to the end of an existing list:

>>> list_1 = [1, 2, 3]
>>> hex(id(list_1))
'0x7efeb0087248'
>>> list_1.append(4)
>>> hex(id(list_1))
'0x7efeb0087248'

After the instantiation of the list, we can do whatever we want to it — add values, remove values, change indices; heck, we can even empty the entire list, and the object will remain the same object:

>>> list_1 = [1, 2, 3]
>>> hex(id(list_1))
'0x7efeb0087248'
>>> list_1.pop()
3
>>> hex(id(list_1))
'0x7efeb0087248'
>>> list_1[0] = "hello"
>>> hex(id(list_1))
'0x7efeb0087248'
>>> del list_1[1]
>>> hex(id(list_1))
'0x7efeb0087248'

The variable list_1 will refer to the same object for the duration of its lifetime, or until it is reassigned to a new list.

>>> list_1 = [1, 2, 3]
>>> hex(id(list_1))
'0x7efeb0087248'
>>> list_1 = [4, 5, 6]
>>> hex(id(list_1))
'0x7efeb0087fc8'

IMMUTABLE BUT POTENTIALLY CHANGING — TUPLES

So, the identity of immutable objects in Python is directly died to its value — any alterations made to that value lose the reference to the initial object. The identity of mutable objects, on the other hand, is not tied to its value — identity will be retained across any and all changes made to its value.

When Python creates an immutable object, it first checks if that object already exists in memory before allocating a new instance. Mutable objects, on the other hand, are always instantiated immediately — each mutable object must have its own unique identity.

This behavior and instantiation process of Python objects is consistent…besides one exception. Tuples.

Tuples, like lists, are sequence types that can hold any variable number of objects of any type. They are represented in Python with parentheses (). Tuples can be empty:

>>> t1 = ()
>>> type(t1)
<class 'tuple'>

Or can contain any variable number of comma-separated objects of any type:

>>> t1 = (1, 2)
>>> type(t1)
<class 'tuple'>

Commas are mandatory for non-empty tuples. Parentheses around a single value are meaningless:

>>> t1 = (1)
>>> type(t1)
<class 'int'>
>>> t1 = (1, )
>>> type(t1)
<class 'tuple'>

Unlike lists, tuples are immutable. In most cases, Python will not let you make changes to the values of a tuple:

>>> t = (1, 2)
>>> t[0] += 1
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment

Check this out, however. Say we have a tuple, an immutable object, that contains a list, a mutable object. Now, let’s try and make a change to that list within the tuple.

>>> t = (1, [2, 3])
>>> t[1].append(4)
>>> t
(1, [2, 3, 4])

Pretty nifty! Note that the identity of the tuple did not change:

>>> t = (1, [2, 3])
>>> hex(id(t))
'0x7fc8c2fbdb08'
>>> t[1].append(4)
>>> hex(id(t))
'0x7fc8c2fbdb08'

But, of course it didn’t — tuples are immutable!

Although tuples are immutable, this feature, that they can contain mutable values, differentiates their instantiation behavior from that of other immutable objects such as integers and strings. Whereas duplicates of other immutable objects are not instantiated unless they don’t already exist in memory, tuples are instantiated like mutable objects — immediately. This is to account for the potential that the contents of a tuple may change if it contains mutable objects.

>>> t1 = (1, [2, 3])
>>> t2 = (1, [2, 3])
>>> t1 == t2
True
>>> t1 is t2
False

In the above, the values of the two tuples t1 and t2 are identical, but their identities differ — they are two separate objects. This way, no matter what happens to the list value within either tuple, the identity of the overall tuple will remain constant.

Behavior such as this can be confusing, but don’t worry, tuples just happen to be unique. In general, you can safely rely on the earlier-described process for thinking through how Python objects are instantiated.

OBJECTS AND FUNCTIONS

Source: https://imgflip.com/i/2r1stf

Alright, so int, str, and tuple objects are immutable, lists are mutable, Python generally tries to avoid duplicating immutable objects while always instantiating new mutable objects, and tuples are special.

So what? How relevant is the mutability of Python objects to their usage in functions?

As is turns out, very. And I’ll show you why. Take a look at the following function that increments a number by 1:

>>> def increment(n):
>>> ... n += 1

If we were to pass an integer to this function, that integer would retain its identity beyond the scope of the function:

>>> x = 1
>>> increment(x)
>>> x
1

This is because int type objects are immutable — the reference to 1 passed to n in increment becomes a new object within the scope of the function, while the variable x retains the identity of 1 beyond it. The above is equivalent to our earlier example:

>>> x = 1
>>> n = x
>>> n += 1
>>> x
1
>>> n
2

This behavior is important — changes made to immutable objects within functions do not register beyond the scope of the function.

In contrast, let’s update our example to increment list objects:

>>> def increment(n):
>>> ... n.append(4)

When we pass a list to this function, note that changes are registered beyond the scope of the function:

>>> list_1 = [1, 2, 3]
>>> increment(list_1)
>>> list_1
[1, 2, 3, 4]

Surprise surprise, this behavior occurs because list objects are mutable. Here, the variable n receives a reference to the same object referred to by list_1 outside the function. Since this object, as a mutable type, can withstand changes without becoming a new object, the appending of 4 is registered in list_1 beyond the scope of increment.

Again, this is equivalent to one of our earlier examples:

>>> list_1 = [1, 2, 3]
>>> n = list_1
>>> n.append(4)
>>> list_1
[1, 2, 3, 4]
>>> n
[1, 2, 3, 4]

To counterbalance our earlier maxim — changes made to mutable objects within functions do register beyond the scope of the function.

Combined, these two maxims become crucial to the proper implementation of a program. For instance, say you are working with a string, an immutable type, and wish to pass it to a function that appends an exclamation point. To pick up the appended exclamation point beyond the scope of the function, you’d need to specifically return the alteration:

>>> def add_exclamation(string):
>>> ... string += "!"
>>> ... return string
>>>
>>> string = "Hello Holberton"
>>> string = add_exclamation(string)
>>> string
'Hello Holberton!"

On the flip side, say you are working with a list, a mutable object, and wish to pass it to a function without permanently altering it. In such a case, you would need to make a new instance of the list with a copy. There are three quick and easy ways to do so:

>>> list_1 = [1, 2, 3]
>>> copy_1 = list_1[:] # slicing
>>> copy_2 = list(list_1) # casting
>>> copy_3 = list_1.copy() # copy() method

Each of the above creates new lists with the same values as list_1 but with unique identities:

>>> hex(id(list_1))
'0x7fc8c5588388'
>>> hex(id(copy_1))
'0x7fc8c2faef08'
>>> hex(id(copy_2))
'0x7fc8c2fbdd48'
>>> hex(id(copy_3))
'0x7fc8c5588548'

These copies could then be passed to a function and altered however you pleased without editing the original list:

>>> def extend_list(a_list):
>>> ... a_list.append(4)
>>>
>>> list_1 = [1, 2, 3]
>>> copy_1 = list_1.copy()
>>> extend_list(copy_1)
>>> list_1
[1, 2, 3]
>>> copy_1
[1, 2, 3, 4]

All in all, mutability matters. When going about writing a Python program involving variables passed to functions, it is important to note the mutability of the objects those variables are referring to.

Sometimes, you may wish to pick up the changes made to a mutable object within a function — in such a case, return the changed object so that you can receive it beyond the function.

Other times, you may wish to pass an immutable object to a function without permanently altering the original object’s identity — in these scenarios, do not pass the original object, but a copy of it.

Once you get the hang of it, you’ll be throwing around Python objects left and right, to whichever variables you please. After all… everything is an object!

ASIDE & PREVIEW — SHARED VALUES

I’ve spent this article painstakingly stepping through the process of object instantiation in Python. Specifically regarding immutable objects, I stated that Python searches for pre-existing objects of the same value before allocating new instances. Reiterating our example:

>>> x = 98
>>> y = 98
>>> x is y
True

Now, what if I told you that the following…

>>> x = 400
>>> y = 400

…was false.

>>> x is y
False
Source: https://imgflip.com/i/2r0nsv

I know, this is cruel. But I promise I didn’t [entirely] mislead you. It turns out that this behavior has to do with a specific implementation feature of the CPython virtual machine, the C-based, original and most commonly-used implementation of Python. Here, Python is still checking for pre-existing objects; it’s just specifically searching for pre-existing shared objects. I’ll be delving more into this implementation feature in the next part of this series.

TL;DR:

  • Everything in Python is an object.
  • Objects are represented as classes which make up a general classification structure including type, identity, and mutability.
  • Immutable objects are unchangeable — any changes made to them changes their identity.
  • When instantiating an immutable object, Python first checks if an object of the same value already exists in memory before instantiating a new object.
  • Mutable objects are changeable — their identity will be retained across changes.
  • When instantiating a mutable object, Python does so immediately — all mutable objects must have their own unique identity.
  • Tuples are unique — immutable objects that are potentially changing.
  • Because of this, tuples are instantiated like mutable objects — immediately.
  • Mutability matters — changes made to immutable objects within functions do not register beyond the scope of the function.
  • Mutability matters — changes made to mutable objects within functions do register beyond the scope of the function.
  • Everything in Python is an object.

Object Instantiation Chart Version One:

Python Object Mutability Table:

More From the Python Objects Series:

Finally, you didn’t think I was going to get away writing this article without the Oprah meme, did you?

--

--