Python Object Identity and Mutability

Learn how Python object identity works with id() and is, the difference between mutable and immutable types, and how mutability affects your programs.

8 min read

Object identity is the unique number Python assigns to every object when it is created. Mutability is the property that determines whether an object's value can change after creation. These two concepts are connected because identity comparison tells you if two names point to the same object, and mutability tells you whether changes through one name are visible through another.

The article on the Python object model established that every object has identity, type, and value. This article zooms in on identity and mutability: how to inspect them, how they interact, and where they cause surprises.

Object identity with id() and is

Every object has an identity guaranteed to be unique during its lifetime. The built-in id function returns this identity as an integer. On CPython this is the memory address of the object.

You can create two lists with identical content and see that they have different identities. A third name assigned from the first will share its identity.

pythonpython
a = [1, 2, 3]
b = [1, 2, 3]
c = a
 
print(id(a))
print(id(b))
print(id(c))

Lists a and b have different identities because they are separate objects in memory. Name c shares the identity of a because both labels point to the same list object.

texttext
4381834368
4381834432
4381834368

The is operator compares identity directly. It returns True only when both operands are the exact same object in memory.

pythonpython
print(a is b)
print(a is c)
print(a is not b)

Name c points to the same list object as a, so the identity check returns True. Names a and b point to different objects even though those objects contain equal integer values.

texttext
False
True
True

The == operator compares values, not identity. Two distinct objects with the same content are equal with the == operator but not with the is operator.

pythonpython
print(a == b)
print(a is b)

Value equality does not imply identity equality. Always use the == operator for comparing content and reserve the is operator only for checking whether two names refer to the exact same object.

texttext
True
False

The most common correct use of the is operator is comparing against None, because None is a singleton. Every occurrence of None in a program refers to the exact same object.

Mutable and immutable types

An immutable object cannot change after it is created. Any operation that looks like modification actually creates a new object. A mutable object can change in place without creating a new object.

These are the common immutable types: int, float, complex, str, bytes, tuple, frozenset, and bool. These are the common mutable types: list, dict, set, bytearray, and most user-defined class instances.

How immutability works in practice

When you concatenate a string, Python creates a new string object rather than modifying the original. You can see this by checking the identity before and after.

pythonpython
text = "hello"
print(id(text))
 
text = text + " world"
print(id(text))
print(text)

The identity changed because the concatenation created a new string object in memory. The name text was rebound to the new object, leaving the original string unreferenced.

texttext
4381503728
4381602160
hello world

The original "hello" string still exists in memory but is no longer referenced. Python may eventually reclaim it through garbage collection.

How mutability works in practice

When you append to a list, the list object changes in place. Its identity stays the same before and after the mutation.

pythonpython
items = [1, 2, 3]
print(id(items))
 
items.append(4)
print(id(items))
print(items)

The identity did not change because append modified the existing list object in place. Any other name pointing to the same list would also see the change reflected in its contents.

texttext
4381834368
4381834368
[1, 2, 3, 4]

Python's identity caching optimizations

Python caches small integers and short strings as a performance optimization. When you create these values, Python may reuse an existing object instead of allocating a new one.

pythonpython
a = 256
b = 256
print(a is b)
 
c = 257
d = 257
print(c is d)

CPython caches integers from -5 to 256. For values outside this range, Python typically creates new objects, which is why the second pair of equal integers above compares unequal with is.

texttext
True
False

This caching is an implementation detail of CPython, and other Python implementations may have different behaviour. Never use the is operator for comparing integers or strings; always use the == operator.

Short strings that look like identifiers are often interned. Strings with spaces or special characters typically are not. Again, this is an optimization, not a language guarantee.

How mutability affects function arguments

When you pass an object to a function, the parameter becomes a new name for the same object. If the object is mutable and the function modifies it, the caller sees the change through the original name.

pythonpython
def add_item(lst, item):
    lst.append(item)
 
items = [1, 2, 3]
add_item(items, 4)
print(items)

The function receives a reference to the same list object. The append call modifies that object in place, so the change is visible through every name that points to it, including the caller's.

texttext
[1, 2, 3, 4]

If the function reassigns the parameter name instead of mutating the object, only the local label moves. The caller's name is unaffected.

pythonpython
def replace_list(lst):
    lst = ["new", "list"]
 
items = [1, 2, 3]
replace_list(items)
print(items)

The reassignment inside the function moved the local name to a new list. The caller's name still points to the original.

texttext
[1, 2, 3]

