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)
- 🎥 Intuition (5–15 min) · Python in 100 Seconds — Fireship — The 100-second bird's-eye view of the language.
- 🎥 Deep dive (30–60 min) · Facts and myths about Python names and values — Ned Batchelder (PyCon) — The one talk that makes Python's variable model click. Life-changing.
- 🎥 Hands-on demo (10–20 min) · Python Data Types — Corey Schafer — Clear, code-along walk through int/float/str/bool/list/dict.
- 📖 Canonical article · Python Docs — Data Model (§3.1 Objects, values and types) — The authoritative 'what is an object' page. Read the first two sections.
- 📖 Book chapter / tutorial · Real Python — Variables in Python — Beginner-friendly walkthrough with mental-model diagrams.
- 📖 Engineering blog · Instagram Engineering — Static Analysis at Scale — How Instagram uses type hints on millions of lines of Python.
(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:
| Type | Example | Mutable? | Use when |
|---|---|---|---|
int | 42 | No | Counts, indices. |
float | 3.14 | No | Measurements; NEVER money (use decimal.Decimal). |
bool | True / False | No | Flags; bool is a subclass of int — True == 1. |
str | "hi" | No | Text. |
list | [1, 2, 3] | Yes | Ordered, growable collection. |
tuple | (1, 2, 3) | No | Fixed record: e.g. (lat, lon). |
dict | {"a": 1} | Yes | Key→value lookup, O(1). |
set | {1, 2, 3} | Yes | Unique membership tests, O(1). |
None | None | — | The 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]) # 0Checklist — what to observe:
- Step 1:
id(x)changes when you rebind, confirming the label moved to a new object. - Step 2: mutating through
bis visible througha— aliasing. - Step 4:
TypeError: 'tuple' object does not support item assignment. - Step 6: the first
addcall proves the default list is shared across calls. - 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 viamypy/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
frozensetunless mutation is genuinely needed. It removes an entire class of "who mutated my dict" bugs. is Nonenot== None. The linter enforces it;isis 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. listappend is O(1) amortised;list.insert(0, x)is O(n) — usecollections.dequefor a queue.dictandsetlookup 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
- Explain in one sentence what happens in memory when you write
a = [1, 2, 3]; b = a. - Which of these are mutable and which are immutable:
int,list,tuple,dict,str,set,frozenset? - Why is
def f(items=[])a bug, and what's the correct pattern? - What's the difference between
isand==, and when should you use each? - Why is
0.1 + 0.2 == 0.3False, and what type should you use for money instead offloat?
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:
- What is Python Variables & Types? (one sentence, no jargon)
- When would you use it? (one concrete example)
- When would you NOT use it? (one anti-pattern)
What comes next
- Next session (S006): Control Flow — if/else, loops, comprehensions
- Previous (S004): Reading Docs & Effective Googling — the Meta-Skill
- Hub: The 6-Month Learning Plan
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 →