Variables #

A variable is a name that points to a value in memory. In Python, this concept is better described as name binding — you’re not creating a “box” that holds a value, but giving a name to an object that already exists. This distinction feels abstract at first, but it becomes crucial when you work with objects that can be modified (mutable), or when two variables point to the same object. This article covers how Python variables work from the ground up: assignment, naming, unpacking, scope, and the type hints that make code more explicit and safer.

Assignment and Dynamic Typing #

In Python, a variable is created the first time you assign a value to it with the = operator. No type declaration is needed — Python automatically determines the type from the assigned value.

# Basic assignment
name = "Budi"        # str
age = 25             # int
height = 175.5       # float
active = True        # bool

# Check the type with type()
print(type(name))    # → <class 'str'>
print(type(age))     # → <class 'int'>
print(type(height))  # → <class 'float'>
print(type(active))  # → <class 'bool'>

Python is a dynamically typed language — a variable can point to values of different types within the same program:

data = 42            # data is an int
print(type(data))    # → <class 'int'>

data = "forty-two"   # now data is a str
print(type(data))    # → <class 'str'>

data = [4, 2]        # now data is a list
print(type(data))    # → <class 'list'>

This differs from statically typed languages like Java or Go, where a variable’s type is fixed at declaration and can’t change.

How Python Stores Variables #

Understanding how Python manages memory prevents a lot of confusing bugs:

# Every assignment creates a "label" pointing to an object
a = [1, 2, 3]
b = a             # b and a point to THE SAME OBJECT

b.append(4)
print(a)          # → [1, 2, 3, 4]  ← a changed too!
print(b)          # → [1, 2, 3, 4]

# Verify that both point to the same object
print(a is b)     # → True
print(id(a) == id(b))  # → True (id() returns the memory address)

For a clearer picture of how variables in Python act as labels (pointers) to objects in heap memory, look at the memory reference diagram below:

flowchart LR
    subgraph Namespace ["Variable Names (Stack)"]
        a["Variable: a"]
        b["Variable: b"]
    end

    subgraph Heap ["Heap Memory"]
        obj["List Object: [1, 2, 3, 4]"]
    end

    a -->|"Point to the same reference"| obj
    b -->|"Point to the same reference"| obj

When you modify the object through the label b (b.append(4)), the physical object in heap memory is what changes. Since a also points to the same object, the change is immediately visible through the variable a.

# ANTI-PATTERN: assuming assignment creates an automatic copy
def add_item(cart):
    cart.append("new item")  # modifies the original object!

shopping = ["apple", "orange"]
add_item(shopping)
print(shopping)  # → ["apple", "orange", "new item"]  ← unexpected?

# CORRECT: make an explicit copy if you don't want to modify the original
def add_item_safe(cart):
    copy = cart.copy()       # or: list(cart)
    copy.append("new item")
    return copy

shopping = ["apple", "orange"]
result = add_item_safe(shopping)
print(shopping)  # → ["apple", "orange"]  ← unchanged
print(result)    # → ["apple", "orange", "new item"]
This behavior only applies to mutable objects (list, dict, set). For immutable objects (int, str, tuple, float), assignment always creates a new binding — no hidden side effects.

Variable Naming Rules #

Python has hard rules (which cause errors if violated) and soft conventions (no error, but unidiomatic):

Hard Rules #

# ✓ Allowed: letters, digits (not at the start), underscore
user_name = "alice"
data2024 = []
_private = True
__very_private = False

# ✗ Error: starting with a digit
# 2024data = []       # SyntaxError

# ✗ Error: containing special characters
# user-name = ""      # SyntaxError (parsed as a subtraction)
# email@domain = ""   # SyntaxError
# score% = 0          # SyntaxError

# ✗ Error: using a Python keyword
# if = 10             # SyntaxError
# for = []            # SyntaxError
# class = "A"         # SyntaxError

Naming Conventions (PEP 8) #

# ✓ snake_case for variables and functions
student_count = 30
full_name = "John Doe"
is_active = True

# ✗ camelCase (unidiomatic in Python, even though it doesn't error)
studentCount = 30      # Java/JavaScript style
FullName = "John"      # C# style

# ✓ UPPER_SNAKE_CASE for constants
CREDIT_LIMIT = 50_000_000
TAX_RATE = 0.11

# ✓ PascalCase for class names
class UserData:
    pass

