The Story That Explains Variables in Python
Python does something completely different. In Python, values (objects) live in memory on their own. A variable is just a sticky note with a name on it, stuck onto an object. When you write a = 10, Python creates the integer object 10 somewhere in memory, then slaps a sticky note labelled a onto it. The sticky note IS the variable — it points at the object, but it is not the object.
This one design choice explains almost every "weird" thing about Python: why a++ doesn't exist, why memory addresses change when you "modify" a number, why small integers behave strangely with is, and much more. Understand this model and Python's memory behaviour becomes obvious.
"A Python variable is a name that refers to an object; it is not the object itself." This one sentence is the key to everything below. Repeat it until it feels obvious.
How Python Actually Stores a Variable — Visualised
When you write a = 10, three separate things happen inside Python's memory. Follow the animation below carefully — it shows the exact sequence.
# ─── PROVE IT WITH id() ─────────────────────────────────
a = 10
print(a) # 10 — the value the sticky note refers to
print(id(a)) # 140234563 — the memory address of the object
print(type(a)) # <class 'int'> — what kind of object
# The number 10 exists IN MEMORY. The name 'a' is just a label pointing at it.
What Happens When You Do a = 20?
This is where Python's model shocks most beginners. When you write a = 20 after a = 10, the number 10 is not overwritten. Python does not go into that memory box and change its contents. Instead, it moves the sticky note to a brand-new object.
Step 1 — Before Reassignment
Step 2 — You Write a = 20
Python creates a new integer object 20 somewhere else in memory:
Proof With Real Code
# ─── WATCH THE MEMORY ADDRESS CHANGE ────────────────────
a = 10
print(f"a = {a}, id(a) = {id(a)}")
# a = 10, id(a) = 140234563
a = 20 # reassignment — NEW object!
print(f"a = {a}, id(a) = {id(a)}")
# a = 20, id(a) = 140234789 ← DIFFERENT address!
# The value 10 was NOT modified. A brand-new object 20 was created,
# and the name 'a' was rebound to it.
# ─── SAME BEHAVIOUR EVEN FOR ARITHMETIC ─────────────────
x = 100
print(id(x)) # e.g. 140000
x = x + 1 # NOT modifying 100 — creating NEW object 101
print(id(x)) # DIFFERENT id — new object!
Reassignment (moving the sticky note to a new object) always works — even for immutable types. Mutation (changing the object's internal state) works only for mutable types like list and dict. Integers, strings, and floats are immutable — the only way to "change" them is to build a new object.
Why a++ and b++ Don't Exist in Python
In C, C++, Java, and JavaScript, a++ is a beloved shortcut for "add 1 to a". Python flat-out refuses to support it. Try it and you get a SyntaxError. This is not an oversight — it is a deliberate design decision that follows directly from how Python stores variables.
# ─── THIS WILL NOT WORK ─────────────────────────────────
a = 5
# a++ → SyntaxError: invalid syntax
# ++a → NOT an increment; Python reads it as +(+a) → 5, does nothing
# a-- → SyntaxError
# ─── THE PYTHONIC WAY ───────────────────────────────────
a = a + 1 # explicit — build a new int, rebind 'a'
a += 1 # shorthand — same thing under the hood
Why? Three Reasons Rooted in Python's Design
What Happens Under the Hood for a += 1
++a does not raise an error in Python — but it also does not increment. Python reads it as +(+a): two unary plus operators. The result is just a itself, unchanged. Silent bug! If you switched from C to Python and reflexively typed ++counter in a loop, your counter would never grow. Always use counter += 1.
The -5 to 256 Rule — Small Integer Caching
Here is a puzzle every serious Python developer must understand. Try this in the Python REPL and watch your brain short-circuit:
a = 100
b = 100
print(a is b) # True ← same object?!
x = 1000
y = 1000
print(x is y) # False ← different objects?!
Why does the same code give opposite answers for 100 and 1000? Because Python pre-creates every integer from -5 to 256 when it starts up, and reuses them everywhere. This is called small integer caching (or the small-int pool).
Visualising the Small-Int Pool
What Happens for Numbers Outside the Cache
# ─── PROVE THE CACHE EXISTS ─────────────────────────────
a = 100
b = 100
print(id(a) == id(b)) # True — same object from the cache
print(a is b) # True
x = 1000
y = 1000
print(id(x) == id(y)) # False — two separate objects
print(x is y) # False
print(x == y) # True — but values are equal!
# ─── BOUNDARIES OF THE CACHE ────────────────────────────
print(-5 is -5) # True — lowest cached value
print(-6 is -6) # False — one below the cache!
print(256 is 256) # True — highest cached value
print(257 is 257) # False — one above the cache!
Why -5 to 256? The Reasoning
Use is only for None, True, and False. For every number, string, list, or dict — use ==. The small-int cache is fascinating trivia, but the correct programming rule ignores it entirely.
Type Conversion — int(), float(), str()
Because a variable is just a name attached to an object, Python cannot change an object's type in place. Instead, you use type conversion functions (also called casting) that build a new object of the desired type.
int() — Building a New Integer
# ─── FROM STRINGS (most common — user input!) ───────────
print(int("42")) # 42
print(int("-17")) # -17
print(int(" 100 ")) # 100 (whitespace stripped)
# ─── FROM FLOATS — TRUNCATES (does NOT round) ───────────
print(int(3.9)) # 3 (drops decimal — no rounding!)
print(int(-3.9)) # -3 (truncates toward zero)
print(round(3.9)) # 4 ← use round() when you want rounding
# ─── FROM BOOLEANS ──────────────────────────────────────
print(int(True)) # 1
print(int(False)) # 0
# ─── DIFFERENT NUMBER BASES ─────────────────────────────
print(int("101", 2)) # 5 — from binary
print(int("FF", 16)) # 255 — from hex
print(int("777", 8)) # 511 — from octal
# ─── DANGEROUS CONVERSIONS (will crash!) ────────────────
# int("3.14") → ValueError — can't convert decimal string directly
# int("hello") → ValueError — not a number
# int("") → ValueError — empty string
# ─── SAFE: WRAP IN float() FIRST ────────────────────────
print(int(float("3.14"))) # 3 — two-step conversion works
float() — Building a New Float
# ─── FROM STRINGS ───────────────────────────────────────
print(float("3.14")) # 3.14
print(float("-0.5")) # -0.5
print(float("1e-3")) # 0.001 — scientific notation
print(float("6.022e23")) # 6.022e+23 — Avogadro's number
# ─── FROM INTEGERS ──────────────────────────────────────
print(float(7)) # 7.0 — always has decimal point
print(float(-42)) # -42.0
# ─── SPECIAL FLOAT VALUES ───────────────────────────────
print(float("inf")) # inf — positive infinity
print(float("-inf")) # -inf — negative infinity
print(float("nan")) # nan — "not a number"
# ─── FROM BOOLEANS ──────────────────────────────────────
print(float(True)) # 1.0
print(float(False)) # 0.0
# ─── FAILS ──────────────────────────────────────────────
# float("hello") → ValueError
# float("3,14") → ValueError (comma is not a decimal separator)
str() — Building a New String
# ─── STR() WORKS ON EVERYTHING ──────────────────────────
print(str(42)) # "42"
print(str(3.14)) # "3.14"
print(str(True)) # "True" (capital T!)
print(str(None)) # "None"
print(str([1, 2, 3])) # "[1, 2, 3]"
print(str({"a": 1})) # "{'a': 1}"
# ─── ESSENTIAL FOR CONCATENATION ────────────────────────
age = 25
# msg = "I am " + age → TypeError: can only concatenate str to str
msg = "I am " + str(age) # "I am 25"
# ─── BUT F-STRINGS AUTO-CONVERT (preferred!) ────────────
msg = f"I am {age}" # "I am 25" — no str() needed!
Conversion Cheat Sheet
| From \ To | int() | float() | str() |
|---|---|---|---|
| int (e.g. 42) | no-op → 42 | 42.0 | "42" |
| float (e.g. 3.9) | 3 (truncates!) | no-op → 3.9 | "3.9" |
| str (e.g. "42") | 42 | 42.0 | no-op → "42" |
| str (e.g. "3.14") | ValueError! | 3.14 | no-op → "3.14" |
| str (e.g. "hi") | ValueError! | ValueError! | no-op → "hi" |
| bool (True/False) | 1 / 0 | 1.0 / 0.0 | "True" / "False" |
What Conversion Really Does — In Memory
Putting It All Together — Practice Program
# variable_lab.py — see every concept from this tutorial in one place
def show_binding(name, value):
"""Print a variable's value, type, and memory address."""
print(f" {name:<8} = {value:<12} type={type(value).__name__:<6} id={id(value)}")
def main():
line = "─" * 60
# ─── DEMO 1: BASIC VARIABLE BINDING ─────────────────
print(f"{line}\n 1. Basic binding — a = 10\n{line}")
a = 10
show_binding("a", a)
# ─── DEMO 2: REASSIGNMENT CHANGES THE ADDRESS ───────
print(f"\n{line}\n 2. Reassignment — a = 20\n{line}")
old_id = id(a)
a = 20
show_binding("a", a)
print(f" Old id was: {old_id} → different from new id!")
# ─── DEMO 3: THE +=1 IDIOM ──────────────────────────
print(f"\n{line}\n 3. a += 1 (Python's answer to a++)\n{line}")
a += 1
show_binding("a", a) # still a new id!
# ─── DEMO 4: SMALL-INT CACHE ────────────────────────
print(f"\n{line}\n 4. Small-int cache (-5 to 256)\n{line}")
x, y = 100, 100
print(f" x = 100, y = 100 → x is y = {x is y} (cached)")
p, q = 1000, 1000
print(f" p = 1000, q = 1000 → p is q = {p is q} (not cached)")
print(f" BUT p == q is always {p == q} — use == for values!")
# ─── DEMO 5: TYPE CONVERSIONS ───────────────────────
print(f"\n{line}\n 5. int() / float() / str()\n{line}")
user_input = "42" # imagine this from input()
show_binding("input", user_input)
show_binding("as_int", int(user_input))
show_binding("as_flt", float(user_input))
show_binding("back", str(int(user_input)))
# ─── DEMO 6: WHY a++ DOES NOT EXIST ─────────────────
print(f"\n{line}\n 6. Counter loop — the Pythonic way\n{line}")
counter = 0
for _ in range(5):
counter += 1 # NOT counter++
print(f" Final counter: {counter}")
if __name__ == "__main__":
main()