"""
02_beginner_immutable_types.py
TOPIC: Immutable Types and Memory Behavior
LEVEL: Beginner
DURATION: 45-60 minutes
LEARNING OBJECTIVES:
1. Understand what immutability means in Python
2. Learn which types are immutable (int, float, str, tuple, frozenset, bytes)
3. Explore how immutable types behave with assignment and modification
4. Understand why immutability matters for memory and performance
5. Learn about string interning and integer caching
KEY CONCEPTS:
- Immutable objects cannot be changed after creation
- "Modification" creates new objects
- Immutable types: int, float, str, tuple, bool, frozenset, bytes, None
- String interning for memory optimization
- Integer caching for small integers (-5 to 256)
"""
# ============================================================================
# SECTION 1: What is Immutability?
# ============================================================================
if __name__ == "__main__":
print("=" * 70)
print("SECTION 1: Understanding Immutability")
print("=" * 70)
# IMMUTABLE means "cannot be changed"
# Once an immutable object is created, its VALUE cannot be modified
# Any "modification" creates a NEW object
# Let's demonstrate with integers:
x = 10
print(f"Initial: x = {x}, id(x) = {id(x)}")
# What happens when we "change" x?
original_id = id(x)
x = x + 5 # This looks like modification, but...
print(f"After x = x + 5: x = {x}, id(x) = {id(x)}")
print(f"Did the object change? {original_id == id(x)}")
print(f"No! A NEW object was created and x now references it.")
# MEMORY MODEL:
# BEFORE x = x + 5: AFTER x = x + 5:
# STACK HEAP STACK HEAP
# ┌───┐ ┌──────┐ ┌───┐ ┌──────┐
# │ x─┼───>│ 10 │ │ x─┼───>│ 15 │ (new object)
# └───┘ └──────┘ └───┘ └──────┘
# ┌──────┐
# │ 10 │ (old object, may be GC'd)
# └──────┘
# ============================================================================
# SECTION 2: Common Immutable Types in Python
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 2: Immutable Types in Python")
print("=" * 70)
# Python's immutable built-in types:
immutable_examples = {
"int": 42,
"float": 3.14,
"str": "Hello",
"tuple": (1, 2, 3),
"bool": True,
"frozenset": frozenset([1, 2, 3]),
"bytes": b"hello",
"NoneType": None,
}
print("\nDemonstrating immutable types:")
for type_name, value in immutable_examples.items():
print(f" {type_name:12} : {value} (type: {type(value).__name__})")
# ============================================================================
# SECTION 3: Integers and Immutability
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 3: Integer Immutability")
print("=" * 70)
# Integers are immutable - you cannot change an integer object's value
a = 100
b = a # b references the same object as a
print(f"Initially:")
print(f" a = {a}, id(a) = {id(a)}")
print(f" b = {b}, id(b) = {id(b)}")
print(f" a is b: {a is b}")
# Now "modify" a
a = a + 1
print(f"\nAfter a = a + 1:")
print(f" a = {a}, id(a) = {id(a)}")
print(f" b = {b}, id(b) = {id(b)}")
print(f" a is b: {a is b}")
# Key insight: b is unchanged! a now references a different object
# This is different from mutable types (we'll see this in the next file)
# ============================================================================
# SECTION 4: Integer Caching (Optimization)
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 4: Integer Caching (-5 to 256)")
print("=" * 70)
# Python pre-creates and caches integer objects from -5 to 256
# This is a performance optimization for commonly used integers
# Cached integers (same object):
x1 = 100
x2 = 100
print(f"Cached integers:")
print(f" x1 = {x1}, id(x1) = {id(x1)}")
print(f" x2 = {x2}, id(x2) = {id(x2)}")
print(f" x1 is x2: {x1 is x2}") # True! Same object
# Non-cached integers (different objects, usually):
y1 = 1000
y2 = 1000
print(f"\nNon-cached integers:")
print(f" y1 = {y1}, id(y1) = {id(y1)}")
print(f" y2 = {y2}, id(y2) = {id(y2)}")
print(f" y1 is y2: {y1 is y2}") # May be False (implementation dependent)
# WHY DOES THIS MATTER?
# - Memory efficiency: Small integers are shared
# - Performance: No need to allocate new objects repeatedly
# - BUT: Never rely on 'is' for numeric comparisons - always use '=='
print("\nIMPORTANT: Always use == for value comparison, not 'is'")
print(f" y1 == y2: {y1 == y2}") # Always True if values are equal
# ============================================================================
# SECTION 5: Strings and Immutability
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 5: String Immutability")
print("=" * 70)
# Strings are immutable sequences of characters
s = "Hello"
print(f"Original string: s = '{s}', id(s) = {id(s)}")
# Attempt to "modify" the string
original_id = id(s)
s = s + " World" # Concatenation creates a NEW string object
print(f"After concatenation: s = '{s}', id(s) = {id(s)}")
print(f"Same object? {original_id == id(s)}")
# String "methods" that seem to modify actually return new strings:
text = "python"
print(f"\nOriginal: text = '{text}', id = {id(text)}")
upper_text = text.upper()
print(f"text.upper() = '{upper_text}', id = {id(upper_text)}")
print(f"Original text unchanged: '{text}', id = {id(text)}")
# IMPORTANT: Strings cannot be modified in place
# This won't work: text[0] = 'P' # TypeError: 'str' object does not support item assignment
# ============================================================================
# SECTION 6: String Interning (Optimization)
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 6: String Interning")
print("=" * 70)
# Python "interns" some strings to save memory
# Interning means reusing the same string object for identical values
# Automatically interned: identifiers and short strings
s1 = "hello"
s2 = "hello"
print(f"Short strings:")
print(f" s1 = '{s1}', id = {id(s1)}")
print(f" s2 = '{s2}', id = {id(s2)}")
print(f" s1 is s2: {s1 is s2}") # Often True
# Strings with spaces or special characters may not be interned:
s3 = "hello world"
s4 = "hello world"
print(f"\nStrings with spaces:")
print(f" s3 = '{s3}', id = {id(s3)}")
print(f" s4 = '{s4}', id = {id(s4)}")
print(f" s3 is s4: {s3 is s4}") # May be True or False
# Explicit interning using sys.intern():
import sys
s5 = sys.intern("hello world")
s6 = sys.intern("hello world")
print(f"\nExplicitly interned strings:")
print(f" s5 = '{s5}', id = {id(s5)}")
print(f" s6 = '{s6}', id = {id(s6)}")
print(f" s5 is s6: {s5 is s6}") # Always True
# WHY STRING INTERNING MATTERS:
# - Memory savings when you have many identical strings
# - Faster string comparison (identity check instead of character comparison)
# - Useful for dictionary keys and identifiers
# ============================================================================
# SECTION 7: Tuples and Immutability
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 7: Tuple Immutability")
print("=" * 70)
# Tuples are immutable sequences
t = (1, 2, 3)
print(f"Original tuple: t = {t}, id = {id(t)}")
# Cannot modify tuple elements:
# t[0] = 10 # TypeError: 'tuple' object does not support item assignment
# "Modifying" a tuple creates a new one:
original_id = id(t)
t = t + (4, 5)
print(f"After concatenation: t = {t}, id = {id(t)}")
print(f"Same object? {original_id == id(t)}")
# ============================================================================
# SECTION 8: Nested Immutability - Important Gotcha!
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 8: Tuples with Mutable Elements (GOTCHA!)")
print("=" * 70)
# IMPORTANT: A tuple itself is immutable, but it can contain mutable objects!
tuple_with_list = (1, 2, [3, 4])
print(f"Tuple with list: {tuple_with_list}")
print(f" Tuple id: {id(tuple_with_list)}")
print(f" List id: {id(tuple_with_list[2])}")
# We CANNOT change which objects the tuple references:
# tuple_with_list[2] = [5, 6] # TypeError
# BUT we CAN modify the mutable object (list) inside the tuple:
tuple_with_list[2].append(5)
print(f"\nAfter modifying the list inside:")
print(f" Tuple: {tuple_with_list}")
print(f" Tuple id: {id(tuple_with_list)} (unchanged)")
print(f" List id: {id(tuple_with_list[2])} (unchanged)")
# MEMORY MODEL:
# The tuple still references the same list object
# The list object's contents changed, but the list object itself is the same
#
# ┌─────────────────┐
# │ Tuple (1,2,L) │ (immutable - references can't change)
# └────────┬────────┘
# │
# ├──> int: 1
# ├──> int: 2
# └──> ┌───────────┐
# │ List [3,4,5]│ (mutable - contents can change)
# └───────────┘
# ============================================================================
# SECTION 9: Immutability and Function Parameters
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 9: Immutability in Functions")
print("=" * 70)
def try_to_modify_int(x):
"""
This function tries to modify an integer parameter
"""
print(f" Inside function before: x = {x}, id = {id(x)}")
x = x + 10 # This creates a new object and rebinds local x
print(f" Inside function after: x = {x}, id = {id(x)}")
return x
def try_to_modify_string(s):
"""
This function tries to modify a string parameter
"""
print(f" Inside function before: s = '{s}', id = {id(s)}")
s = s + " World" # Creates new string, rebinds local s
print(f" Inside function after: s = '{s}', id = {id(s)}")
return s
# Test with integer
num = 5
print(f"Before function call: num = {num}, id = {id(num)}")
result = try_to_modify_int(num)
print(f"After function call: num = {num}, id = {id(num)}")
print(f"Returned value: result = {result}")
print()
# Test with string
text = "Hello"
print(f"Before function call: text = '{text}', id = {id(text)}")
result = try_to_modify_string(text)
print(f"After function call: text = '{text}', id = {id(text)}")
print(f"Returned value: result = '{result}'")
# KEY INSIGHT: Immutable objects passed to functions cannot be modified
# by the function. The function can only create new objects.
# ============================================================================
# SECTION 10: Why Immutability Matters
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 10: Benefits of Immutability")
print("=" * 70)
print("""
WHY IMMUTABILITY IS IMPORTANT:
1. THREAD SAFETY
- Immutable objects can be safely shared between threads
- No risk of concurrent modification issues
2. HASHABILITY
- Immutable objects can be used as dictionary keys
- Immutable objects can be added to sets
- Hash values remain constant
3. PREDICTABILITY
- Functions can't unexpectedly modify immutable arguments
- Easier to reason about code behavior
4. MEMORY OPTIMIZATION
- Python can reuse immutable objects (caching, interning)
- Reduces memory allocation overhead
5. SECURITY
- Prevents accidental modification of important data
- Safer for use as constants or configuration values
""")
# Demonstrating hashability:
# Immutable objects can be dictionary keys
immutable_key = (1, 2, 3) # Tuple
my_dict = {immutable_key: "value"}
print(f"Dictionary with tuple key: {my_dict}")
# Mutable objects cannot be dictionary keys:
# mutable_key = [1, 2, 3] # List
# my_dict = {mutable_key: "value"} # TypeError: unhashable type: 'list'
# ============================================================================
# SECTION 11: Performance Implications
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 11: Performance Considerations")
print("=" * 70)
import time
# String concatenation creates many intermediate objects
# This is inefficient for large-scale operations
def inefficient_string_building():
result = ""
for i in range(1000):
result = result + str(i) # Creates new string each iteration!
return result
# Better approach: use join() which creates only one final string
def efficient_string_building():
parts = []
for i in range(1000):
parts.append(str(i))
return "".join(parts)
# Measure performance
start = time.time()
s1 = inefficient_string_building()
time1 = time.time() - start
start = time.time()
s2 = efficient_string_building()
time2 = time.time() - start
print(f"Inefficient string concatenation: {time1:.4f} seconds")
print(f"Efficient join method: {time2:.4f} seconds")
print(f"Speedup: {time1/time2:.2f}x faster")
# ============================================================================
# SECTION 12: Key Takeaways
# ============================================================================
print("\n" + "=" * 70)
print("KEY TAKEAWAYS")
print("=" * 70)
print("""
1. Immutable types: int, float, str, tuple, bool, frozenset, bytes, None
2. Immutable objects cannot be changed; "modification" creates new objects
3. Python optimizes memory by caching small integers (-5 to 256)
4. Python interns some strings to save memory
5. Immutable objects are thread-safe and hashable (can be dict keys)
6. Tuples are immutable, but can contain mutable objects
7. Function parameters that are immutable cannot be modified by the function
8. String concatenation in loops is inefficient; use join() instead
9. Use == for value comparison, not 'is' (identity comparison)
10. Understanding immutability is crucial for writing correct Python code
""")
# ============================================================================
# PRACTICE EXERCISES
# ============================================================================
print("\n" + "=" * 70)
print("PRACTICE EXERCISES")
print("=" * 70)
print("""
Try these exercises to reinforce your understanding:
1. Create a function that tries to modify a tuple parameter. What happens?
2. Investigate integer caching by checking 'is' for various integer values.
Find the exact range where caching occurs.
3. Create a tuple containing a list. Try to modify the list. What happens
to the tuple's id?
4. Write two functions to concatenate 10,000 strings: one using += and
one using join(). Compare their performance.
5. Explain why immutable objects can be dictionary keys but mutable objects
cannot. Create examples demonstrating this.
6. Investigate string interning with various strings (with/without spaces,
special characters, etc.). When does interning occur?
See exercises_01_beginner.py for complete practice problems!
""")