Change a list through one name and watch a completely different name show the change too. That bug bites nearly every beginner, and it happens because Python variables are names attached to objects, not boxes holding values. This post covers naming rules, assignment, dynamic typing, id() and is vs ==, plus the integer caching quirk that surprises even experienced developers.
“Names are not the things they refer to. A variable in Python is a name that refers to an object. It is not a box that contains a value.”
Ned Batchelder, PyCon 2015
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 20 minutes
Every Python variable is a sticky note, not a box. That one sentence will save you hours of confusion down the road. Most programming tutorials teach variables as “containers that hold values”, and that mental model works until it doesn’t. Python does something different. When you write age = 28, Python creates an integer object with value 28 somewhere in memory, then attaches the name age to it. The name points to the object. The name is not the object.
Why does this matter? Because when you write b = a, you’re not copying the value into a new box. You’re sticking a second label onto the same object. Both names now point to the same thing in memory. If that object can be changed in place (like a list), changing it through one name means the other name sees the change too. This is the source of the single most common Python bug beginners hit.
Here is the everyday version. Open the contacts app on your phone. You can save the same phone number under two names, say “Mom” and “Emergency”. Two labels, one number. Tapping either one calls the same phone. Python variables work exactly like that: the name is the label in your contacts, the object is the actual phone number it points to. The label is not the number.
In the first section below you will see this memory model drawn out as a diagram, and once it clicks, everything else about Python variables follows from it.
Table of Contents
Your First Variable
A variable is created the moment you assign a value to a name. No declarations, no type keywords, no ceremony. In the example below I store my own profile, a developer named Rahul, in four variables.
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The diagram shows Python’s memory model in action. Variable names on the left are not containers, they are labels that point to objects in heap memory on the right, and the arrows are references. Notice that score and b both point to the same integer object (95), which is what you get after writing score = 95 and then b = score. Likewise x and y both point to the same object (42). Two labels, one object. This “names are references” idea is the foundation for understanding mutable versus immutable behaviour, which you will meet the moment you start working with lists.
📄 first_variable.py: creating a variable is just assignment
name = "Rahul Mahadik" age = 28 height = 5.9 is_developer = True print(name) print(age) print(height) print(is_developer) print(type(name), type(age), type(height), type(is_developer))
▶ Output
Rahul Mahadik 28 5.9 True <class 'str'> <class 'int'> <class 'float'> <class 'bool'>
What happened here: Python built four objects in memory: a string, an integer, a float, and a boolean. Then it stuck the names name, age, height, and is_developer onto them. The type() function reports what kind of object each name points to. Notice what you did not have to write: no int age = 28;, no type keyword, none of the ceremony that Java or C demands. You hand Python a value, and it works out the type for you.
Naming Rules and Conventions
Python has strict rules about what a variable name can be, and strong conventions about what it should be. It works like picking a username on any website: the site enforces hard rules (no spaces, cannot start with a digit), while the community has unwritten norms about what looks right. Breaking the rules gives you a SyntaxError. Breaking the conventions gives you a code review rejection. The example below uses two students, Niranjan and Viraj, as sample values.
📄 naming_rules.py: what Python allows and what the community expects
# VALID names
student_name = "Niranjan" # snake_case, the Python standard
_private_var = "internal" # leading underscore, convention for private
__dunder = "special" # double underscore, name mangling (advanced)
camelCase = "works but wrong" # valid but NOT Pythonic
x = 42 # single letter, OK for math and loops
student2 = "Viraj" # numbers are OK, just not at the start
# INVALID names (these would crash)
# 2nd_student = "Pravin" # SyntaxError: can't start with a number
# my-variable = "nope" # SyntaxError: hyphens are minus operator
# class = "reserved" # SyntaxError: 'class' is a keyword
# Python keywords you can NOT use as names
import keyword
print("Reserved keywords:", len(keyword.kwlist))
print(keyword.kwlist[:10])
▶ Output
Reserved keywords: 35 ['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class']
What happened here: Python has 35 reserved keywords you cannot use as variable names. Names must start with a letter or underscore, followed by letters, digits, or underscores. No hyphens, no spaces, no special characters. The convention that matters most: snake_case for everything. Not camelCase (that’s Java/JavaScript), not PascalCase (that’s reserved for class names in Python). If you write studentName instead of student_name, every Python developer who reads your code will know you came from another language.
Assignment Binds Names to Objects
This is where the “sticky note, not box” model gets real. Assignment in Python does not copy a value into a variable. It binds a name to an object.
📄 assignment.py: assignment creates references, not copies
# Step 1: Create an object, bind a name to it
score = 95
print(f"score = {score}, id = {id(score)}")
# Step 2: Bind ANOTHER name to the SAME object
backup_score = score
print(f"backup_score = {backup_score}, id = {id(backup_score)}")
# Are they the same object?
print(f"Same object? {score is backup_score}") # True!
# Step 3: Rebind score to a NEW object
score = 100
print(f"\nAfter score = 100:")
print(f"score = {score}, id = {id(score)}")
print(f"backup_score = {backup_score}, id = {id(backup_score)}")
print(f"Same object? {score is backup_score}") # False now
▶ Output
score = 95, id = 140234863259312 backup_score = 95, id = 140234863259312 Same object? True After score = 100: score = 100, id = 140234863259472 backup_score = 95, id = 140234863259312 Same object? False
What happened here: When we wrote backup_score = score, Python did NOT copy the value 95 into a new location. It pointed backup_score at the exact same int object, the same id(), the same memory address. Then when we wrote score = 100, Python created a new int object with value 100 and repointed score at it. backup_score still points to the original 95 object. The old sticky note stayed put; we just moved the score label to a new object.
Multiple Assignment Tricks
Python lets you assign multiple variables in one line. Think of a school office handing out ID cards to a whole row of students in one sweep instead of calling them up one by one. This isn’t just syntactic sugar. It’s used constantly in real code, especially when unpacking return values from functions. In the example, a developer named Pravin from Pune joins a small team.
📄 multiple_assignment.py: assigning several names at once
# Multiple assignment, different values
name, age, city = "Pravin", 26, "Pune"
print(f"{name}, {age}, {city}")
# Same value to multiple names
lead = backup = "Anvay"
print(f"lead = {lead}, backup = {backup}")
print(f"Same object? {lead is backup}")
# Swapping, the Pythonic way
x, y = 10, 20
print(f"Before swap: x={x}, y={y}")
x, y = y, x
print(f"After swap: x={x}, y={y}")
# Unpacking from a list
team = ["Aditi", "Viraj", "Niranjan"]
first, second, third = team
print(f"Team lead: {first}")
▶ Output
Pravin, 26, Pune lead = Anvay, backup = Anvay Same object? True Before swap: x=10, y=20 After swap: x=20, y=10 Team lead: Aditi
What happened here: The swap trick (x, y = y, x) is pure Python elegance. In C or Java, you need a temporary variable. Python evaluates the right side completely before binding the left side, so the swap just works. The unpacking syntax (first, second, third = team) pulls apart a list into individual names, and we’ll lean on it constantly once we get to tuples and function returns.
Dynamic Typing
In Java or C, a variable has a fixed type: int age = 28; means age can only ever hold an integer. Python doesn’t work that way. A name can point to any type of object, and you can change what it points to at any time. It’s like the “captain” title in a sports club: the word stays the same, but it refers to whoever holds the role this season, and next season it can point to someone completely different.
📄 dynamic_typing.py: names can point to any type
# The same name can refer to different types
data = 42
print(f"data = {data}, type = {type(data).__name__}")
data = "Niranjan"
print(f"data = {data}, type = {type(data).__name__}")
data = [1, 2, 3]
print(f"data = {data}, type = {type(data).__name__}")
data = True
print(f"data = {data}, type = {type(data).__name__}")
▶ Output
data = 42, type = int data = Niranjan, type = str data = [1, 2, 3], type = list data = True, type = bool
What happened here: Each assignment created a brand new object and repointed data at it. First an int, then a string, then a list, then a boolean. Python didn’t complain at any point. This is dynamic typing, types belong to objects, not to names. The upside: rapid prototyping and flexible code. The downside: bugs where you accidentally reassign a variable to the wrong type and only discover it at runtime, not compile time. That’s why Python 3.5+ added optional type hints (we’ll cover those much later).
Constants Convention
Python has no const keyword. No final. No way to make a variable truly immutable at the language level. Instead, there’s a convention: if a variable name is ALL_CAPS, treat it as a constant. Don’t reassign it. It works like a “wet paint” sign on a bench: nothing physically stops you from touching it, but everyone understands you shouldn’t.
📄 constants.py: convention, not enforcement
# Constants, ALL_CAPS by convention
MAX_RETRIES = 3
API_TIMEOUT = 30
DATABASE_URL = "postgresql://localhost:5432/mydb"
PI = 3.14159265358979
print(f"Max retries: {MAX_RETRIES}")
print(f"API timeout: {API_TIMEOUT}s")
print(f"Pi: {PI}")
# Python won't stop you from reassigning, but your team will
# MAX_RETRIES = 999 # technically works, socially unacceptable
▶ Output
Max retries: 3 API timeout: 30s Pi: 3.14159265358979
What happened here: MAX_RETRIES, API_TIMEOUT, DATABASE_URL, and PI are regular variables. Python treats them exactly the same as lowercase names. The ALL_CAPS is a signal to human readers: “don’t touch this.” Every Python developer understands this convention. Break it, and your pull request will be rejected before anyone even reviews the logic.
The id() Function and Object Identity
Every object in Python has a unique identity number, returned by id(). Think of it like an Aadhaar or passport number: two people can share the name Rahul, but if their ID numbers match, you are looking at the same person. In Python, id() is the object’s identity, effectively its memory address, and two names pointing to the same object have the same id(). In the example below, two friends named Rahul and Viraj happen to be the same age.
📄 id_function.py: two names can point to one object
rahul_age = 28
viraj_age = 28
# Your id numbers will differ from mine. What matters is whether the two match.
print(f"rahul_age: value={rahul_age}, id={id(rahul_age)}")
print(f"viraj_age: value={viraj_age}, id={id(viraj_age)}")
print(f"Same object? {rahul_age is viraj_age}")
▶ Output
rahul_age: value=28, id=140234863257200 viraj_age: value=28, id=140234863257200 Same object? True
What happened here: Both names show the same id, so rahul_age and viraj_age are not just equal, they are literally the same object in memory. (Your own id numbers will look nothing like mine, and that is fine. The exact address depends on your machine and the run. What matters is that the two match.) Surprised that two separate assignments landed on one shared object? That is integer caching, and it is worth its own section, coming up next.
is vs ==, Identity vs Equality
This distinction trips up even intermediate developers. Picture identical twins: they look exactly alike (equal), but they are still two different people (not identical in the Python sense). == checks if two objects have the same value. is checks if two names point to the exact same object in memory. The example below compares two team rosters that list the same three players, Rahul, Niranjan, and Viraj.
📄 is_vs_equals.py: same value does not mean same object
# Two lists with the same content
team_a = ["Rahul", "Niranjan", "Viraj"]
team_b = ["Rahul", "Niranjan", "Viraj"]
print(f"team_a == team_b: {team_a == team_b}") # same VALUE? Yes
print(f"team_a is team_b: {team_a is team_b}") # same OBJECT? No
print(f"id(team_a): {id(team_a)}")
print(f"id(team_b): {id(team_b)}")
# Make them point to the same object
team_c = team_a
print(f"\nteam_a is team_c: {team_a is team_c}") # same object
# The ONLY thing you should use 'is' for in practice
result = None
print(f"\nresult is None: {result is None}") # correct
print(f"result == None: {result == None}") # works but bad practice
▶ Output
team_a == team_b: True team_a is team_b: False id(team_a): 140234866019072 id(team_b): 140234866019136 team_a is team_c: True result is None: True result == None: True
What happened here: team_a and team_b contain the same names, but they’re two separate list objects in memory, with different id() values. == compares content, so it returns True. is compares identity (memory address), so it returns False. In real Python code, you should only use is for one thing: checking None. Always write if x is None, never if x == None. The is check is faster and can’t be fooled by custom __eq__ methods.
The Integer Caching Surprise
Here’s the catch that makes even experienced developers pause. Python pre-creates integer objects for the range -5 to 256 and reuses them. Any time you use one of these small integers, Python hands you the same cached object instead of creating a new one. It’s like a tea stall that keeps regular chai ready-made because everyone orders it, while special orders are brewed fresh every time.
📄 Python REPL: the 256 boundary that surprises everyone
>>> a = 256 >>> b = 256 >>> a is b # -5 to 256 are cached, so this is one shared object True >>> c = 257 >>> d = 257 >>> c is d # 257 is past the cache, so two separate objects False >>> e = -5 >>> f = -5 >>> e is f # -5 is cached True >>> g = -6 >>> h = -6 >>> g is h # -6 sits just below the cache False >>> c == d # a value check is always correct, caching or not True
python with no file name, then type the lines one at a time. Here is the catch. When Python compiles a whole .py file, it spots the two identical 257 literals and stores just one copy, so c is d comes back True even for big numbers. The REPL compiles each line on its own, so it shows you the real cache boundary above. Same Python, opposite answer, purely because of how the code was compiled. That is the whole reason you never lean on is to compare numbers.What happened here: When CPython starts up, it pre-builds the integer objects from -5 to 256, because those numbers show up everywhere (loop counters, status codes, small sums). Ask for 256 and Python hands you the copy it already made. Ask for 257 at the REPL and you get a brand new object each time. The lesson never changes: use == to compare values, and keep is for checking None. The is operator looks correct for small integers, then quietly lies to you for bigger ones.
Common Mistakes
Mistake 1: Using is to compare values
🚫 Wrong
user_input = 300
if user_input is 300: # works... sometimes. Fails unpredictably.
print("Match!")
✅ Correct
user_input = 300
if user_input == 300: # always correct, compares values
print("Match!")
Why: is compares object identity, not value. It happens to work for small integers due to caching, but breaks for numbers above 256, strings with special characters, and almost everything else. Python itself warns you here: since Python 3.8, writing is against a literal like 300 triggers a SyntaxWarning. Use == for values, is only for None.
Mistake 2: Using camelCase instead of snake_case
🚫 Non-Pythonic
firstName = "Aviraj" lastName = "Patel" isActive = True
✅ Pythonic
first_name = "Aviraj" last_name = "Patel" is_active = True
Why: PEP 8 (Python’s official style guide) prescribes snake_case for variable and function names. PascalCase is for class names. ALL_CAPS is for constants. The community follows this consistently.
Mistake 3: Shadowing built-in names
🚫 Dangerous
list = [1, 2, 3] # shadows the built-in list() function type = "premium" # shadows the built-in type() function print = "hello" # shadows the built-in print() function # Now you can't use list(), type(), or print() anymore!
✅ Safe
team_list = [1, 2, 3] # descriptive, doesn't shadow account_type = "premium" # clear meaning message = "hello" # no conflict
Why: Python lets you use built-in names as variables without warning. Once you assign to list, the built-in list() function becomes unreachable in that scope. This causes confusing TypeError: 'list' object is not callable errors five minutes later when you try to use list().
Best Practices
- DO use
snake_casefor variables and functions:user_name,total_score - DO use
ALL_CAPSfor constants:MAX_RETRIES,API_KEY - DO use descriptive names:
student_countoverscorx - DO use
==for value comparison,isonly forNone - DO use
id()when debugging reference issues - DON’T shadow built-in names: never use
list,type,str,dictas variable names - DON’T use single-letter names except in loops (
i,j) or math formulas (x,y) - DON’T rely on
isfor integer comparisons, since caching boundaries are an implementation detail
Conclusion
Python variables are names that refer to objects, not boxes that contain values. Assignment with = binds a name to an object. Multiple names can point to the same object. id() reveals the identity of what a name points to, and is checks whether two names point to the same object while == checks whether two objects have the same value.
The integer caching surprise (-5 to 256) is not something you need to memorize for daily coding. It’s something you need to know exists so you never accidentally rely on is for value comparison. Use == always. Use is only for None.
Next up: Data Types, every built-in type Python offers, from int to bytes, plus the mutable-versus-immutable distinction that prevents a whole category of Python bugs. The memory model you just learned becomes critical the moment you reach mutable data structures like lists and dictionaries, and later when you build your own classes in Part 2.
This post is part of a much bigger journey. Browse every lesson, from your first variable to full AI/ML projects, at the Python + AI/ML tutorial series home.
Practice Exercises
- Exercise 1: Create variables for name, age, city. Print in a sentence.
- Exercise 2: Create two variables pointing to the same list. Modify via one, verify via the other using
id(). - Exercise 3: Demonstrate integer caching: compare
id()for small vs large integers. Showisvs==.
Frequently Asked Questions
What is a variable in Python?
A Python variable is a name that refers to an object in memory. Unlike languages such as C or Java, Python variables are not typed containers, they are labels that point to objects. The object holds the value and type information, not the variable name.
What is the difference between is and == in Python?
== compares the values of two objects (equality). is checks whether two names refer to the exact same object in memory (identity). Use == for comparing values and is only for checking None (if x is None).
Why does Python cache small integers?
CPython pre-allocates integer objects for -5 through 256 because these values appear extremely frequently in Python programs (loop counters, index values, status codes). Reusing these objects saves memory and speeds up execution. This is an implementation detail of CPython, not a language guarantee.
What naming convention should I use for Python variables?
Use snake_case for variables and functions (user_name, total_count). Use ALL_CAPS for constants (MAX_SIZE, API_KEY). Use PascalCase only for class names. This follows PEP 8, Python’s official style guide.
Can Python variables change type?
Yes. Python uses dynamic typing, where a variable name can point to an object of any type, and you can reassign it to a different type at any time. x = 42 followed by x = 'hello' is valid. The type belongs to the object, not the name.
Does Python have constants?
Python has no const keyword or language-level constant enforcement. By convention, names in ALL_CAPS (like MAX_RETRIES = 3) are treated as constants. Python will not prevent reassignment, but the development community treats ALL_CAPS variables as read-only.
Interview Questions on Python Variables
Try each one aloud first. The phrasing you produce under mild pressure is what interviews measure.
Q: You write b = a where a is a list, append an item through b, and suddenly a has changed too. Why, and how do you get an independent copy?
Assignment never copies data in Python, it only binds a second name to the same object. So a and b are two labels on one list, and a mutation through either name is visible through both. To get an independent copy, use b = a.copy(), b = list(a), or b = a[:]. If the list contains nested lists, you need copy.deepcopy(a) because a shallow copy still shares the inner objects.
Q: A teammate compares numbers with if count is 500:. It seems to pass in their script but fails when they test the same lines at the REPL. What is going on, and what should they write instead?
is checks object identity, not value. Inside one compiled .py file, Python may fold identical literals into a single shared object, so the check can accidentally pass. At the REPL each line is compiled separately, and 500 is outside the -5 to 256 cache, so two separate objects are created and is returns False. Python 3.8+ even raises a SyntaxWarning for is with a literal. The fix is always if count == 500:.
Q: What actually happens under the hood when you swap with x, y = y, x?
Python evaluates the entire right-hand side first, packing the current values of y and x together, then unpacks them and binds the names on the left. Because the right side is fully evaluated before any rebinding happens, no temporary variable is needed. This is the same unpacking mechanism used for a, b, c = some_list and for functions that return multiple values.
Q: What is the difference between rebinding a name and mutating an object?
Rebinding (score = 100) points the name at a brand new object and leaves the old object untouched; other names still see the old object. Mutating (team.append("Anvi")) changes the object in place, so every name bound to that object sees the change. Immutable types like int and str can only be rebound, while mutable types like list and dict can be changed in place. Knowing which one your line of code does is the fastest way to predict aliasing bugs.
Q: You named a variable list earlier in a script, and later list(range(5)) crashes with TypeError: 'list' object is not callable. What happened and how do you fix it?
The assignment shadowed the built-in list type: in that scope, the name list now points to your data instead of the built-in callable, so calling it fails. Python gives no warning when you shadow built-ins. Rename the variable to something descriptive like team_list or scores. In a pinch at the REPL, del list removes the shadowing name and restores access to the built-in.
Q: Does del x delete the object that x points to?
No. del x only removes the name binding, the sticky note, not the object itself. The object is destroyed only when its reference count drops to zero, meaning no other names or containers refer to it anymore. If y also points to the same object, the object stays alive after del x. CPython also runs a garbage collector to clean up objects that reference each other in cycles.
Want more? the official Python documentation documents everything this post could not fit.
Related Posts
Previous: Python: Install Python, VS Code & Write Your First Program (Hello World)
Next: Python: Data Types (int, float, str, bool, complex, bytes, bytearray, None)
Series Home: Python + AI/ML Tutorial Series

No comment