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.
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.
4381834368
4381834432
4381834368The is operator compares identity directly. It returns True only when both operands are the exact same object in memory.
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.
False
True
TrueThe == operator compares values, not identity. Two distinct objects with the same content are equal with the == operator but not with the is operator.
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.
True
FalseThe 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.
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.
4381503728
4381602160
hello worldThe 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.
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.
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.
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.
True
FalseThis 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.
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.
[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.
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.
[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.
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.
['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.
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.
['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.
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.
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.
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.
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.
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.
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
Key Insights
- Object identity is a unique number accessed with id(); the
isoperator 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.
Frequently Asked Questions
What is the difference between `is` and `==` in Python?
Why do mutable default arguments cause bugs in Python?
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.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.