Python Variables, Data Types, and Type Conversion: How Python Stores Data in Memory, Namespaces, Reference Counting, Type System, and Every Data Type Explained
Most Python tutorials start with x = 5 and say “x is a variable that stores the value 5.” That is technically wrong. Python variables do NOT store values. They are labels — name tags attached to objects that live in memory. Understanding this distinction is the foundation of everything in Python: why lists behave differently from integers, why is and == give different answers, and why function arguments sometimes change the original data and sometimes do not.
This post goes deeper than any tutorial you have seen. We will start with how your computer actually stores data (RAM, addresses, the heap), then build up to Python’s variable model (namespaces, references, reference counting), and finally cover every data type with real-life analogies and type conversion. By the end, you will understand Python’s memory model — not just the syntax.
Table of Contents
- How Data Lives in Your Computer
- RAM, Addresses, and the Three Properties
- What Happens When You Write x = 42
- Variables Are Labels, Not Boxes
- The Box Analogy Is Wrong
- The Label Analogy Is Right
- Proving It with id()
- Namespaces: Where Python Stores Variable Names
- What a Namespace Actually Stores
- Global vs Local Namespaces
- Local Variable Tables in Functions
- Reference Counting and Garbage Collection
- What del Actually Does
- Python Data Types Overview
- Everything Is an Object
- Mutable vs Immutable Types
- Numeric Types
- int (Integers)
- float (Floating Point)
- complex (Complex Numbers)
- bool (Boolean)
- Text Type
- str (Strings)
- Sequence Types
- list (Mutable Sequences)
- tuple (Immutable Sequences)
- range (Numeric Sequences)
- Mapping Type
- dict (Dictionaries)
- Set Types
- set (Mutable Unique Collection)
- frozenset (Immutable Set)
- None Type
- Type Checking
- type() vs isinstance()
- Type Conversion (Casting)
- Implicit Conversion (Coercion)
- Explicit Conversion
- Conversion Gotchas
- Integer Interning and String Interning
- Common Mistakes
- Interview Questions
- Wrapping Up
How Data Lives in Your Computer
RAM, Addresses, and the Three Properties
Before understanding Python variables, you need to understand where data actually lives. When your Python program runs, it becomes a process — the operating system allocates a chunk of RAM (Random Access Memory) for it. Every piece of data your program uses lives somewhere in this RAM.
Your computer's memory (RAM) is like a massive hotel:
- Each room has a ROOM NUMBER (memory address)
- Each room has CONTENTS (the actual value)
- Each room has a TYPE tag on the door (what kind of data is inside)
┌──────────────┬────────────────┬─────────────┐
│ Address │ Value │ Type │
├──────────────┼────────────────┼─────────────┤
│ 0x7F8B001 │ 42 │ int │
│ 0x7F8B002 │ 3.14 │ float │
│ 0x7F8B003 │ "hello" │ str │
│ 0x7F8B004 │ [1, 2, 3] │ list │
└──────────────┴────────────────┴─────────────┘
Every piece of data in memory has THREE properties:
1. ADDRESS — where it lives (the room number)
2. VALUE — what it contains (the contents)
3. TYPE — what kind of data it is (the door tag)
What Happens When You Write x = 42
Step 1: Python creates an integer OBJECT on the heap (a region of RAM)
This object is a C struct containing:
- Reference count: 1
- Type pointer: points to the int class
- Value: 42
Step 2: Python adds the name "x" to the current NAMESPACE
The namespace is a dictionary: {"x": address_of_int_object}
Step 3: The name "x" now POINTS TO (references) the integer object
NAMESPACE (dict) HEAP (RAM)
┌──────────────┐ ┌─────────────────────┐
│ "x" ─────────┼───────────▶│ int object │
└──────────────┘ │ refcount: 1 │
│ type: class 'int' │
│ value: 42 │
│ addr: 0x7F8B... │
└─────────────────────┘
The variable "x" does NOT contain 42.
The variable "x" contains the ADDRESS of an object that contains 42.
x = 42
print(id(x)) # e.g., 140234866221360 — the memory address of the int object
print(type(x)) # <class 'int'> — the type of the object x points to
print(x) # 42 — the value of the object x points to
Variables Are Labels, Not Boxes
The Box Analogy Is Wrong
WRONG mental model (the "box" analogy):
x = 42 → x is a box containing 42
y = x → y is a new box, copy of 42 put inside
y = 99 → y box now contains 99, x box still has 42
This model BREAKS with lists:
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4] ← Wait, I only changed b!
The Label Analogy Is Right
CORRECT mental model (the "label" analogy):
a = [1, 2, 3] → Create list object, stick label "a" on it
b = a → Stick label "b" on the SAME list object
b.append(4) → Modify the list object (both labels still point to it)
print(a) → [1, 2, 3, 4] — correct! Both labels, same object
Proving It with id()
a = [1, 2, 3]
b = a
print(id(a)) # 140234866221360
print(id(b)) # 140234866221360 ← SAME address! Same object!
print(a is b) # True
b.append(4)
print(a) # [1, 2, 3, 4] — both labels, same list
b = [10, 20, 30] # b now points to a DIFFERENT object
print(a is b) # False — different objects now
Namespaces: Where Python Stores Variable Names
What a Namespace Actually Stores
x = 42
name = "Naveen"
# The namespace stores name → address (not name → value)
# Python displays values for convenience:
print(globals()) # {..., 'x': 42, 'name': 'Naveen'}
# But internally: {'x': 0x7F8B001, 'name': 0x7F8B002}
Global vs Local Namespaces
x = 10 # Global namespace
def my_function():
y = 20 # Local namespace (exists only during function execution)
print(x) # Looks in local first, then global → finds x=10
my_function()
# print(y) # NameError! y does not exist in the global namespace
# LEGB rule: Local → Enclosing → Global → Built-in
Local Variable Tables in Functions
# Inside functions, CPython uses a fixed-size C array (not a dict)
# This makes function variable access FASTER than global access
def my_function(a, b):
total = a + b
return total
print(my_function.__code__.co_varnames) # ('a', 'b', 'total')
# Python knew all three names at compile time and assigned them array positions
# Accessing total = array[2] — direct index lookup, no hashing
Reference Counting and Garbage Collection
import sys
x = [1, 2, 3]
print(sys.getrefcount(x)) # 2 (x + getrefcount temporary reference)
y = x
print(sys.getrefcount(x)) # 3
del y
print(sys.getrefcount(x)) # 2
del x # refcount drops to 0 → Python immediately frees the memory
What del Actually Does
# del removes the NAME from the namespace, NOT the object
x = [1, 2, 3]
y = x
del x
print(y) # [1, 2, 3] — the list is fine, y still points to it
Python Data Types Overview
Everything Is an Object
x = 42
print(type(x)) # <class 'int'>
print(x.bit_length()) # 6 — even integers have methods!
Mutable vs Immutable Types
| Category | Immutable (cannot change) | Mutable (can change in place) |
|---|---|---|
| Numeric | int, float, complex, bool | — |
| Text | str | — |
| Sequence | tuple, range, frozenset | list |
| Mapping | — | dict |
| Set | frozenset | set |
| None | NoneType | — |
# IMMUTABLE: changing value creates a NEW object
x = 42
print(id(x)) # address A
x = 43
print(id(x)) # address B (different object!)
# MUTABLE: changing value modifies the SAME object
scores = [90, 85, 92]
print(id(scores)) # address A
scores.append(88)
print(id(scores)) # address A (same object!)
Numeric Types
int (Integers)
age = 25
population = 1_400_000_000 # Underscores for readability
big = 10 ** 100 # Python ints have UNLIMITED precision
print(10 / 3) # 3.3333 (true division → always returns float!)
print(10 // 3) # 3 (floor division → returns int)
print(10 % 3) # 1 (modulus)
print(10 ** 3) # 1000 (exponentiation)
float (Floating Point)
pi = 3.14159
print(0.1 + 0.2) # 0.30000000000000004 (NOT 0.3!)
print(0.1 + 0.2 == 0.3) # False!
# Fix with Decimal for exact arithmetic
from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3')) # True
complex (Complex Numbers)
z = 3 + 4j
print(z.real) # 3.0
print(z.imag) # 4.0
print(abs(z)) # 5.0 (magnitude)
bool (Boolean)
is_active = True
print(isinstance(True, int)) # True! (bool is a subclass of int)
print(True + True) # 2
# Falsy: False, 0, 0.0, "", [], {}, (), set(), None
# Truthy: everything else
print(bool(0)) # False
print(bool("hello")) # True
print(bool([])) # False
Text Type
str (Strings)
name = "Naveen"
# Strings are IMMUTABLE
upper_name = name.upper() # Returns NEW string, name unchanged
# f-strings (Python 3.6+)
age = 30
print(f"My name is {name} and I am {age} years old")
print(f"Pi is {3.14159:.2f}") # "Pi is 3.14"
print(f"{1000000:,}") # "1,000,000"
# Common methods
print(" hello ".strip()) # "hello"
print("hello world".split()) # ['hello', 'world']
print(", ".join(["a", "b", "c"])) # "a, b, c"
print("42".isdigit()) # True
Sequence Types
list (Mutable Sequences)
fruits = ["apple", "banana", "cherry"]
fruits.append("date")
fruits.insert(1, "blueberry")
fruits.remove("banana")
fruits.sort()
# copy vs reference
a = [1, 2, 3]
b = a # Same object (reference)
c = a.copy() # New object (shallow copy)
a.append(4)
print(b) # [1, 2, 3, 4] — same object as a
print(c) # [1, 2, 3] — separate copy
tuple (Immutable Sequences)
coordinates = (10.5, 20.3)
single = (42,) # Single-element tuple NEEDS the comma!
not_tuple = (42) # Just 42 in parentheses, NOT a tuple
# Tuple unpacking
x, y = coordinates # x=10.5, y=20.3
a, b = 1, 2
a, b = b, a # Swap — no temp variable needed!
range (Numeric Sequences)
# range is memory efficient — does NOT store values, generates on demand
r = range(1_000_000_000) # Uses almost NO memory!
print(list(range(5))) # [0, 1, 2, 3, 4]
Mapping Type
dict (Dictionaries)
student = {"name": "Naveen", "age": 30, "scores": [90, 85]}
print(student["name"]) # "Naveen"
print(student.get("email", "N/A")) # "N/A" (no KeyError)
student["email"] = "n@email.com" # Add key
del student["scores"] # Remove key
for key, value in student.items():
print(f"{key}: {value}")
# Keys must be immutable (hashable): str, int, tuple — NOT list
Set Types
set (Mutable Unique Collection)
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b) # {1, 2, 3, 4, 5, 6} — union
print(a & b) # {3, 4} — intersection
print(a - b) # {1, 2} — difference
# Deduplication
emails = ["a@x.com", "b@x.com", "a@x.com"]
unique = list(set(emails)) # ['a@x.com', 'b@x.com']
frozenset (Immutable Set)
fs = frozenset([1, 2, 3])
# fs.add(4) # AttributeError — cannot modify
# frozenset CAN be a dict key or set member (set cannot)
None Type
result = None
a = None
b = None
print(a is b) # True — there is only ONE None object (singleton)
# Always use "is" for None comparison, not "=="
if result is None:
print("No result")
Type Checking
type() vs isinstance()
print(type(42)) # <class 'int'>
print(type(True)) # <class 'bool'>
print(isinstance(True, int)) # True (bool IS a subclass of int)
print(isinstance(42, (int, float))) # True (check multiple types)
# PREFER isinstance() — handles subclasses correctly
Type Conversion (Casting)
Implicit Conversion (Coercion)
# int + float → float (narrower type promoted)
print(10 + 3.14) # 13.14 (float)
print(True + 2) # 3 (bool → int)
# "age: " + 25 # TypeError! No implicit string conversion
Explicit Conversion
print(int(3.9)) # 3 (truncates, does NOT round!)
print(int("42")) # 42
print(float("3.14")) # 3.14
print(str(42)) # "42"
print(bool("")) # False
print(bool("hello")) # True
print(list("hello")) # ['h', 'e', 'l', 'l', 'o']
print(set([1, 2, 2])) # {1, 2}
Conversion Gotchas
# int() truncates, does NOT round
print(int(3.9)) # 3 (not 4!) — use round(3.9) for rounding
# bool("False") is True!
print(bool("False")) # True — non-empty string is truthy!
print(bool("")) # False — only EMPTY string is falsy
# int("3.14") → ValueError! Convert to float first
print(int(float("3.14"))) # 3
Integer Interning and String Interning
# Python pre-creates integers from -5 to 256
a = 42
b = 42
print(a is b) # True — SAME object (interned)
a = 1000
b = 1000
print(a is b) # May be False — different objects (not interned)
# LESSON: Never use "is" to compare VALUES. Use "=="
# "is" only reliable for None, True, False
Common Mistakes
- Thinking variables store values — variables are labels (references) pointing to objects. Two variables can point to the same object.
- Using
isinstead of==—ischecks identity (same object).==checks equality (same value). Use==for values. - Expecting
int(3.9)to round — it truncates. Useround(3.9)for rounding. - Thinking
bool("False")is False — any non-empty string is truthy. - Mutating a default list argument —
def f(items=[])shares the SAME list across calls. Usedef f(items=None). - Comparing floats with
==—0.1 + 0.2 == 0.3is False. Useround()orDecimal. - Forgetting strings are immutable —
name.upper()returns a NEW string, does not modifyname.
Interview Questions
Q: What is the difference between is and == in Python?
A: == checks value equality. is checks identity (same object in memory, same id()). a = [1, 2]; b = [1, 2] → a == b is True but a is b is False. Use is only for None.
Q: What are mutable and immutable types? A: Immutable objects cannot change after creation (int, float, str, tuple, frozenset). Mutable objects can change in place (list, dict, set). Two variables pointing to the same mutable object share changes.
Q: What is a namespace in Python? A: A dictionary mapping variable names to object references. Python has local, enclosing, global, and built-in namespaces. Lookup follows LEGB rule.
Q: What does del x do?
A: Removes the name x from the namespace and decreases the object’s reference count. The object is only destroyed when its reference count reaches zero.
Q: Why is 0.1 + 0.2 != 0.3?
A: Floats use binary representation (IEEE 754). 0.1 cannot be represented exactly in binary, causing tiny errors. Use Decimal for exact arithmetic or math.isclose() for comparisons.
Q: What is integer interning?
A: Python pre-creates integers -5 to 256 at startup and reuses them. a = 42; b = 42; a is b → True (same cached object). Larger integers may create separate objects.
Q: What is the difference between shallow copy and deep copy?
A: Shallow copy (list.copy()) creates a new list but elements reference the same objects. Deep copy (copy.deepcopy()) recursively copies all nested objects. Use deep copy for lists containing mutable objects.
Wrapping Up
Variables in Python are not boxes that store values — they are labels attached to objects in memory. This single mental model shift explains why lists behave differently from integers, why is and == give different answers, and why function arguments sometimes change the original data.
Understanding the memory model — namespaces, references, reference counting, mutability — separates someone who writes Python from someone who truly understands it. Every concept in this series builds on this foundation.
Next in this series: Strings — Slicing, f-Strings, and String Methods
Naveen Vuppula is a Senior Data Engineering Consultant and app developer based in Ontario, Canada. He writes about Python, SQL, AWS, Azure, and everything data engineering at DriveDataScience.com.