For immutable objects, there is no way for a function to change the caller's data through a parameter. Any operation creates a new object, and the function would need to return it for the caller to see the change.

The mutable default argument trap

Default argument values are evaluated once, when the function is defined. If the default value is a mutable object, every call that uses the default shares the same object.

pythonpython
def add_player(name, team=[]):
    team.append(name)
    return team
 
print(add_player("Maya"))
print(add_player("Kai"))

The same list object persists across calls, since the default was created once when Python defined the function. The second call sees the name added by the first call.

texttext
['Maya']
['Maya', 'Kai']

The correct pattern uses None as the default and creates a new mutable object inside the function body on each call that needs it.

pythonpython
def add_player(name, team=None):
    if team is None:
        team = []
    team.append(name)
    return team
 
print(add_player("Maya"))
print(add_player("Kai"))

Each call now gets its own fresh list instead of sharing one across every invocation. The is None check runs every time the default is used, creating a new list only when the caller did not supply one.

texttext
['Maya']
['Kai']

Copying mutable objects

When you need an independent copy of a mutable object, assignment is not enough. Assignment only creates another name for the same object. You need to create a new object with the same content.

A shallow copy creates a new container, but the elements inside are references to the same nested objects. A deep copy creates a new container and recursively copies every nested object.

pythonpython
import copy
 
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
 
original[0][0] = 99
print("original:", original)
print("shallow: ", shallow)
print("deep:    ", deep)

The shallow copy shares nested lists with the original. Changing a nested element through the original also appears in the shallow copy.

texttext
original: [[99, 2], [3, 4]]
shallow:  [[99, 2], [3, 4]]
deep:     [[1, 2], [3, 4]]

The deep copy is fully independent because every level of nesting was duplicated. The article on copying objects in Python covers these patterns in more detail.

Mutability and dictionary keys

Dictionary keys must be hashable, which means they must be immutable and define a hash method. If you try to use a mutable type as a key, Python raises a TypeError immediately.

pythonpython
points = {(0, 0): "origin", (1, 0): "right"}
print(points[(0, 0)])
 
try:
    bad = {[1, 2]: "value"}
except TypeError as e:
    print(e)

Tuple keys work because tuples are immutable. List keys fail because lists are mutable and therefore unhashable, so Python refuses to use one as a key rather than risk a key that changes after insertion.

texttext
origin
unhashable type: 'list'

Sets have the same restriction. Set elements must be hashable, so you can put tuples in a set but not lists.

Mutability within immutable containers

A tuple is immutable, which means you cannot reassign its elements. But if a tuple contains a mutable object like a list, that list can still be modified through the reference the tuple stores.

pythonpython
data = ([1, 2, 3], "hello")
print(id(data[0]))
 
data[0].append(4)
print(id(data[0]))
print(data)

The list inside the tuple is the same object before and after the append. The tuple's immutability prevents reassigning that slot, but does not prevent modifying the object that slot points to.

texttext
4381834368
4381834368
([1, 2, 3, 4], 'hello')

Immutability only locks the container's own slots. Anything mutable stored inside those slots keeps behaving like any other mutable object you can reach through a reference.

Rune AI

Rune AI

Key Insights

  • Object identity is a unique number accessed with id(); the is operator compares identity, and == compares value.
  • Immutable objects like int, str, and tuple cannot change after creation; mutable objects like list, dict, and set can.
  • Python caches small integers and short strings as an optimization; do not rely on identity comparison for these types.
  • Mutable default arguments are evaluated once and shared across calls; use None as the default instead.
  • Shallow copy creates a new container but shares nested objects; deep copy duplicates everything recursively.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between `is` and `==` in Python?

The `is` operator compares object identity: it returns True only when both sides refer to the exact same object in memory. The `==` operator compares object values: it returns True when the objects are considered equal according to their type's equality definition. For immutable types with interning optimizations, identity and equality can coincidentally match, but you should never rely on this.

Why do mutable default arguments cause bugs in Python?

Default argument values are evaluated once at function definition time, not each time the function is called. When the default is a mutable object like a list or dictionary, every call that uses the default shares the same object. Mutations from one call persist into the next, which is almost never the intended behaviour. The safe pattern is to use None as the default and create a new mutable object inside the function body.

Conclusion

Object identity and mutability are two sides of the same coin. Identity tells you whether two names point to the same object. Mutability tells you whether that object can change. Together they explain argument passing, default values, copy behaviour, and the difference between is and ==. Mastering these concepts removes a whole category of subtle Python bugs.