# Beyond the Label: How Python Variables Really Work with Memory

You've mastered the basics: variables are labels, not boxes. You know about `is` vs. `==`. Now, let's pull back the curtain further and see what happens when you deal with more complex data structures. This knowledge is key to avoiding some of the most common and frustrating bugs.

## 1\. Everything is an Object (And Has an ID)

In Python, *everything* is an object. Integers, strings, functions, modules, even classes themselves—they all live in memory and have three things: 1.  **Identity:** A unique, constant number (its **identity**) that acts like a memory address in CPython. You see this with the `id()` function. This number is guaranteed to be unique for the object's lifetime. 2.  **Type:** What kind of object it is (e.g., `int`, `str`, `list`). 3.  **Value:** The actual data it holds.

The `id()` is the "home address" of the object. The `is` keyword simply compares these IDs.

```python
a = [1, 2, 3]  # Python creates a list object, gives it an ID, and tags it `a`
print(id(a))    # Prints a long number, e.g., 139936863411456

b = a           # This attaches a new tag `b` to the SAME object (same ID)
print(id(b))    # Same number as above!
print(a is b)   # True, because their IDs are identical.
```

This relationship can be visualized simply:

```text
a --> [1, 2, 3] <-- b
```

Both variables are references (labels) pointing to the same single list object in memory.

---

## 2\. Assignment vs. Shallow Copy vs. Deep Copy

This is the heart of the matter. The confusion between these three operations is a classic rite of passage.

* **Assignment (**`=`): This only creates a new label (variable) for the *existing* object. No new object is created. You now have two labels pointing to the same data. Change the data through one label, and it changes for the other.
    
* **Shallow Copy:** Creates a *new* outer object, but instead of creating copies of the inner objects, it just copies the *references* to them. It's like buying a new binder (`new_list`) and putting photocopies of the old binder's table of contents inside. The chapters (the inner objects) themselves are still the same. You can create a shallow copy with `.copy()`, `list()`, or slicing `original_list[:]`.
    
* **Deep Copy:** Creates a *new* outer object and then *recursively* creates new copies of every object found within the original. It's a complete duplicate, with no connection to the original.
    

Let's see the crucial difference with a list containing another list (a nested structure):

```python
import copy

original = [1, 2, [3, 4]] # A list within a list

# Assignment
assigned = original

# Shallow Copy (using .copy() - list(original) or original[:] work the same)
shallow_copied = original.copy()

# Deep Copy
deep_copied = copy.deepcopy(original)

# Now, let's change the inner list from the original
original[2].append(5)

print("Original:", original)   # [1, 2, [3, 4, 5]]
print("Assigned:", assigned)   # [1, 2, [3, 4, 5]] (changed - same object)
print("Shallow Copy:", shallow_copied) # [1, 2, [3, 4, 5]] 😲 (changed! The inner list is shared)
print("Deep Copy:", deep_copied)      # [1, 2, [3, 4]]    (unchanged - truly independent)
```

**The Shallow Copy Surprise:** This is the key takeaway. The outer list `shallow_copied` is new, so appending to *it* wouldn't affect `original`. But the inner list `[3, 4]` is the *same object* in both lists. Modifying it through one variable affects the other.

---

## 3\. Function Arguments are "Passed by Object Reference"

This concept ties everything together. People often ask, "Is Python pass-by-reference or pass-by-value?" The most accurate answer is: **It's "pass-by-object-reference."**

When you call a function, a new *label* (the parameter name) is assigned to the same object that was passed in. The same rules apply. The key is to understand the difference between **modifying an object** and **re-binding a label**.

* **Modifying a mutable object in-place** (e.g., using `.append()`, `.update()`) will be visible outside the function.
    

```python
def append_to_list(some_list):
    some_list.append("oops") # This modifies the original object itself.
    print("Inside function:", some_list)

my_list = ["hello"]
append_to_list(my_list) # Output: Inside function: ['hello', 'oops']
print("Outside function:", my_list) # Output: Outside function: ['hello', 'oops'] 😲
```

* **Re-binding a label** inside a function (using `=`) only changes what that local label points to. It has no effect on the original outside variable.
    

```python
def reassign_list(some_list):
    some_list = ["a", "new", "list"] # Re-binds the local `some_list` label to a new object.
    print("Inside function (after re-assignment):", some_list)

my_list = ["hello"]
reassign_list(my_list) # Output: Inside function (after re-assignment): ['a', 'new', 'list']
print("Outside function:", my_list) # Output: Outside function: ['hello'] ✅ (Unaffected!)
```

---

## Your Mental Model Checklist

Before you write a line of code, ask yourself: 1.  **What is the data type?** Is it mutable (list, dict, set) or immutable (int, str, tuple)? 2.  **What operation am I performing?** Am I assigning (`=`), making a shallow copy (`.copy()`, `list()`, `[:]`), or a deep copy (`copy.deepcopy()`)? 3.  **If it's a function, what will happen inside?** Will it modify the object I pass (in-place change) or just reassign the parameter label (no outside effect)?

Understanding this moves you from writing code that *works by accident* to writing code that *works by design*. You stop fearing side effects and start controlling them.

---

*Aaron Rose is a software engineer and technology writer at* [*tech-reader.blog*](https://www.tech-reader.blog) *and the author of* [*Think Like a Genius*](https://amazon.com/author/aaron.rose)*.*