# ✓ Underscore prefix for private conventions
_cache = {}             # protected (single underscore)
__internal_state = {}   # name-mangled (double underscore)

# ✓ Single underscore for unused variables
for _ in range(5):
    print("hello")

Names to Avoid #

# ANTI-PATTERN: shadowing Python built-ins — no error but very dangerous
list = [1, 2, 3]       # shadows the built-in list()
dict = {"a": 1}        # shadows the built-in dict()
str = "hello"          # shadows the built-in str()
id = 42                # shadows the built-in id()
type = "integer"       # shadows the built-in type()
input = "data"         # shadows the built-in input()
len = 10               # shadows the built-in len()

# After the lines above, you can no longer use:
numbers = list(range(5)) # TypeError: 'list' object is not callable

# CORRECT: use more descriptive names
number_list = [1, 2, 3]
config = {"a": 1}
error_message = "hello"
user_id = 42

Multiple Assignment #

Python provides several concise ways to assign values to many variables at once.

# Chained assignment — all variables point to THE SAME OBJECT
a = b = c = 0
print(a, b, c)   # → 0 0 0

# Safe for immutable (int, str, float)
a = b = c = 0
a = 10           # creates a new binding for a only
print(a, b, c)   # → 10 0 0  ← b and c unchanged

# DANGEROUS for mutable (list, dict)
x = y = z = []   # all three point to THE SAME LIST
x.append(1)
print(y)         # → [1]  ← y changed too!

# CORRECT for mutable: create separate objects
x = []
y = []
z = []
# Tuple unpacking — assign several variables at once
a, b, c = 1, 2, 3
print(a, b, c)   # → 1 2 3

# Swap values without a temporary variable — a signature Python idiom
x = 10
y = 20
x, y = y, x
print(x, y)      # → 20 10

# ANTI-PATTERN: swapping the way other languages do
# temp = x
# x = y
# y = temp

Variable Unpacking #

Unpacking is the way to extract elements from a data collection straight into individual variables. It’s one of Python’s most expressive features.

Basic Unpacking #

# Unpacking from a tuple
coordinates = (10.5, -6.2)
latitude, longitude = coordinates
print(latitude)   # → 10.5
print(longitude)  # → -6.2

# Unpacking from a list
rgb = [255, 128, 0]
red, green, blue = rgb
print(f"R={red}, G={green}, B={blue}")  # → R=255, G=128, B=0

# Unpacking from a string
letter_a, letter_b, letter_c = "ABC"
print(letter_a)  # → A

Extended Unpacking with * #

The * (star) operator captures the remaining elements that aren’t explicitly unpacked:

# Grab the first and last elements, keep the rest in the middle
first, *middle, last = [1, 2, 3, 4, 5]
print(first)   # → 1
print(middle)  # → [2, 3, 4]
print(last)    # → 5

# Grab only some of many values
head, *tail = [10, 20, 30, 40, 50]
print(head)    # → 10
print(tail)    # → [20, 30, 40, 50]

*front, end = [10, 20, 30, 40, 50]
print(front)   # → [10, 20, 30, 40]
print(end)     # → 50

Unpacking in Loops #

# Unpack directly in a for loop — extremely common
pairs = [(1, "one"), (2, "two"), (3, "three")]

# ANTI-PATTERN: accessing via index
for item in pairs:
    print(item[0], item[1])

# CORRECT: unpack right in the for header
for number, word in pairs:
    print(number, word)

# Real-world example with enumerate()
fruits = ["apple", "orange", "mango"]
for index, name in enumerate(fruits):
    print(f"{index}: {name}")
# → 0: apple
# → 1: orange
# → 2: mango

# Real-world example with dict.items()
prices = {"apple": 5000, "orange": 8000, "mango": 12000}
for fruit_name, unit_price in prices.items():
    print(f"{fruit_name}: Rp{unit_price:,}")

Unpacking to Ignore Values #

Use _ for values you don’t need:

# Ignore the middle element
first, _, last = (10, 99, 20)
print(first, last)  # → 10 20

# Ignore several elements
name, _, _, city = ("Budi", "25", "Male", "Jakarta")
print(name, city)  # → Budi Jakarta

# Ignore all the rest
head, *_ = [1, 2, 3, 4, 5]
print(head)  # → 1

Variable Scope #

