Python Memory Management Lab

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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top