BASIC Python (Beginner Level) 📂 Basic Python Programming · 4 of 15 40 min read

Python Variables & Memory — How Storage Works

Understand Python variables at the memory level. This animated tutorial explains how Python stores variables as sticky-note references to objects, what really happens when you write a = 10 then a = 20 (with visual memory diagrams and IDs), why a++ and ++a don't work in Python, the famous -5 to 256 small-integer cache with proof, and the built-in conversion functions int(), float(), str() with a full cheat sheet and hands-on practice program.

Section 01

The Story That Explains Variables in Python

Sticky Notes vs Labelled Boxes
In C, C++, and Java, a variable is a labelled box. When you write int a = 10;, the language reserves a fixed-size box in memory called a, and drops the number 10 inside. When you change a = 20;, the same box now holds 20. The box itself is the variable.

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.
💡
The Golden Sentence to Memorise

"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.


Section 02

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.

NAMESPACE a sticky note HEAP MEMORY (where objects live) 10 int object id: 140234563 refers to
1
Python creates the integer object 10 somewhere in the heap — a big pool of memory. It gets a fixed memory address, say 140234563.
2
Python creates a name binding — a sticky note labelled a — in the current namespace.
3
Python attaches the sticky note to the object. Now a refers to the integer at address 140234563.
# ─── 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.

Section 03

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

NAMESPACE a HEAP 10 id: 140234563

Step 2 — You Write a = 20

Python creates a new integer object 20 somewhere else in memory:

NAMESPACE a HEAP 10 id: 140234563 (no name → will be freed) 20 id: 140234789
1
Python creates a new int object 20 at a new address like 140234789. The old object 10 is still sitting there.
2
Python peels the sticky note off object 10 and sticks it onto object 20. The name a now refers to 20.
3
The old object 10 now has zero references. Python's garbage collector will eventually reclaim that memory. You don't have to do anything.

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!
🔑
The Two Kinds of «Change»

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.


Section 04

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

🔒
Reason 1 — Ints Are Immutable
Cannot mutate in place
a++ in C means "increment the value stored at variable a". In Python there is no "value stored at a" — a is just a sticky note pointing at an int object. You cannot mutate an int. The only option is rebind to a new int, which a = a + 1 already expresses clearly.
🔮
Reason 2 — No Silent Side Effects
Explicit > implicit
a++ is famously ambiguous in C. Does it return the value before or after incrementing? What about a = a++ + ++a? Python's philosophy — «Explicit is better than implicit» — rules out operators whose behaviour depends on where they appear.
🎯
Reason 3 — One Obvious Way
Zen of Python
Python's Zen: «There should be one — and preferably only one — obvious way to do it.» If a += 1 already means "add 1 to a", why add a second, more cryptic way? Every extra syntax rule is one more thing beginners must learn.

What Happens Under the Hood for a += 1

BEFORE: a = 5 a += 1 EXECUTES AFTER: a refers to 6 a 5 id: 140000 5 + 1 → new int Python builds a NEW object a 6 id: 140099 (NEW!) 5 Object 5 — no refs — GC'd
⚠️
The Trap of ++a

++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.


Section 05

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

SMALL-INT POOL — pre-created at Python startup, always alive -5 -4 ··· 0 1 100 ··· 255 256 a b Both point to the SAME object 100 → a is b → True

What Happens for Numbers Outside the Cache

Numbers > 256 or < -5 → each assignment creates a NEW object x 1000 id: 140100 y 1000 id: 140200 Same VALUE, DIFFERENT objects → x is y → False But x == y → True (equality still checks the value)
# ─── 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

Speed — Every Program Uses Small Numbers Constantly
Loop counters, list indices, character codes, boolean-like 0/1 — the small integers appear in every single Python program. Pre-allocating them once saves billions of object-creation calls over the life of a process.
Massive real-world win
💾
Memory — Reuse Instead of Duplicate
Every int object costs ~28 bytes in CPython. If a million variables all hold the value 1, sharing one object saves ~28 MB. Multiply that across a large application and you save serious RAM.
One object, unlimited references
🎯
Why 256 and Not 1000?
256 = 28, the byte boundary — covers every character code, every small counter, every "small" integer in practice. A larger cache would waste startup time and memory. Below zero, -5 covers common negative indices without going overboard.
Sweet spot
🔒
Implementation Detail — Do NOT Rely On It
The cache is a CPython-specific optimisation. Other Python implementations (PyPy, Jython) may cache different ranges — or nothing at all. This is why you must never use is to compare numbers. Always use ==.
CPython only
⚠️
The One Rule That Matters

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.


Section 06

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(x)
Convert to Integer
Builds a new integer from a number, a numeric string, or a boolean. Truncates floats toward zero (does not round).
float(x)
Convert to Float
Builds a new float from a number, a numeric string (with or without decimals), or a boolean. Accepts scientific notation like "1e-3".
str(x)
Convert to String
Builds a new string representation of any object — numbers, lists, dicts, custom classes. Works on anything because every object has a string form.

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

int("42") — a brand-new object is created "42" str object id: 140100 int() creates new object 42 id: 140200 (NEW!) The original string "42" is untouched. Both objects now exist.
Section 07

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()
OUTPUT
──────────────────────────────────────────────────────────── 1. Basic binding — a = 10 ──────────────────────────────────────────────────────────── a = 10 type=int id=140234563 ──────────────────────────────────────────────────────────── 2. Reassignment — a = 20 ──────────────────────────────────────────────────────────── a = 20 type=int id=140234789 Old id was: 140234563 → different from new id! ──────────────────────────────────────────────────────────── 3. a += 1 (Python's answer to a++) ──────────────────────────────────────────────────────────── a = 21 type=int id=140234821 ──────────────────────────────────────────────────────────── 4. Small-int cache (-5 to 256) ──────────────────────────────────────────────────────────── x = 100, y = 100 → x is y = True (cached) p = 1000, q = 1000 → p is q = False (not cached) BUT p == q is always True — use == for values! ──────────────────────────────────────────────────────────── 5. int() / float() / str() ──────────────────────────────────────────────────────────── input = 42 type=str id=140301100 as_int = 42 type=int id=140234563 as_flt = 42.0 type=float id=140301200 back = 42 type=str id=140301300 ──────────────────────────────────────────────────────────── 6. Counter loop — the Pythonic way ──────────────────────────────────────────────────────────── Final counter: 5

Section 08

Golden Rules

🐍 Variables & Memory in Python — Non-Negotiable Rules
1
A variable is a name, not a container. It refers to an object; it is not the object itself. Every "assignment" is a re-labelling operation, not a value copy.
2
Reassignment changes the memory address for immutable types. a = 10; a = 20 creates a brand-new int object 20. id(a) changes. The number 10 is not modified — it is abandoned and garbage-collected.
3
Never use a++ or ++a. a++ is a syntax error. ++a silently does nothing (it's parsed as +(+a)). Always write a += 1.
4
Never rely on the small-int cache in your code. Yes, integers from -5 to 256 are shared. No, you should not use this fact to compare numbers with is. Always use == for value comparison.
5
Use is only for None, True, and False. These are singletons — there is always exactly one instance of each. For every other value, use ==.
6
int() truncates, it does not round. int(3.9) == 3, not 4. Use round() when you actually want rounding, and math.floor() / math.ceil() for explicit floor/ceiling.
7
User input is always a string. input() returns str no matter what the user types. Convert with int() or float() before doing arithmetic — or you'll hit TypeError.
8
Conversions produce new objects. int("42") does not modify the string — it creates a fresh int with a new memory address. This is consistent with how everything else in Python works.