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
- Variables are not boxes - They're references (Python), addresses (C), or bindings (functional languages)
- Assignment doesn't copy - It creates new references to the same object
- Mutable vs Immutable matters - Lists can change, tuples can't
- Memory is real - Understanding it prevents bugs and improves performance
- 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.
Variables are where data lives. Loops are where programs spend their time, and where production incidents start.
Next: Loops in Production →