Sitemap
Benjamin

Benjamin

Python — Mutable, Immutable… everything is object!

12 min readJul 22, 2025

--

Disclaimer

This article contains analogies and metaphors inspired by the Star Wars universe. These references are used solely for illustrative and educational purposes, to simplify complex programming concepts.

Note to the reader

The use of Star Wars references follows the Feynman Technique, a method that encourages understanding through simplification and storytelling.

You’ll encounter datapads, holocrons, Jedi, and more — not to geek out (well, maybe a little), but to make Python’s memory model and object behavior more approachable and memorable.

May the code be with you.

Introduction: The Invisible Force of Python

Welcome to this special module dedicated to one of Python’s most elusive, yet crucial aspects: how objects and references really work.

At first glance, Python appears simple, expressive, and forgiving. You can assign values, pass variables into functions, return data structures… all without worrying too much about how memory works. But this apparent ease hides a subtle complexity: in Python, you’re almost never working directly with the data itself — you’re manipulating references to objects.

This concept may seem trivial, even abstract. However, misunderstanding it can lead to unexpected behaviors, especially when working with lists, dictionaries, or function parameters. Have you ever tried to modify a list inside a function, expecting the changes to persist, but they didn’t? Or the reverse — you changed something you thought was local, but it altered your original object?

These mysteries all stem from one fundamental idea:

In Python, variables don’t hold values. They point to objects in memory.

Throughout this module, we’ll explore:

  • what Python really manipulates when we use variables
  • the difference between mutable and immutable objects
  • the impact of operations like +=, =, and slicing on memory
  • what “passing by reference” or “passing by value” truly means in Python

The goal isn’t just to provide definitions or memorization tricks. We’ll analyze behaviors, decode memory addresses, and observe outputs to understand what Python does under the hood. At the end of this journey, you’ll be able to:

  • confidently explain why a is b can be True or False
  • predict when a change to a list will affect its “copy”
  • understand why += on a list behaves differently than + on a list

You’ll no longer be a passive user of Python, but a wielder of its Force — mastering its most hidden mechanisms.

Let’s begin.

« id and type »

Type, ID, and Reference — The Identity of an Object

Before we dive into the more subtle behaviors of Python, we must master three fundamental tools that allow us to inspect what Python is really doing under the hood:

type(), id(), and is

In Python, you can use:

  • type() to know the nature of the object (Is it an int? A list? A string?)
  • id() to get its identity (a unique number assigned to the object in memory — this number corresponds to the memory address in CPython)
  • is to check whether two variables refer to the exact same object in memory

Let’s see this in action:

>>> a = 42
>>> b = a

>>> type(a)
<class 'int'>

>>> id(a)
139987210260624

>>> id(b)
139987210260624

>>> a is b
True

In this example:

  • a is an integer of value 42
  • b = a creates a new variable b pointing to the same object in memory as a
  • Both id(a) and id(b) return the same value: these two variables reference the same object
  • a is b is True — not because their values are equal, but because their identities are the same

Now observe what happens when we change a:

>>> a = 1000
>>> id(a)
139987210312240

>>> id(b)
139987210260624

>>> a is b
False

Even though we used a = 1000, Python created a new object in memory and updated the reference of a. The object 42 still exists and is still pointed to by b.

Summary

  • Every object in Python has a type, an identity, and a value
  • Variables are not containers but names (references) pointing to objects
  • The function id() gives us the memory identity
  • The operator is checks if two variables point to the same object, not if they hold the same value

This mechanism is essential to understand how Python handles assignments, function calls, and mutations. And it all starts here — with the identity of an object.

« Mutable objects »

Mutable Types: The Modifiable Datapads

In Python, certain objects have a particular property: they can be modified without changing their identity. These are called mutable objects.

To understand this, imagine a datapad (like the ones the Jedi use): you can add new messages, erase existing ones, or rearrange your notes. It’s still the same datapad — you didn’t replace it, you just changed its content. That’s the behavior of mutable objects in Python.

Examples of built-in mutable types:

  • list → lists
  • dict → dictionaries
  • set → sets
  • bytearray → mutable byte sequences

All these types share this common trait: when you modify them, you are altering the object itself, not creating a new one.

Demonstration

Let’s take a simple list:

>>> l1 = [1, 2, 3]
>>> l2 = l1

>>> id(l1)
140250873964416

>>> id(l2)
140250873964416

>>> l2.append(99)
>>> l1
[1, 2, 3, 99]

Let’s break this down:

  1. We create a list l1
  2. We assign l2 = l1 — this means l2 now points to the same object as l1
  3. We verify that id(l1) and id(l2) are the same — they reference the same object
  4. We use append() on l2 to modify the list
  5. The modification is also visible via l1, because it’s the same list

This is where things can go wrong if you’re not aware of how Python handles references. Many beginners expect l2 to be a copy — it’s not. It’s an alias.

The Power — and Risk — of Mutability

