Variables seem like the simplest possible topic in any programming language, and in one sense, they are — I just pick a name, use =, and I’m done. But the way Python handles variables under the hood is genuinely different from many other languages, and understanding that difference has saved me from bugs I couldn’t have explained years ago. In this guide, I’ll go from the absolute basics of variable creation all the way to how Python actually manages variable references and memory internally.
Creating a Variable
In Python, there’s no separate “declaration” step — creating a variable and assigning it a value happen in the same line, using the = operator:
name = "Ali"
age = 28
height = 5.9
is_student = False
print(name, age, height, is_student)
Output:
Ali 28 5.9 False
There’s no var, let, int, or String keyword required. Python figures out the type automatically based on the value assigned — this is a direct consequence of Python being dynamically typed, which I cover in depth in the datatypes guide.
Variable Naming Rules
Python enforces a few strict rules, and I follow a few more conventions on top of them:
Strict rules (violating these causes a SyntaxError):
- Must start with a letter (a-z, A-Z) or an underscore
_, never a digit. - Can only contain letters, digits, and underscores after the first character.
- Cannot be a reserved keyword (
class,for,if,return, etc.). - Case-sensitive —
age,Age, andAGEare three completely different variables.
_valid = 1
valid_2 = 2
Valid3 = 3
# 2invalid = 4 -> SyntaxError: cannot start with a digit
# class = 5 -> SyntaxError: 'class' is a reserved keyword
Conventions I follow (from PEP 8), not enforced by Python but expected in professional code:
snake_casefor variable and function names:first_name,total_score.UPPER_CASEfor constants:MAX_RETRIES = 5.PascalCasereserved for class names, not variables.- Avoid single-character names except for short-lived loop counters (
i,j) or well-understood math contexts.
Multiple Assignment
Python supports several convenient assignment patterns I use constantly:
# Assign the same value to multiple variables
x = y = z = 0
print(x, y, z) # Output: 0 0 0
# Assign different values in a single line
a, b, c = 1, 2, 3
print(a, b, c) # Output: 1 2 3
# Swapping values without a temporary variable
a, b = b, a
print(a, b) # Output: 2 1
# Unpacking a list or tuple
coordinates = (10, 20)
x_coord, y_coord = coordinates
print(x_coord, y_coord) # Output: 10 20
# Unpacking with a "catch-all" star
first, *rest = [1, 2, 3, 4, 5]
print(first) # Output: 1
print(rest) # Output: [2, 3, 4, 5]
The swap pattern (a, b = b, a) is something I use so often that it took me a while to realize other languages typically need a temporary variable to achieve the same thing — Python evaluates the entire right-hand side into a temporary tuple first, then unpacks it into the left-hand names.
How Variable Assignment Actually Works Internally
This is the part that genuinely changed how I write Python. Unlike languages where a variable is a labeled memory slot holding a value directly, Python variables are names bound to objects. The assignment x = 5 doesn’t put 5 “inside” x — it creates an integer object 5 somewhere in memory, and makes the name x point to it.
x = 5
y = x
print(id(x), id(y)) # Output: same memory address for both -- they reference the same object
x = 10
print(id(x), id(y)) # Output: x now points to a new object; y still points to the old one
print(x, y) # Output: 10 5
Because integers are immutable, reassigning x doesn’t change the object 5 — it just makes x point to a brand-new object 10, while y keeps pointing to the original 5. This behaves intuitively for immutable types, but it’s a very different story with mutable types like lists:
list1 = [1, 2, 3]
list2 = list1 # Both names now reference the SAME list object
list2.append(4)
print(list1) # Output: [1, 2, 3, 4] -- list1 changed too!
print(list1 is list2) # Output: True -- confirms they're the same object
If I actually wanted an independent copy, I’d need to explicitly copy it:
list3 = list1.copy() # or list1[:] or list(list1)
list3.append(99)
print(list1) # Output: [1, 2, 3, 4] -- unaffected
print(list3) # Output: [1, 2, 3, 4, 99]
Small Integer and String Caching (CPython Implementation Detail)
Out of curiosity, I once tested this:
a = 5
b = 5
print(a is b) # Output: True
a = 500
b = 500
print(a is b) # Output: False (in most standard CPython runs)
This surprised me until I learned that CPython — the standard Python implementation — pre-caches small integers (typically -5 to 256) as singleton objects for performance reasons, since they’re used so frequently. Larger integers are created fresh each time, so two variables holding the same large number aren’t necessarily the same object in memory, even though they’re equal in value. This is purely an implementation detail of CPython, not a guaranteed language feature, which is exactly why I always use == to check equality and reserve is strictly for identity checks like is None.
Constants in Python
Python doesn’t have a true, enforced constant keyword — there’s no way to make a variable truly unchangeable at the language level (unlike const in JavaScript or final in Java). Instead, the convention is to name it in UPPER_CASE to signal “please don’t change this” to other developers:
MAX_CONNECTIONS = 100
PI = 3.14159
# Nothing technically stops this, but it's considered bad practice:
# MAX_CONNECTIONS = 200
If I genuinely need enforced immutability, I reach for typing.Final (a type-checker hint, not a runtime enforcement) or define the value inside an immutable structure like a tuple or a frozen dataclass.
Variable Scope
Where a variable is created determines where it can be accessed — this is called scope, and Python follows the LEGB rule (Local, Enclosing, Global, Built-in):
x = "global value"
def my_function():
x = "local value"
print(x) # Output: local value
my_function()
print(x) # Output: global value -- unaffected by the function's local x
If I want a function to modify a variable defined outside it, I need the global keyword explicitly:
counter = 0
def increment():
global counter
counter += 1
increment()
increment()
print(counter) # Output: 2
Without global, attempting counter += 1 inside the function would raise UnboundLocalError, because Python would treat counter as a new local variable the moment it saw an assignment to it inside the function body — and referencing it before that local assignment fails.
Common Mistakes I’ve Made
- Assuming
list2 = list1creates a copy — it doesn’t; both names reference the same mutable object. - Relying on
isfor value comparison — worked by coincidence for small cached integers, then broke mysteriously for larger numbers or other objects. - Forgetting
globalwhen trying to modify a global variable inside a function, resulting inUnboundLocalError. - Using mutable objects as default function arguments, like
def f(data=[]):, not realizing the same list object persists and accumulates across every call that doesn’t pass its own argument.
Real-World Applications
Solid variable naming and scoping discipline directly affects how maintainable real projects are. In every codebase I’ve worked on professionally, clear variable names (user_email instead of ue), disciplined use of constants for configuration values, and careful handling of mutable default state have been the difference between code that’s easy to review and code that causes production bugs from unexpected shared state.
Frequently Asked Questions
Do I need to declare a variable’s type in Python? No — Python infers the type automatically from the assigned value, and the same variable name can be reassigned to a different type later.
What’s the difference between = and ==? = assigns a value to a variable; == compares two values for equality and returns a boolean.
Can I create a variable name that starts with a number? No — Python variable names must start with a letter or underscore, never a digit.
Why did changing one variable also change another that I never touched? This happens when two variable names reference the same mutable object (like a list or dictionary) rather than independent copies — use .copy() to avoid this.
Summary
Creating variables in Python is deceptively simple on the surface — just a name and an = sign — but underneath, every assignment is really about binding a name to an object in memory, not copying a value into a box. Understanding that distinction, along with naming conventions, scope rules, and the mutable-vs-immutable behavior of assignment, is foundational to writing correct, bug-free Python code.