Ask a room of working programmers a simple question: "What happens in memory when you write x = 5 in Python?"

Most cannot answer it. They know it "stores 5 in x", but what does that actually mean? Where is x? Where is 5? What connects them? Programmers write that line thousands of times without being able to draw what it does, and that gap is where an entire family of production bugs lives.

The Misconceptions

Here are the most common mental models of variables, each one wrong in a way that eventually costs you:

  • Boxes that hold values (wrong)
  • Names for memory locations (half wrong, at least in Python)
  • Just labels (too simple)
  • Magic that somehow works (honest but useless)

Now look at this industrial sensor code and predict its output:

# Why doesn't this work as expected? sensor_readings = [100, 102, 98, 101, 99] baseline = sensor_readings baseline[0] = 0 # Reset first reading print(sensor_readings) # Expected: [100, 102, 98, 101, 99] # Actual: [0, 102, 98, 101, 99] # WHY?!

If the "actual" line surprises you, this module is for you. If it doesn't, work through it anyway; the goal is to be able to draw why, not just recall the rule.

What Variables Actually Are

In Python: References to Objects

When you write: x = 5 Memory looks like this: ┌─────────────┐ ┌─────────────┐ │ Name: x │────▶│ Object: 5 │ │ (in namespace) │ │ (in heap) │ └─────────────┘ └─────────────┘ When you write: y = x ┌─────────────┐ │ Name: x │────▶┌─────────────┐ └─────────────┘ │ Object: 5 │ ┌─────────────┐ │ (shared!) │ │ Name: y │────▶└─────────────┘ └─────────────┘
The Key Idea: In Python, variables don't contain values. They point to objects. x = 5 means "make the name 'x' refer to the object 5". This is exactly why the sensor_readings example above corrupts data – both names point to the same list.

The Industrial Impact

This "small" misunderstanding causes real problems in production code:

# WRONG: This corrupts the original quality-tracking data def process_batch(temperatures): normalized = temperatures # This does NOT copy the data for i in range(len(normalized)): normalized[i] = (normalized[i] - 1500) / 100 return normalized daily_temps = [1520, 1510, 1525, 1518, 1522] normalized = process_batch(daily_temps) # daily_temps is now corrupted! Original data lost! # RIGHT: Actually create a copy def process_batch(temperatures): normalized = temperatures.copy() # or temperatures[:] for i in range(len(normalized)): normalized[i] = (normalized[i] - 1500) / 100 return normalized

In a plant historian pipeline, the wrong version silently destroys the raw record while appearing to work. Nothing crashes. The data is simply gone.

Variables Across Languages

Each language handles this differently, and knowing the difference matters:

C/C++: Actual Memory Addresses

int x = 5; // x IS a memory location containing 5 int* ptr = &x; // ptr contains the address of x // In industrial control systems, this matters: float sensor_value = 0; float* current_reading = &sensor_value; // Direct hardware memory mapping for real-time systems

Python: Names Bound to Objects

x = 5 # x refers to integer object 5 x = "hello" # x now refers to string object "hello" # The integer 5 still exists until garbage collected

JavaScript: The var/let/const Confusion

var x = 5; // Function-scoped, hoisted let y = 5; // Block-scoped const z = 5; // Block-scoped, can't reassign const readings = [1, 2, 3]; readings.push(4); // This works! const doesn't mean immutable

The Exercise: Understanding Memory

Exercise: Variable Detective

Part 1: Predict the Output

Without running this code, predict what each print statement will output:

# Predict the output a = [1, 2, 3] b = a c = a[:] d = list(a) b.append(4) c.append(5) d.append(6) print(f"a = {a}") # What will this print? print(f"b = {b}") # What will this print? print(f"c = {c}") # What will this print? print(f"d = {d}") # What will this print? # Now test if a and b refer to the same object print(f"a is b: {a is b}") # True or False? print(f"a is c: {a is c}") # True or False? print(f"a == c: {a == c}") # True or False?

Part 2: Fix the Function

This function has a dangerous bug. Find and fix it:

def adjust_temperatures(readings, calibration_offset=0): """Adjust temperature readings by calibration offset.""" adjusted = readings # BUG HERE! for i in range(len(adjusted)): adjusted[i] += calibration_offset return adjusted # Test case that reveals the bug: original = [100, 102, 104, 103, 101] calibrated = adjust_temperatures(original, -2) print(f"Original: {original}") # Should be unchanged! print(f"Calibrated: {calibrated}")

Industrial Variable Patterns

These are the variable patterns that matter most in industrial systems:

1. Immutable Defaults for Safety

# DANGEROUS: Mutable default argument def log_reading(value, history=[]): history.append(value) return history # SAFE: Use None and create new list def log_reading(value, history=None): if history is None: history = [] history.append(value) return history

2. Deep vs Shallow Copies for Nested Data

import copy # Sensor configuration with nested structure config = { 'sensors': ['temp1', 'temp2'], 'thresholds': {'high': 1500, 'low': 1000} } # Shallow copy - nested objects still shared! config2 = config.copy() config2['thresholds']['high'] = 1600 # Changes original! # Deep copy - completely independent config3 = copy.deepcopy(config) config3['thresholds']['high'] = 1600 # Original unchanged

3. Memory-Efficient Industrial Data

# For large sensor arrays, understand memory impact import sys # Integer caching in Python (-5 to 256) a = 100 b = 100 print(a is b) # True - same object! a = 1000 b = 1000 print(a is b) # False - different objects # For industrial data, use numpy arrays import numpy as np readings = np.array([1500] * 1000000) # Memory efficient print(f"Size: {sys.getsizeof(readings)} bytes")

The Challenge: Sensor Data Manager

Challenge: Build a Safe Sensor Data Manager

Create a class that safely manages sensor data without corruption:

class SensorDataManager: def __init__(self): self.raw_data = [] self.processed_data = [] self.alerts = [] def add_reading(self, value): """Add a new sensor reading.""" # YOUR CODE: Add to raw_data pass def process_data(self): """Process raw data without modifying it.""" # YOUR CODE: Create processed version # Don't modify raw_data! pass def get_statistics(self): """Return statistics without exposing internal data.""" # YOUR CODE: Return safe copy of statistics # Don't return direct references to internal lists! pass # Test your implementation: manager = SensorDataManager() manager.add_reading(1520) manager.add_reading(1525) stats = manager.get_statistics() stats.append(999) # This should NOT affect manager's internal data!

Key Takeaways

  1. Variables are not boxes - They're references (Python), addresses (C), or bindings (functional languages)
  2. Assignment doesn't copy - It creates new references to the same object
  3. Mutable vs Immutable matters - Lists can change, tuples can't
  4. Memory is real - Understanding it prevents bugs and improves performance
  5. Each language is different - Don't assume Python rules apply to C++
The Bottom Line: If you can't draw the memory diagram, you don't understand your variables. If you don't understand your variables, you're coding by luck, not logic.