Mutable types are powerful because they let you build and evolve complex data structures. But they’re also dangerous if you’re not careful with how references propagate.

In particular, when you pass mutable objects to functions, they can be modified inside the function — and those changes will persist outside the function. We’ll come back to this in detail later.

Summary

  • A mutable object can be modified in place, without changing its identity
  • Lists, dictionaries, and sets are mutable
  • Assignment copies the reference, not the object
  • Mutability makes it possible to build evolving structures, but requires caution to avoid side effects

Composite Objects: Tuples, Lists, and Shared Memory

In Python, it is common to work with composite objects, i.e., objects that contain other objects.

The two most familiar composite types are:

  • list → mutable container
  • tuple → immutable container

But things are not always that simple. Even if a container is immutable (like a tuple), it may still contain mutable elements.

⚠️Immutable containers that contain mutable objects

Let’s observe the following example:

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

What’s happening here?

  • t is a tuple — it cannot be changed (we cannot reassign t[0] = X)
  • However, t[0] is a list — it can be modified
  • So by doing t[0].append(99), we are modifying the object contained in the tuple, not the tuple itself

This subtle distinction often leads to confusion:

An immutable container does not make its contents immutable.

Shared memory between containers

Things get more complex when we reuse the same mutable object in several structures.

Example:

>>> l1 = [0, 1]
>>> l2 = [l1, l1]
>>> l2
[[0, 1], [0, 1]]

>>> l1.append(99)
>>> l2
[[0, 1, 99], [0, 1, 99]]

Explanation:

  • l2 contains two references to the same list l1
  • When we modify l1, both l2[0] and l2[1] reflect the change — they are the same object

This is called aliasing, and it leads to shared memory zones. It’s extremely powerful — for efficiency and dynamic updates — but also risky if you don’t expect this behavior.

Summary

  • Composite objects (lists, tuples, etc.) can contain references to other objects
  • Tuples are immutable, but their contents may not be
  • You can end up with several variables pointing to the same internal object, even across multiple containers
  • Modifying one element may cause unexpected changes elsewhere

You must be vigilant when manipulating complex structures — especially when combining mutable and immutable types.

« Immutable objects »

Mutation vs Immutability: The Objects That Change and Those That Don’t

In Python, not all objects behave the same way when we try to modify them. Some accept changes (mutation), while others resist it completely. Understanding this distinction is crucial to managing state and memory effectively.

Get Benjamin’s stories in your inbox

Join Medium for free to get updates from this writer.

Let’s illustrate this with a simple comparison:

>>> x = [1, 2, 3]
>>> y = x
>>> x[0] = 999
>>> y
[999, 2, 3]

What just happened?

  • We created a list x, and then assigned it to y
  • Modifying x[0] also impacted y
  • Why? Because lists are mutable
  • x and y point to the same object, and that object was modified

Now let’s try with an integer:

>>> a = 42
>>> b = a
>>> a = 99
>>> b
42

This time:

  • Changing a did not affect b
  • Why? Because integers are immutable
  • The operation a = 99 does not modify the object, but creates a new object and binds a to it

Mutable vs Immutable: A Practical Perspective

Here are some fundamental distinctions:

Press enter or click to view image in full size

When you mutate a mutable object, you alter its contents in place, without changing its identity (id() remains the same). In contrast, when you “modify” an immutable object, you’re actually creating a new object and reassigning the variable to it.

Key Idea: Mutability is about the object’s behavior, not its container

You might hold an immutable object inside a mutable one — and vice versa.

Understanding what objects can mutate — and which ones can’t — helps prevent bugs, optimize memory, and predict Python’s behavior in tricky cases.

Immutable Types: Sealed Holocrons

In the vast archives of the Jedi and Sith, certain artifacts — like holocrons — are immutable. Once sealed, they cannot be altered. In Python, some objects behave exactly like these immutable holocrons: once created, their state can no longer be changed.

What is an immutable object?

An immutable object is one whose contents cannot be modified after it is created. Any “change” made to such an object will actually result in the creation of a new object in memory.

Let’s explore this in code:

>>> a = 1000
>>> b = a
>>> a += 1
>>> b
1000

What happened?

  • b = a → both variables point to the same object
  • a += 1 → Python creates a new integer object 1001 and rebinds a to it
  • b remains unchanged, still pointing to 1000

The same principle applies to strings:

>>> s = "Jedi"
>>> t = s
>>> s += " Knight"
>>> print(s)
'Jedi Knight'
>>> print(t)
'Jedi'

Common Immutable Types in Python

Here are Python’s most commonly used immutable types:

These objects are locked in time. Trying to mutate them leads to errors:

>>> t = (1, 2, 3)
>>> t[0] = 99
TypeError: 'tuple' object does not support item assignment

Why immutability matters

  • Immutability guarantees stability and predictability
  • It allows objects to be used as dictionary keys or set elements
  • It helps prevent side effects when passing variables to functions
  • It improves performance through object caching

