Back to Blog
Python

Understanding Python Memory References and Object Identity

python memory references: Learn how Python variables act as references to objects, how assignment and copying work, and how to avoid common memory-related pitfalls.

pythonmemory referencesobject identityshallow copydeep copymutability
Diagram showing multiple Python variables referencing the same object in memory

In Python, every variable you create is a reference to an object in memory. This behavior, often summarized as python memory references, explains why some operations appear to share data unexpectedly and why others do not. Understanding how references work is essential for writing predictable code, especially when dealing with mutable collections.

Variables Are References, Not Containers

When you write x = [1, 2, 3], Python does not copy the list into x. Instead, it creates a list object in memory and makes x a reference to that object. The variable x holds the memory address where the list lives. This is different from languages like C where a variable directly stores a value.

a = [1, 2, 3] b = a

Here a and b both reference the same list. If you modify the list through a, b sees the change because they point to the same object. This is the core of Python's reference semantics.

How Assignment Copies References

Assignment in Python never copies the object. It copies the reference. So when you write b = a, you are not creating a new list; you are creating a new reference to the same list object.

original = [10, 20] ref = original ref.append(30) print(original) # [10, 20, 30]

This behavior is often surprising to developers coming from value-semantics languages. To actually create a separate object, you must explicitly copy it using copy.copy() or copy.deepcopy().

Identity vs. Equality: is and ==

Because variables are references, Python distinguishes between identity and equality. The == operator compares the values of two objects. The is operator compares whether two references point to the same object in memory.

x = [1, 2, 3] y = [1, 2, 3] print(x == y) # True print(x is y) # False

x == y is true because the lists contain the same elements. x is y is false because they are two distinct objects. For immutable types like integers and strings, Python sometimes reuses objects, so is may return true for equal values, but you should not rely on that. Always use == for value comparison and is only when you need to check if two variables reference the same object, such as when comparing to None.

Mutable and Immutable Objects

The reference behavior of Python becomes more nuanced when you consider mutability. Immutable objects like integers, strings, and tuples cannot be changed in place. When you perform an operation that seems to modify them, Python creates a new object and reassigns the reference.

s = "hello" s += " world"

This does not mutate the original string. It creates a new string object and updates s to reference it. The original string is left for garbage collection.

Mutable objects like lists, dictionaries, and sets can be changed in place. When you call append() or update(), the object's contents change, and all references to that object observe the change.

def add_item(lst, item): lst.append(item) my_list = [1, 2] add_item(my_list, 3) print(my_list) # [1, 2, 3]

This is a common source of bugs when a function modifies a mutable argument unintentionally. To avoid this, you can pass a copy or create a new list inside the function.

Shallow and Deep Copy

To create an independent copy of a mutable object, you have two options: shallow copy and deep copy. A shallow copy creates a new object but does not copy nested objects. The nested objects are still shared between the original and the copy.

import copy original = [[1, 2], [3, 4]] shallow = copy.copy(original) shallow[0].append(99) print(original) # [[1, 2, 99], [3, 4]]

A deep copy recursively copies all nested objects, producing a fully independent object graph.

deep = copy.deepcopy(original) deep[0].append(100) print(original) # [[1, 2, 99], [3, 4]] print(deep) # [[1, 2, 99, 100], [3, 4]]

Use shallow copy when you only need to protect the top-level structure. Use deep copy when nested objects must also be independent. Deep copy is more expensive, so avoid it when not necessary.

Memory and Performance Implications

Reference semantics have direct memory and performance consequences. Because assignment does not copy, it is cheap. Passing a large list to a function does not duplicate the data; it only passes a reference. This is efficient but also means that unintended sharing can cause memory bloat if you keep references longer than needed.

Creating many copies of large objects can consume memory quickly. For example, a deep copy of a large nested structure can be costly in both time and memory. On the other hand, keeping references to objects that are no longer needed can prevent garbage collection, leading to memory leaks in long-running applications.

Python's garbage collector handles reference counting and cycle detection, but you can help it by explicitly deleting references when they are no longer needed, especially for large data structures. Use del to remove a reference, which may allow the object to be freed if no other references exist.

Avoiding Common Reference Pitfalls

One of the most common pitfalls is using a mutable default argument in a function definition. The default value is evaluated once at function definition time, and the same object is reused for every call.

def append_to(item, lst=[]): lst.append(item) return lst print(append_to(1)) # [1] print(append_to(2)) # [1, 2]

This happens because lst references the same list object across calls. The fix is to use None as the default and create a new list inside the function.

def append_to(item, lst=None): if lst is None: lst = [] lst.append(item) return lst

Another pitfall is assuming that slicing a list creates a deep copy. Slicing list[:] creates a shallow copy. For a list of lists, the inner lists are still shared. Always use copy.deepcopy() if you need full independence.

Understanding python memory references is not just an academic exercise. It directly affects how you design functions, manage data, and debug unexpected behavior. When you see a change in one variable affect another, the cause is almost always a shared reference. By keeping the reference model in mind, you can write code that behaves predictably and avoids memory-related surprises.

python memory references: Practical Usage and Code Examples | RYUSLOG DEV