Search Tech Journey

Find topics, journeys and posts

6-month learning plan5 / 130
back to blog
pythonbeginner 15m read

S005 · Python Variables & Types — Mental Model of Memory

What actually happens when you write x = 5.

Module M01: Python Foundations · Session 5 of 130 · Track: SW 🧠

What you'll be able to do after this session

  • Explain Python Variables & Types to a friend in one minute, out loud, no notes.
  • Recognise it when you see it in real code / systems / papers.
  • Complete the hands-on exercise at the end without looking up help.

Prerequisites

If you can't answer these in 30 seconds each, redo the linked session first:


Pre-read (skim before the session)


(a) Intuition · 5 min

The one-paragraph mental model. Why this concept exists, what problem it solves, and the analogy that makes it stick.

In one sentence: What actually happens when you write x = 5.

Most people learning to code are told "a variable is a box that holds a value." That analogy is good enough for spreadsheets but wrong for Python. In Python, a variable is a sticky note with a name, and the value it points to is a separate object living somewhere in memory. When you write x = 5, Python creates an object 5 and slaps a sticky note labelled x onto it. Do y = x, and Python just puts a second sticky note y onto the same 5. There's no "copy."

Why does this matter? Because when the value is mutable (a list, dict, or your own class), two variables pointing at the same object means changing it through one name changes it through the other. This is the source of ~30 % of "why did my code do that?" moments in the first month.

Types (int, float, str, bool, list, dict, tuple, set, None) tell you what operations are legal on a value. Python is dynamically typed — the type lives on the value, not the variable — which is why you can do x = 5 and then x = "hi" and Python shrugs. It's also strongly typed"3" + 5 raises TypeError, unlike JavaScript's "3" + 5 == "35".

Gotcha to carry forever: list and dict are mutable; int, float, str, tuple, frozenset, bool, None are immutable. When you pass a mutable object to a function, the function can change it for the caller. When you assign a default def f(items=[]), that list is created once at function definition and shared across every call. Both are top-5 Python bugs.


(b) Visual walkthrough · 15 min

Diagrams, worked examples, step-by-step visuals.

Python's memory model in one picture — variables are labels on objects, not boxes:

Worked example 1 — the aliasing bug every beginner hits:

a = [1, 2, 3]
b = a         # b is another sticky note on the SAME list
b.append(4)
print(a)      # [1, 2, 3, 4]  ← surprise!

To actually copy, use b = a.copy() or b = list(a) or b = a[:]. For nested structures, use copy.deepcopy(a).

Worked example 2 — the mutable-default-argument trap:

def add(item, bag=[]):     # bag is created ONCE
    bag.append(item)
    return bag
 
print(add("apple"))        # ['apple']
print(add("pear"))         # ['apple', 'pear']  ← the same bag!

Fix: def add(item, bag=None): bag = [] if bag is None else bag.

The type cheat sheet:

TypeExampleMutable?Use when
int42NoCounts, indices.
float3.14NoMeasurements; NEVER money (use decimal.Decimal).
boolTrue / FalseNoFlags; bool is a subclass of intTrue == 1.
str"hi"NoText.
list[1, 2, 3]YesOrdered, growable collection.
tuple(1, 2, 3)NoFixed record: e.g. (lat, lon).
dict{"a": 1}YesKey→value lookup, O(1).
set{1, 2, 3}YesUnique membership tests, O(1).
NoneNoneThe absence of a value.

The is vs == trap: == asks "same value?"; is asks "same object?" (same sticky-note target). [1] == [1] is True; [1] is [1] is False. Rule: use is only for None, True, False.


(c) Hands-on · 20 min

Code you type and run. Not pseudo-code — real, runnable, copy-pasteable.

Run this in a Python REPL or as a script. Every print statement is an experiment — predict the output before running.

# s005_variables.py
from copy import deepcopy
 
# 1. Dynamic typing — the label moves, the object stays.
x = 5
print(type(x), id(x))
x = "hello"
print(type(x), id(x))           # different id: x now points elsewhere
 
# 2. Aliasing with mutable objects.
a = [1, 2, 3]
b = a
b.append(4)
print("a:", a, " b:", b)         # both changed
print("same object?", a is b)    # True
 
# 3. Real copy vs alias.
c = a.copy()
c.append(99)
print("a:", a, " c:", c)         # a unchanged
print("same object?", a is c)    # False
 
# 4. Immutability of tuples.
t = (1, 2, 3)
try:
    t[0] = 99
except TypeError as e:
    print("cannot mutate tuple:", e)
 
# 5. But a tuple of lists is a trap.
tl = ([1], [2])
tl[0].append(9)                  # ok: the tuple slot still points at the same list
print("tricky tuple:", tl)
 
# 6. The mutable-default bug.
def add(item, bag=[]):
    bag.append(item)
    return bag
print(add("apple"))               # ['apple']
print(add("pear"))                # ['apple', 'pear']  <-- BUG
 
def add_fixed(item, bag=None):
    bag = [] if bag is None else bag
    bag.append(item)
    return bag
