Python Variables
Naming, Assignment, and Scope
Variables are the containers that hold your data. Learn how Python binds names to values, how scope determines what a variable refers to, and how Python's memory model differs from other languages.
name = "Alice" age = 30 is_student = False print(f"{name} is {age} years old.") age = age + 1 print(f"Next year, {name} will be {age}.")
Alice is 30 years old.
Next year, Alice will be 31.
📋 What This Lesson Covers
Everything you need to know about Python's most fundamental building block
📦 Why Variables Matter
A variable is a name that refers to a value stored in memory. Without variables, every program would have to hardcode its data — you couldn't read user input, loop over items, or build up results incrementally. Variables are how programs remember things.
Python's variable model is subtly different from most languages. In C or Java, a variable is a box that holds a value. In Python, a variable is a name that refers to an object — assignment doesn't copy values, it binds names to objects. This difference matters: it explains why two variables can point to the same list, why id() is useful, and why reassigning one variable doesn't always affect the other.
This lesson walks through assignment, multiple assignment, naming rules, scope (the LEGB rule), the memory model, and conventions for constants. By the end, you'll have a solid mental model for how Python handles data.
What is a Variable?
Names, values, and the binding between them — the conceptual foundation.
CoreAssignment
The = operator, augmented assignment, and why order matters.
Scope & LEGB
Local, enclosing, global, built-in — how Python resolves names.
IntermediateMemory Model
Objects, references, id(), and the difference between is and ==.
📦 What Is a Variable?
A name that refers to a value — but the exact meaning is more subtle than it first appears
🎯 The Conceptual Model
In Python, a variable is a name bound to an object. When you write x = 42, Python:
- Creates an integer object with value
42somewhere in memory. - Binds the name
xto that object. - Stores the binding in the current scope (usually the local namespace of your script or function).
x = 42 print(x) print(id(x)) print(type(x))
The id() function returns the object's memory address — a unique integer that identifies which object x refers to. Two names referring to the same object will have the same id().
🔁 Reassignment Rebinds, Not Mutates
When you reassign a variable, Python doesn't modify the original object — it binds the name to a new object. The old object may still exist if other names refer to it.
a = 10 b = a print("a:", a, "b:", b) a = 20 print("a:", a, "b:", b) print("Same object?", a is b)
Output:
a: 10 b: 10
a: 20 b: 10
Same object? FalseWhen a = 20 runs, Python creates a new integer object with value 20 and binds a to it. The name b still refers to the original 10 object. Both objects coexist in memory — 10 because b still references it, 20 because a now does.
a = 20 would change the value inside a's box, and b would be a separate box holding the original copy. Python's behavior is more like "referencing" than "copying."📋 Variables in Python vs Other Languages
| Language | Model | Behavior of x = 42 |
|---|---|---|
| Python | Name → object | Binds the name x to a new object 42. |
| C | Box → value | Stores 42 in a memory location named x. |
| Java | Box → value/reference | Primitives store the value; objects store a reference to the object. |
| JavaScript | Reference or value | Similar to Python for objects, but primitive types are copied. |
✏️ Assignment — Binding Names to Values
The = operator, augmented assignment, and common pitfalls
🎯 Basic Assignment
An assignment statement uses the = operator. The left side is the name; the right side is an expression that evaluates to a value.
x = 42 name = "Alice" is_active = True pi = 3.14159 empty = None result = 10 + 20 * 3 user = {"name": "Bob", "age": 25} print(x, name, is_active, pi, empty) print(result)
The right side is evaluated first — fully, to a single value — and then the assignment happens. So x = x + 1 works because x + 1 is computed using the old value of x, and only after that does x get rebound to the result.
➕ Augmented Assignment
Python provides shorthand operators that combine arithmetic with assignment: +=, -=, *=, /=, //=, %=, **=. These are equivalent to the long form but more concise.
counter = 0 counter += 1 counter += 1 counter += 1 print(counter) total = 100 total -= 25 total *= 2 total //= 5 print(total) message = "Hello" message += ", World!" print(message)
x += [1] on a list mutates in place; x = x + [1] creates a new list. For immutable objects like integers and strings, they're equivalent.🚫 Assignment Is Not Equality
A common beginner mistake is confusing = (assignment) with == (equality comparison). They are completely different operators.
if x = 5:
print("x is five")
Error: SyntaxError: invalid syntax. You cannot use assignment inside a condition.
if x == 5:
print("x is five")
== compares; = assigns. Use the right one.
:= which does assign inside an expression: if (n := len(data)) > 10: print(f"Too long: {n}"). This is the only case where assignment appears inside a condition, and it's still fairly rare in day-to-day code.🔗 Chained Assignment
You can assign the same value to multiple names in a single statement:
a = b = c = 0 print(a, b, c) x = y = [1, 2, 3] x.append(4) print(y)
Chained assignment with mutable objects is dangerous — all three names point to the same object. Modifying through one name affects the others. This is often a subtle bug.
a = b = []. Use a = []; b = [] instead if you want each to be independent. The chain form is safest with immutable values like 0, None, or strings.🔗 Multiple Assignment & Unpacking
Assign several variables in one line — one of Python's most convenient features
🔀 Tuple Unpacking
Python evaluates the right side as a tuple, then matches each element to a name on the left:
x, y, z = 1, 2, 3 print(x, y, z) name, age, city = "Alice", 30, "Manila" print(name, age, city)
If the number of names doesn't match the number of values, Python raises a ValueError:
# ValueError: too many values to unpack # a, b = 1, 2, 3 # ValueError: not enough values to unpack # a, b, c = 1, 2
⭐ Starred Assignment
A single name can be prefixed with * to absorb all "extra" values into a list. This is called extended unpacking and is useful when you know the first item or two but not the rest.
first, *middle, last = [1, 2, 3, 4, 5] print("First:", first) print("Middle:", middle) print("Last:", last) head, *tail = ["a", "b", "c", "d"] print(head, tail)
*head, last = ... works too.🔄 Swapping Variables Without a Temp
Because Python evaluates the right side first, you can swap two variables without a temporary:
a = 10
b = 20
a, b = b, a
print(a, b)Compare to the C-style approach, which needs a temp variable:
temp = a a = b b = temp
Python's version is shorter, safer, and more readable — no risk of accidentally overwriting a value before it's been used.
🎯 Unpacking Function Return Values
When a function returns a tuple, you can unpack it directly into names:
def min_max(numbers): return min(numbers), max(numbers) low, high = min_max([3, 1, 4, 1, 5, 9, 2, 6]) print(f"Low: {low}, High: {high}") values = min_max([1, 2, 3]) print(values)
divmod(17, 5) returns (3, 2), and you can write quotient, remainder = divmod(17, 5) for a clean, direct destructuring.🔤 Variable Naming Rules & Conventions
What Python allows, and what the community expects
✅ Rules the Interpreter Enforces
These are hard requirements — violating any of them raises a SyntaxError or NameError at runtime:
- ✓Must begin with a letter (a–z, A–Z) or underscore (
_) - ✓Can contain letters, digits, and underscores after the first character
- ✓Cannot be a reserved keyword (
class,def,if,for, etc.) - ✓Case-sensitive —
name,Name, andNAMEare three different variables - ✓Cannot contain spaces, hyphens, or most punctuation
# Valid names: name = "Alice" _user_id = 42 MAX_SIZE = 100 count2 = 5 myLongVariableName = True # Invalid — will raise SyntaxError: # 2fast = 10 # first-name = "Bob" # class = "Physics" # my var = 5
💡 Conventions Python Expects (PEP 8)
These aren't enforced by the interpreter, but they're followed universally in professional Python code. Following them makes your code instantly readable to any other Python developer.
| Name Style | Used For | Example |
|---|---|---|
snake_case | Variables, functions, modules | user_name, total_price |
PascalCase | Classes | UserAccount, HttpClient |
UPPER_SNAKE_CASE | Constants | MAX_RETRIES, DEFAULT_TIMEOUT |
_leading_underscore | Internal / private use | _cache, _helper |
__double_leading | Name mangling (rare) | __secret (used inside classes) |
__dunder__ | Python-defined special names | __init__, __repr__ |
🎯 What Makes a Good Variable Name
Beyond the rules, good names have a few qualities:
- ✓Descriptive, not abbreviated.
user_account_balanceis clearer thanuaborbal. - ✓Pronounceable. If you can't say it out loud, you'll struggle to talk about the code with others.
- ✓Not too long.
the_total_quantity_of_items_in_the_shopping_cartis excessive —cart_totalis better. - ✓No type suffixes.
user_listorname_strare un-Pythonic — let the value speak for itself. - ✓Not shadowing built-ins. Never name a variable
list,dict,type,id, orinput— it breaks the built-in function.
active_users = get_active_users() max_retry_count = 3 is_authenticated = check_auth()
au = get_active_users() mrc = 3 flag = check_auth()
au, mrc, and flag tell the reader nothing. They were fast to type and will be slow to read six months from now.
⚠️ Never Shadow Built-ins
Python has 35+ built-in functions and constants available by default. Naming a variable after one of them "shadows" the built-in, making it unavailable in the current scope — and leading to confusing bugs.
list = [1, 2, 3] print(list(x)) # TypeError: 'list' object is not callable
After list = [1, 2, 3], the name list refers to a list, not the built-in function. Anywhere else in this scope that calls list() will fail.
my_list = [1, 2, 3]
print(list(my_list))
list, dict, set, tuple, str, int, float, bool, type, id, input, print, sum, min, max, len, range, open, file, bytes, object.🔭 Variable Scope & the LEGB Rule
How Python decides which object a name refers to — and why UnboundLocalError happens
Not every name is visible everywhere. Python has four scopes, and its rules for resolving names are governed by the LEGB rule: Local, Enclosing, Global, Built-in. Understanding this is essential for debugging errors and writing clean, side-effect-free functions.
🧭 The Four Scopes
| Scope | Letter | What It Contains |
|---|---|---|
| Local | L | Names defined inside the current function. |
| Enclosing | E | Names defined in any enclosing function (for nested functions). |
| Global | G | Names defined at the top level of the module. |
| Built-in | B | Names Python provides by default (print, len, range, etc.). |
When Python encounters a name, it searches these scopes in order: L → E → G → B. The first match wins.
x = "global" def outer(): x = "enclosing" def inner(): x = "local" print(x) inner() print(x) outer() print(x)
Output:
local enclosing global
Each function defines a new local scope. The inner function's x shadows the enclosing function's, which shadows the global. Reading the name at different points in the program gives different results.
🌐 Global Statement
By default, assignment inside a function creates a local variable — it doesn't touch the global. To reassign a global variable from inside a function, use the global keyword.
counter = 0 def increment_bad(): counter = counter + 1 # UnboundLocalError def increment_good(): global counter counter += 1 increment_good() print(counter)
increment_bad raises UnboundLocalError — not NameError. This confuses beginners. The reason: Python sees counter = ... inside the function, so it treats counter as a local name for the entire function. When the right side tries to read counter (before assignment), it isn't yet bound locally — hence "referenced before assignment."global is often considered a code smell. Prefer returning values from functions over mutating global state. Global variables make code harder to test, reason about, and parallelise.🏠 nonlocal Statement
For nested functions, nonlocal rebinds a name in the nearest enclosing scope — useful for closures and counters.
def make_counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment counter = make_counter() print(counter()) print(counter()) print(counter())
Each call to increment() modifies the count in the enclosing make_counter scope. The nonlocal keyword tells Python "this name is not local to increment; look in the enclosing scope."
🔍 Inspecting Scope — locals() and globals()
You can inspect the current scope's namespaces at any time:
x = "global_x" y = 42 def demo(): local_var = "local" print("Locals:", locals()) print("Globals count:", len(globals())) demo()
locals() returns a dict of the current local namespace. globals() returns a dict of all module-level names — including the ones you defined plus the built-ins that have been imported. Both are useful for debugging.🧠 The Memory Model — Objects & References
Understanding id(), is vs ==, and why mutable variables behave surprisingly
🎯 Every Value Is an Object
In Python, everything is an object: integers, strings, functions, classes, modules, and even None. Every object has:
- ✓An identity — a unique integer, retrievable with
id(obj), never changes during the object's lifetime - ✓A type — retrievable with
type(obj), never changes - ✓A value — mutable objects can change; immutable objects cannot
x = 42 print(id(x)) print(type(x)) print(x) name = "Alice" print(id(name)) print(type(name)) print(name)
🆔 Understanding id() and Identity
id() returns the memory address of an object (in CPython). Two variables referring to the same object will have the same id():
a = [1, 2, 3] b = a c = [1, 2, 3] print(a is b) print(a is c) print(a == c) print(id(a) == id(b)) print(id(a) == id(c))
a and b refer to the same object — same id, is returns True. c is a distinct object with the same value — different id, is returns False, but == returns True.
⚖️ is vs == — The Critical Distinction
| Operator | Compares | When to Use |
|---|---|---|
is | Identity (same object) | Comparing to None, True, False, or checking for object identity |
== | Equality (same value) | Almost everything else — numbers, strings, lists, custom objects |
if value is None: pass if a == b: pass
if value == None: pass if a is b: pass # rarely what you want
is comparison with None works because None is a singleton — there's only ever one None object in Python. Comparing x == None is technically allowed but discouraged — a custom __eq__ could make it return non-True for a genuinely-None value. is bypasses __eq__ entirely.🔒 Mutable vs Immutable Objects
Python objects fall into two camps:
- ✓Immutable:
int,float,str,bool,tuple,frozenset,bytes,None— cannot be changed after creation - ✓Mutable:
list,dict,set,bytearray, custom classes — can be modified in place
# Immutable — reassignment creates a new object s = "hello" s2 = s s += " world" print(s, s2) # s2 is unchanged # Mutable — in-place mutation affects all references nums = [1, 2, 3] nums2 = nums nums.append(4) print(nums, nums2) # both are [1, 2, 3, 4]
my_list.copy()) or design functions to return new lists instead of mutating in place.📋 Copying — copy() vs deepcopy()
For nested mutable structures, a shallow copy doesn't protect you:
import copy original = [[1, 2], [3, 4]] shallow = copy.copy(original) deep = copy.deepcopy(original) original[0][0] = 99 print("Shallow:", shallow) print("Deep:", deep)
Both shallow and original share the same inner lists, so changing one changes both. deepcopy recursively copies every nested object — the truly independent copy.
deepcopy sparingly — it's slow and can't copy objects that don't support deep copying (like file handles). For most flat structures, a shallow copy is enough.🔒 Constants — Python's Convention
Python has no true constants — but the convention is well-established
📌 UPPER_CASE by Convention
Python doesn't have a const keyword. Instead, the community follows a naming convention: a variable whose name is entirely uppercase (UPPER_SNAKE_CASE) is treated as a constant — meaning "don't reassign this."
MAX_RETRIES = 3 DEFAULT_TIMEOUT = 30 API_BASE_URL = "https://api.example.com" PI = 3.141592653589793 print(MAX_RETRIES) print(API_BASE_URL)
🔐 Enforcing Constants with Enums or Frozen Dataclasses
If you truly need immutability, Python has options:
from enum import Enum class Status(Enum): PENDING = "pending" ACTIVE = "active" COMPLETE = "complete" print(Status.PENDING.value) # Status.PENDING = "other" # AttributeError: cannot reassign from dataclasses import dataclass @dataclass(frozen=True) class Config: host: str = "localhost" port: int = 8080 config = Config() # config.port = 9000 # FrozenInstanceError
Enum when you need a fixed set of related constants; use frozen=True on a dataclass when a whole record should be immutable.📍 Where to Put Constants
Two common patterns:
- At the top of a module — best when the constants belong to a single file.
- In a dedicated
constants.py— best when shared across many modules in a project.
# constants.py DATABASE_URL = "postgresql://localhost/mydb" MAX_LOGIN_ATTEMPTS = 5 SESSION_TIMEOUT_SECONDS = 3600 # other_module.py from constants import MAX_LOGIN_ATTEMPTS if attempts >= MAX_LOGIN_ATTEMPTS: lock_account()
➡️ What's Next
You understand variables — now let's explore the types of values they can hold
int, float, str, bool, list, tuple, dict, set, and None — everything a variable can hold.
Start lesson →Integers, floats, and complex numbers — with arithmetic, math module, and precision tips.
Start lesson →Convert between strings, numbers, lists and more — with safe parsing patterns.
Start lesson →Parameters, return values, scope, closures — where variables interact with functions.
Start lesson →❓ Frequently Asked Questions
Common questions about Python variables
No — Python is dynamically typed. When you write x = 42, Python infers the type from the value. You never write int x = 42 like in C or Java. However, Python 3.6+ supports optional type hints: x: int = 42. These hints are ignored at runtime, but tools like mypy and your editor use them to catch type errors before they reach production. Hints are highly recommended for any codebase larger than a single script.
Python allows it — a variable can refer to an integer at one moment and a string the next. This is a consequence of the "name → object" model: reassigning just rebinds the name to a new object of the new type. However, the type checker (if you use one) will flag the inconsistency, and code that switches types is usually harder to follow. If a variable genuinely needs to hold different types at different times, consider whether a union type hint (int | str) or a redesign would be clearer.
This is one of Python's most confusing error messages for beginners. It happens because Python decides at compile time that any name assigned anywhere in a function is a local name for the whole function. So this code:
counter = 0
def increment():
print(counter) # UnboundLocalError
counter = 1fails on the print line — Python sees counter = 1 somewhere in the function, decides counter is local to the whole function, and the print reads an unassigned local. The fix is to remove the assignment, or declare global counter at the top of the function.
x is None is preferred — and it's not just style. None is a singleton: there's only one None object in memory, so is always gives the right answer, and it's faster. x == None works for None, but if x happens to be a custom class with a broken __eq__ method, the comparison could return something unexpected. Using is bypasses __eq__ entirely. Every Python linter will flag == None and suggest is None.
del x removes the binding entirely — afterward, x no longer refers to anything, and trying to read it raises NameError. x = None rebinds x to the None object — the name still exists, just referring to a different value. Use del when you want to free the memory associated with the object (if nothing else references it), and x = None when you want to mark a variable as "empty" but keep it around.
Yes, strictly. myvar, myVar, MyVar, and MYVAR are four completely distinct names — Python treats them as unrelated. This catches beginners who mix camelCase and snake_case across a file. Follow PEP 8's snake_case convention consistently to avoid confusion.
No. Identifiers must start with a letter (a–z, A–Z) or underscore. 2fast is a syntax error. You can have digits later: var2, player_1, and _2fast are all valid. The underscore prefix is the standard workaround when you need a name that conceptually starts with a digit.
Letters (a–z, A–Z), digits (0–9), and underscores (_). Python 3 also technically allows Unicode letters, so variable_eñe, 变数, or 变量 work — but PEP 8 strongly recommends ASCII-only identifiers for portability and readability. Don't use $, @, #, -, or any other punctuation — Python will reject it.
Sometimes. Standard exceptions: i, j, k for loop counters; x, y, z for coordinates; e for exception variables in except blocks; _ for throwaway values (unused tuple elements, unused loop variables). Everywhere else, prefer descriptive names. If a single-letter name needs a comment to explain it, use a longer name instead.
By convention, _ is used to indicate "I don't care about this value." Examples: for _ in range(10): when you just need to loop 10 times, or first, _, third = my_tuple when you only need the first and third elements. The underscore is a normal variable, but the community treats it as a marker of intent — a signal to readers that this value is intentionally ignored.
🗣️ Learner Voices
No comments yet — your voice starts the conversation.