Lab 1: Proving the “Same Address” Theory (Interning)
x = 100
y = 100
print(f"Address of x: {id(x)}")
print(f"Address of y: {id(y)}")
print(f"Are they the same object? {x is y}")
# Output: True (Because 100 is small and cached)
a = 9999
b = 9999
print(f"Are big numbers same? {a is b}")
# Output: False (Usually, unless your editor optimizes it!)
Lab 2: Watching the Reference Count
import sys
message = "DevSecOps"
# The count is 2 (one for variable 'message', one for the argument passed to getrefcount)
print(f"References: {sys.getrefcount(message)}")
copy_msg = message
print(f"References after copy: {sys.getrefcount(message)}")
# Output: Increases by 1
Lab 3: The “Mutable vs. Immutable” Identity Test
Scenario: You are writing a script that updates a server configuration. You need to know: if I update a variable, does it change the memory address?
- Immutable (Integers, Strings, Tuples): Changing the value forces Python to create a new object.
- Mutable (Lists, Dictionaries): Changing the content keeps the same object (same address).
print("--- Immutable Test (Integer) ---")
x = 100
print(f"Original x: {x}, Address: {id(x)}")
x = x + 1 # We modify x
print(f"New x: {x}, Address: {id(x)}")
# Result: The Address CHANGES! The old '100' is abandoned.
print("\n--- Mutable Test (List) ---")
servers = ["Web-01", "Web-02"]
print(f"Original List: {servers}, Address: {id(servers)}")
servers.append("Web-03") # We modify the list in-place
print(f"New List: {servers}, Address: {id(servers)}")
# Result: The Address STAYS THE SAME! Efficient memory usage.
- Architect Insight: This is why appending to a list is fast (cheap), but concatenating huge strings (
s = s + "new") is slow (expensive), because string concatenation constantly creates new objects in memory.
Lab 4: The “Shallow Copy” Danger Zone
Scenario: You want to copy a list of firewall rules to a “Backup” variable before applying changes. You use the standard slicing method [:]. The Trap: This creates a Shallow Copy. The outer list is new, but the items inside still point to the same memory addresses as the original!
import copy
# A list containing a sub-list (Nested Data)
# Think of this as [Rule-Group-ID, [Port-List]]
firewall_rules = [101, [80, 443]]
# 1. Standard Assignment (Reference only - Danger!)
alias = firewall_rules
# 2. Shallow Copy (Slicing - Safe for top level, Unsafe for nested)
backup_shallow = firewall_rules[:]
# 3. Deep Copy (The Real Clone - Totally Safe)
backup_deep = copy.deepcopy(firewall_rules)
print(f"Original: {firewall_rules}")
# Let's Modify the Inner List (The Port List)
print("\n...Hacking the original list...")
firewall_rules[1].append(22) # Adding SSH port 22
print(f"Original: {firewall_rules}")
print(f"Alias: {alias}") # Changed (Expected)
print(f"Shallow Backup: {backup_shallow}") # CHANGED! (Trap Triggered!)
print(f"Deep Backup: {backup_deep}") # Unchanged (Safe)
- Architect Insight: If you are dealing with nested JSON data (common in AWS/Kubernetes configs), standard copies are not enough. You must use
copy.deepcopy()to avoid accidental data corruption.
Lab 5: Forcing the Garbage Collector
Scenario: Your script processes massive log files (GBs of size). You delete the variable log_data to free space, but your OS shows RAM is still full. Why? Concept: Sometimes Python is “lazy” about returning memory. You can force it.
import gc
import sys
# Create a massive list (consumes RAM)
# 10^6 integers
big_data = [i for i in range(1000000)]
print(f"References to big_data: {sys.getrefcount(big_data)}")
# Delete the variable tag
del big_data
# At this point, the list is 'unreachable', but Python might not have cleaned it yet.
# Let's force the cleaning crew to run NOW.
collected = gc.collect()
print(f"Garbage Collector: Cleaned up {collected} objects.")
print("Memory should be freed now.")
- Architect Insight: In long-running scripts (like a daemon process or a custom exporter), manually calling
gc.collect()after a heavy processing job can prevent your server from running out of RAM (OOM Kill).
Lab 6: The sys.getsizeof() Reality Check
Scenario: You are optimizing a script. You want to know which data structure uses less RAM: a List or a Tuple?
import sys
my_list = [1, 2, 3, 4, 5]
my_tuple = (1, 2, 3, 4, 5)
print(f"Size of List: {sys.getsizeof(my_list)} bytes")
print(f"Size of Tuple: {sys.getsizeof(my_tuple)} bytes")
Output (Typical):
- List: ~104 bytes
- Tuple: ~80 bytes
- Why? Lists are mutable, so Python allocates extra “buffer” memory to them so you can append items later without re-allocating everything immediately. Tuples are fixed, so they are perfectly sized.
- Takeaway: For static configuration data that never changes, always use Tuples. It saves RAM.