For example, Python internally reuses small immutable objects to save memory:

>>> a = 10
>>> b = 10
>>> a is b
True

This wouldn’t be possible if integers were mutable.

Immutable ≠ Unchangeable Universe

Be cautious: even if an object is immutable, it can still contain mutable objects:

>>> t = ([1, 2], 3)
>>> t[0][0] = 99
>>> t
([99, 2], 3)

Here, the tuple t is immutable, but it contains a mutable list — which we modified.

“Why does it matter and how differently does Python treat mutable and immutable objects”

When two paths lead to the same object

In the Star Wars universe, two characters might think they’re following different paths, only to discover they’re heading toward the same destiny. In Python, this phenomenon is called aliasing.

What is aliasing?

Aliasing happens when two variables refer to the same object in memory.

This is especially important with mutable objects, because if you modify the object via one alias, the other will “see” the change too — even if it wasn’t directly modified.

Let’s look at a basic example with a list:

original_list = [1, 2, 3]
alias_list = original_list # Both point to the same list in memory
alias_list.append(4)
print(original_list) # [1, 2, 3, 4]

Even though we added 4 to alias_list, it also appears in original_list. Why? Because both variables are just names pointing to the same object.

You can visualize it like this:

original_list ──► [1, 2, 3, 4] ◄── alias_list

With immutable objects

With immutable objects like int, str, or tuple, aliasing isn’t as dangerous. Since you can’t modify these objects, any “change” actually results in the creation of a new object.

x = 42
y = x
x += 1
print(y) # 42 — not affected

Here, x += 1 doesn't modify 42; it creates a new integer (43) and binds x to it. y still points to 42.

Why it matters

Aliasing can create side effects that are hard to track, especially inside functions. For example:

def add_x(lst):
lst.append("X")

data = ["A", "B"]
add_x(data)
print(data) # ["A", "B", "X"]

The list data was modified in place by the function — this is a side effect caused by aliasing. The variable lst inside the function is just another name for data.

If you’re not careful, these implicit modifications can cause bugs in your program.

Best practices:

  • Be explicit when modifying objects: make it clear if a function will alter its arguments.
  • Use .copy() or slicing [:] to clone a list and avoid unwanted aliasing.
  • Understand which types are mutable and which are immutable.

Memory Interpretation — The Impact of += and []

In Python, understanding how operations like += and + behave depending on the mutability of the objects involved can help you avoid unexpected results — especially when aliasing is involved.

+= on mutable types

Let’s start with a list:

a = [1, 2]
b = a
a += [3]
print(a) # [1, 2, 3]
print(b) # [1, 2, 3]

Here, += modifies the list in place. It doesn't create a new list — it extends the existing one.

So, since both a and b point to the same list (aliasing), modifying a also affects b.

This is similar to saying:

a.extend([3])

+= on immutable types

Let’s now try with an integer:

x = 5
y = x
x += 1
print(x) # 6
print(y) # 5

Here, x += 1 is not in-place. Since int is immutable, Python creates a new object 6 and binds x to it. y is still bound to 5.

It’s equivalent to:

x = x + 1

o += behaves differently depending on whether the object is mutable or immutable.

The case of strings

s = "Hello"
t = s
s += " world"
print(s) # "Hello world"
print(t) # "Hello"

Just like integers, strings are immutable. So s += " world" creates a new string and binds it to s, leaving t untouched.

In summary

Press enter or click to view image in full size

Always keep in mind:

  • With mutable types, += modifies in place → affects all aliases.
  • With immutable types, += creates a new object → safe from aliasing.

« how arguments are passed to functions and what does that imply for mutable and immutable objects »

Functions: Argument Passing by the Force

Understanding what happens when you call a function in Python

In Python, when you pass arguments to a function, you are not passing the values themselves, but references to objects — a subtle yet fundamental behavior.

Reference passing

Let’s look at this example:

def add_x(value):
value += "X"

s = "A"
add_x(s)
print(s) # "A"

Why is the value of s still "A"?

Because:

  • s refers to a string → an immutable object.
  • The function add_x receives a reference to "A".
  • But since strings are immutable, value += "X" creates a new object "AX" and binds value to it, leaving s untouched.

Now try with a list (mutable)

def add_x(lst):
lst.append("X")

data = ["A", "B"]
add_x(data)
print(data) # ["A", "B", "X"]

Here, data is modified in place because:

  • data is a list (mutable),
  • The function receives a reference to the list,
  • append("X") mutates the object directly → side effect

Important takeaways

Python always passes arguments by reference. However:

  • If the object is immutable, the function can’t modify it (only rebind it internally).
  • If the object is mutable, the function can modify it in-place, which affects the original object (aliasing + side effect).

This is sometimes summarized (though incorrectly) as:

Python uses “call by object” or “call by object reference.”

In practice, it means:

The function gets the reference to the object, not a copy of the object.

Summary

Press enter or click to view image in full size

--

--