Scope determines where a variable can be accessed. Python follows the LEGB rule: Local → Enclosing → Global → Built-in.

flowchart TD
    B["Built-in (len, print, type, ...)"] --> G["Global (module/file-level variables)"]
    G --> E["Enclosing (variables in the outer function, for nested functions)"]
    E --> L["Local (variables inside the current function)"]

Local Scope #

Variables created inside a function can only be accessed within that function:

def compute():
    result = 100     # local variable
    print(result)    # → 100

compute()
print(result)        # NameError: name 'result' is not defined

Global Scope #

Variables outside a function can be read from inside a function, but can’t be modified without the global keyword:

message = "Hello"   # global variable

def display():
    print(message)  # ✓ can be read

def change():
    message = "Bye" # ← this creates a new LOCAL variable, not changing the global
    print(message)  # → Bye

display()  # → Hello
change()   # → Bye
print(message) # → Hello  ← the global didn't change

# To truly change the global:
def change_global():
    global message
    message = "Bye"

change_global()
print(message)  # → Bye
Using global should be avoided unless truly necessary. Global variables modified from inside functions make data flow hard to trace and code hard to test. A better solution: return the value from the function and capture the result outside.

Nonlocal Scope #

nonlocal is used inside nested functions to access a variable from the enclosing function:

def make_counter():
    count = 0                    # variable in the enclosing scope

    def increment():
        nonlocal count           # reference to count in make_counter
        count += 1
        return count

    return increment

counter = make_counter()
print(counter())  # → 1
print(counter())  # → 2
print(counter())  # → 3

Type Hints (Type Annotations) #

Since Python 3.5, you can add type annotations to variables and function parameters. This doesn’t change program behavior (Python stays dynamic), but it helps IDEs, linters, and other developers understand code faster.

# Variable annotations
name: str = "Budi"
age: int = 25
height: float = 175.5
active: bool = True

# Annotations without an initial value (type declaration only)
class User:
    name: str
    email: str
    age: int
from typing import Optional, Union, List, Dict

# Optional — the variable may be None
def find_user(user_id: int) -> Optional[str]:
    # returns a name or None if not found
    ...

# Union — one of several types (Python 3.9 and below)
def format_score(score: Union[int, float]) -> str:
    return f"{score:.2f}"

# New syntax with | (Python 3.10+)
def format_score(score: int | float) -> str:
    return f"{score:.2f}"

# Collections with element types
def compute_average(data: List[float]) -> float:
    return sum(data) / len(data)

def find_price(catalog: Dict[str, int], name: str) -> int:
    return catalog.get(name, 0)
# ANTI-PATTERN: annotations inconsistent with the actual value
# (no error, but misleading and flagged by linters)
age: int = "twenty-five"     # a str, not an int
active: bool = 1              # an int, not a bool

# CORRECT: annotations matching the actual value
age: int = 25
active: bool = True
Type hints are optional and not enforced by Python at runtime. To actually check type consistency, use a tool like mypy (pip install mypy) or enable type checking in VS Code via Pylance.

Deleting Variables #

The del keyword removes the binding between a variable name and its object. After del, the variable can no longer be accessed.

x = 100
print(x)   # → 100

del x
print(x)   # → NameError: name 'x' is not defined

# Handy for releasing references to large objects
import numpy as np
big_data = np.zeros((10000, 10000))   # ~800MB
# ... process the data ...
del big_data   # helps the garbage collector free memory faster

Summary #

  • Python variables are labels, not boxes — assignment binds a name to an object, it doesn’t copy a value. Two variables can point to the same mutable object and affect each other.
  • Dynamic typing — the type comes from the value, not a declaration. Variables can “change type” at runtime, but that’s rarely good practice.
  • Don’t shadow built-ins — avoid names like list, dict, str, id, type, input for your variables.
  • a = b = c = 0 is safe for immutable, but dangerous for mutable — use separate assignments for lists/dicts.
  • Unpacking makes code more expressive — use a, b = b, a for swapping, for k, v in d.items() for dict iteration, and *rest to capture remaining elements.
  • LEGB scope — Python looks up variables from Local → Enclosing → Global → Built-in. Use global and nonlocal sparingly.
  • Avoid global — it’s better to return values from functions than to modify global variables directly.
  • Type hints don’t change program behavior but greatly help IDEs and collaborators understand code faster.

← Previous: Comments   Next: Constants →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact