In Python, every entity is an object categorized as either mutable (modifiable in-place) or immutable (read-only after creation).
Memory Allocation & Identity: When modifying a mutable object like a list, dictionary, or set, changes occur directly in memory without altering its identity (memory address). Conversely, modifying or reassigning an immutable object such as an int, string, tuple, bytes, or frozenset() allocates a new object at a different memory address. To test if objects are hashable, see hash(); to inspect object classifications, see type() and membership testing with in and not in.
Figure 1: Memory pointer model: mutable objects update in-place at the same address; immutable objects allocate new memory.
Every entity in Python has three core attributes determining its behavior:
id(). Unique during object lifetime.int, str, list), inspected with type().# 1. Object identity, type, and value
name = "plus2net"
nums = [10, 20]
print("name id:", id(name), "| type:", type(name))
print("nums id:", id(nums), "| type:", type(nums))Terminal Output:
name id: 140301582741432 | type: <class 'str'>
nums id: 140301581527624 | type: <class 'list'>
Understanding which built-in data types are mutable or immutable helps avoid unintended mutations:
| Category | Python Data Types | In-Place Modification? | Hashable as Dict Key / Set Element? |
|---|---|---|---|
| Immutable | int, float, bool, complex |
No (reassigns memory) | Yes (implements __hash__) |
| Immutable | str, bytes |
No (read-only sequence) | Yes |
| Immutable | tuple, frozenset |
No (fixed container) | Yes (if children are hashable) |
| Mutable | list, bytearray |
Yes (appends, mutations) | No (raises TypeError: unhashable) |
| Mutable | dict, set |
Yes (insertions, deletions) | No |
Attempting item assignment on an immutable string raises a TypeError, while modifying a list succeeds:
# 2. Immutable TypeError vs. Mutable mutation
site = "plus2net.com"
try:
site[4] = "5"
except TypeError as err:
print("Immutable str error:", err)
lst = [1, 2, 3]
lst[1] = 8
print("Mutable list update:", lst)Terminal Output:
Immutable str error: 'str' object does not support item assignment
Mutable list update: [1, 8, 3]
Tracking addresses with id() reveals how Python treats modifications. Changing an item in a list keeps the same address, while reassigning a string allocates a new object:
# 3. Comparing list vs. string address changes
my_list = [1, 2, 3]
str1 = "welcome"
print("Initial list id:", id(my_list), "| str id:", id(str1))
my_list[1] = 7 # In-place update: id stays identical
str1 = "Python" # Reassignment: allocates a new object
print("Updated list id:", id(my_list), "(Same)")
print("Updated str id: ", id(str1), "(Changed)")Terminal Output:
Initial list id: 140301581527624 | str id: 140301582741432
Updated list id: 140301581527624 (Same)
Updated str id: 140302155689016 (Changed)
The += operator behaves differently depending on whether the operand is mutable or immutable:
list): Invokes __iadd__() to extend the container in-place at the exact same memory address.tuple): Constructs a new combined tuple and rebinds the variable to the new address.# 4. In-place += on list vs. tuple
l = [5, 7]
orig_l_id = id(l)
l += [9]
print("List id unchanged:", id(l) == orig_l_id)
t = (1, 2)
orig_t_id = id(t)
t += (3,)
print("Tuple id changed: ", id(t) != orig_t_id)Terminal Output:
List id unchanged: True
Tuple id changed: True
A tuple holds immutable references to its items. However, if one of those items is a mutable list, the list's contents can still be mutated in-place:
# 5. Nested immutability trap
record = ("Alex", [90, 85])
print("Initial tuple:", record)
record[1].append(100) # Modifies inner list in-place
print("Modified tuple:", record)Terminal Output:
Initial tuple: ('Alex', [90, 85])
Modified tuple: ('Alex', [90, 85, 100])
Python evaluates default parameter values once at function definition time. Using a mutable default parameter (like target=[]) shares the same container across subsequent calls. Use None as a sentinel value instead:
# 6. Mutable default parameter trap and safe fix
def bad_add(item, target=[]):
target.append(item)
return target
print("bad_add 1:", bad_add("A"))
print("bad_add 2:", bad_add("B")) # Accumulates!
def safe_add(item, target=None):
if target is None:
target = []
target.append(item)
return target
print("safe_add 1:", safe_add("A"))
print("safe_add 2:", safe_add("B"))Terminal Output:
bad_add 1: ['A']
bad_add 2: ['A', 'B']
safe_add 1: ['A']
safe_add 2: ['B']
Dictionaries and sets require keys and elements to be hashable:
hash().__hash__ because mutations would invalidate hash table lookups.# 7. Hashability & Dictionary Keys
lookup = {(10, 20): "Alpha", frozenset([1, 2]): "Beta"}
print("Immutable keys lookup:", lookup[(10, 20)])
try:
bad = {[1, 2]: "Gamma"}
except TypeError as err:
print("Mutable key error:", err)Terminal Output:
Immutable keys lookup: Alpha
Mutable key error: unhashable type: 'list'
Protecting runtime configuration dictionaries using MappingProxyType to enforce immutability:
# 8. Recipe: Read-only dict protection
from types import MappingProxyType
config = {"host": "localhost", "port": 8080}
frozen_cfg = MappingProxyType(config)
try:
frozen_cfg["port"] = 9000
except TypeError as err:
print("Protection verified:", err)Terminal Output:
Protection verified: 'mappingproxy' object does not support item assignment
Practice testing mutability rules, tracking memory addresses with id(), and avoiding argument traps in our companion Jupyter Notebook:
Explore interactive code cells, benchmark memory addresses with id(), test tuple mutability edges, and run self-validating assertions.
Run these unit checks directly in Python to verify mutability, identity stability, and hashability rules:
# 9. Unit assertions verifying mutability rules
a = [10, 20]
a_id = id(a)
a.append(30)
assert id(a) == a_id
s = "abc"
s_id = id(s)
s += "d"
assert id(s) != s_id
t = (1, [2])
t[1].append(3)
assert t == (1, [2, 3])
assert hash((1, 2)) is not None
print("All mutable vs. immutable unit assertions passed!")Terminal Output:
All mutable vs. immutable unit assertions passed!
The tuple container itself is immutable (its references cannot be altered). However, if an item inside the tuple is a mutable list, that inner list can mutate in-place.
Lists are mutable and unhashable. If a list were mutated after being stored as a key, its hash bucket position would become invalid, corrupting lookup operations.
Python uses pass-by-object-reference. Mutating a passed mutable object modifies the caller's data; reassigning the parameter rebinds only the local variable name.
Use the built-in frozenset() constructor. Unlike standard sets, a frozenset is immutable, read-only, and fully hashable.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.