print(add_fixed("apple"))         # ['apple']
print(add_fixed("pear"))          # ['pear']            <-- fixed
 
# 7. is vs == — the classic gotcha.
print(1000 == 1000)               # True  (values equal)
print(1000 is 1000)               # CPython may say True or False depending on version — never rely on it
print([1] == [1])                 # True
print([1] is [1])                 # False (different objects)
 
# 8. Deep copy for nested structures.
grid  = [[0]*3 for _ in range(3)]
shallow = grid.copy()
shallow[0][0] = "X"
print("shallow copy leaked:", grid[0][0])    # 'X' — surprise
deep = deepcopy(grid)
deep[1][1] = "Y"
print("deep copy safe:",     grid[1][1])     # 0

Checklist — what to observe:

  1. Step 1: id(x) changes when you rebind, confirming the label moved to a new object.
  2. Step 2: mutating through b is visible through a — aliasing.
  3. Step 4: TypeError: 'tuple' object does not support item assignment.
  4. Step 6: the first add call proves the default list is shared across calls.
  5. Step 8: shallow copy protects the outer list but shares the inner lists — deep copy protects both levels.

Try this modification: change line 7's 1000 to 5. Python caches small ints (roughly −5..256) as singletons, so 5 is 5 is reliably True. Note this as a curiosity — never use it in real code.


(d) Production reality · 10 min

How this shows up in real systems, gotchas, war stories, common bugs.

War story — the shared config dict. An early stripe.com engineer stored per-request state in config = DEFAULTS.copy(). But DEFAULTS had nested dicts, and copy() is shallow, so request A wrote to config["headers"] and every subsequent request saw it. Two hours of intermittent auth failures before someone spotted the shallow-copy bug. Rule at Stripe: always deepcopy shared state, or use immutable data structures (frozenset, tuple, types.MappingProxyType).

War story — floats are not money. A trading system computed 0.1 + 0.2 == 0.3 and got False. Rounding errors accumulated across millions of trades produced multi-thousand-dollar mismatches. Every finance codebase now uses decimal.Decimal for currency. Rule: floats for physics, decimals for accounting.

How top teams tame Python's dynamic typing:

  • Type hints everywhere. Instagram runs 40 million lines of type-annotated Python. Type hints (def add(x: int, y: int) -> int:) don't change runtime behaviour but catch bugs in CI via mypy / pyright. Session S014 covers this.
  • Dataclasses / Pydantic for records. Rather than passing a raw dict {"id": 1, "name": "x"} everywhere and hoping nobody typos "nmae", define @dataclass class User: id: int; name: str. Compiler catches typos; docs auto-generate.
  • Immutable by default. Google's Python style guide leans on tuples and frozenset unless mutation is genuinely needed. It removes an entire class of "who mutated my dict" bugs.
  • is None not == None. The linter enforces it; is is faster and semantically correct.

Performance intuition:

  • Integer math is fast; but for i in range(10_000_000) in pure Python still takes ~0.5 s. NumPy vectorises it to microseconds.
  • list append is O(1) amortised; list.insert(0, x) is O(n) — use collections.deque for a queue.
  • dict and set lookup are O(1); use them whenever you'd think "does this contain x?"
  • Strings are immutable, so s += "x" in a loop is O(n²). Use "".join(parts) — first thing every code reviewer at Google points out.

Common bugs to internalise now: mutable default arg, shallow copy of nested data, using floats for money, using list where you needed set (slow in check), and forgetting that bool is an int (sum([True, True, False]) == 2).


(e) Quiz + exercise · 10 min

  1. Explain in one sentence what happens in memory when you write a = [1, 2, 3]; b = a.
  2. Which of these are mutable and which are immutable: int, list, tuple, dict, str, set, frozenset?
  3. Why is def f(items=[]) a bug, and what's the correct pattern?
  4. What's the difference between is and ==, and when should you use each?
  5. Why is 0.1 + 0.2 == 0.3 False, and what type should you use for money instead of float?

Stretch problem (connects to S003 and forward to S008): write a Python one-liner that reads app.log from S003, counts occurrences of each log level using a dict, and prints the result sorted by count descending. Compare using dict vs collections.Counter — which is shorter, which is clearer?


Explain-out-loud test

If you can't teach these 3 things to a friend without notes, redo the session:

  1. What is Python Variables & Types? (one sentence, no jargon)
  2. When would you use it? (one concrete example)
  3. When would you NOT use it? (one anti-pattern)

What comes next


Part of a 130-session evergreen learning series. Session structure: (a) intuition · (b) visual walkthrough · (c) hands-on · (d) production reality · (e) quiz + exercise. Duration: 60 minutes.

More from M01 · Python Foundations

all modules →
  1. 006Control Flow — if/else, loops, comprehensions
  2. 007Functions — arguments, scope, closures
  3. 008Data Structures — list, tuple, dict, set (when to use what)
  4. 009Classes & Objects — the OOP Mental Model
  5. 010Inheritance, Composition & Polymorphism
  6. 011Errors, Exceptions & Debugging with pdb