Python Variables: Complete Guide with Examples

Variables are named containers that store data in your program's memory. In Python, creating a variable is straightforward—you simply assign a value to a name using the equals sign. Unlike...

Key Insights

  • Python uses dynamic typing where variables can change types at runtime, making code flexible but requiring careful attention to type-related bugs
  • Variable scope follows the LEGB rule (Local, Enclosing, Global, Built-in), and understanding this hierarchy prevents common scoping errors
  • Mutable objects like lists behave differently than immutable objects when assigned to multiple variables, leading to unexpected reference-sharing bugs if not understood

Introduction to Variables in Python

Variables are named containers that store data in your program’s memory. In Python, creating a variable is straightforward—you simply assign a value to a name using the equals sign. Unlike statically-typed languages like Java or C++, Python doesn’t require you to declare a variable’s type beforehand. The interpreter infers the type based on the assigned value.

This dynamic typing system makes Python incredibly flexible and concise. You can write age = 25 instead of int age = 25;. The trade-off is that type-related bugs may only surface at runtime rather than during compilation.

# Basic variable assignment
name = "John"
age = 25
is_student = True

# Multiple assignment in one line
x, y, z = 1, 2, 3
print(x, y, z)  # Output: 1 2 3

# Assigning the same value to multiple variables
a = b = c = 0

Multiple assignment is particularly useful when unpacking data structures or initializing several variables with the same value. This syntax is clean and Pythonic.

Variable Naming Rules and Conventions

Python enforces specific rules for variable names. A valid variable name must start with a letter (a-z, A-Z) or underscore (_), followed by letters, digits (0-9), or underscores. Variable names are case-sensitive, so user and User are different variables.

Beyond the technical requirements, PEP 8—Python’s official style guide—recommends using snake_case for variable names: lowercase words separated by underscores. Choose descriptive names that reveal intent rather than cryptic abbreviations.

# Valid variable names
user_name = "Alice"
_private_var = 42
total_count2 = 100

# Invalid variable names (will cause SyntaxError)
# 2nd_place = "Bob"  # Can't start with digit
# user-name = "Charlie"  # Hyphens not allowed
# class = "Python"  # Reserved keyword

# Poor naming practices
x = "John Doe"  # Too vague
usrnm = "Alice"  # Unnecessary abbreviation
UserName = "Bob"  # Should use snake_case for variables

# Good naming practices
user_full_name = "John Doe"
maximum_retries = 3
is_authenticated = False

Python reserves certain keywords that cannot be used as variable names: if, else, for, while, class, def, return, import, and others. Attempting to use these will raise a SyntaxError.

Variable Types and Dynamic Typing

Python’s dynamic typing means you don’t declare types explicitly—the interpreter determines the type at runtime based on the assigned value. Common built-in types include integers (int), floating-point numbers (float), strings (str), booleans (bool), lists (list), and dictionaries (dict).

The type() function reveals a variable’s current type. Since Python is dynamically typed, a variable can change types during execution by reassigning it to a value of a different type.

# Type inference
count = 42
print(type(count))  # <class 'int'>

price = 19.99
print(type(price))  # <class 'float'>

message = "Hello"
print(type(message))  # <class 'str'>

# Variables can change types
value = 100
print(type(value))  # <class 'int'>

value = "Now I'm a string"
print(type(value))  # <class 'str'>

# Type conversion (casting)
num_str = "42"
num_int = int(num_str)  # Convert string to integer
print(num_int + 8)  # Output: 50

float_num = float("3.14")
str_num = str(100)

Type conversion is essential when working with user input or data from external sources. The int(), float(), and str() functions handle most common conversions, but be aware they’ll raise ValueError if the conversion is impossible (like int("hello")).

Variable Scope and Lifetime

Scope determines where in your code a variable can be accessed. Python follows the LEGB rule to resolve variable names:

  • Local: Variables defined inside a function
  • Enclosing: Variables in the local scope of enclosing functions
  • Global: Variables defined at the module level
  • Built-in: Python’s built-in names like print, len, etc.

When you reference a variable, Python searches these scopes in order until it finds the name.

# Global scope
global_var = "I'm global"

def outer_function():
    # Enclosing scope
    enclosing_var = "I'm in the enclosing scope"
    
    def inner_function():
        # Local scope
        local_var = "I'm local"
        print(local_var)  # Accessible
        print(enclosing_var)  # Accessible from enclosing scope
        print(global_var)  # Accessible from global scope
    
    inner_function()

outer_function()

# Modifying global variables
counter = 0

def increment():
    global counter  # Required to modify global variable
    counter += 1

increment()
print(counter)  # Output: 1

# Using nonlocal for enclosing scope
def outer():
    count = 0
    
    def inner():
        nonlocal count  # Modify enclosing scope variable
        count += 1
        return count
    
    print(inner())  # Output: 1
    print(inner())  # Output: 2

outer()

Avoid overusing global variables—they make code harder to test and reason about. Pass values as function parameters and return results instead.

Variable Assignment Patterns

Python offers several powerful assignment patterns beyond basic assignment. Tuple unpacking lets you assign multiple variables from a sequence in one line. You can swap variables without a temporary variable using tuple packing and unpacking.

Augmented assignment operators (+=, -=, *=, etc.) provide shorthand for modifying a variable’s value. Python 3.8 introduced the walrus operator (:=), which assigns and returns a value in a single expression.

# Tuple unpacking
coordinates = (10, 20)
x, y = coordinates
print(x, y)  # Output: 10 20

# Unpacking with * operator
first, *middle, last = [1, 2, 3, 4, 5]
print(first)   # Output: 1
print(middle)  # Output: [2, 3, 4]
print(last)    # Output: 5

# Variable swapping without temp variable
a, b = 5, 10
a, b = b, a  # Swap values
print(a, b)  # Output: 10 5

# Augmented assignment operators
count = 10
count += 5  # Same as count = count + 5
print(count)  # Output: 15

score = 100
score *= 2  # Same as score = score * 2
print(score)  # Output: 200

# Walrus operator (Python 3.8+)
# Useful in loops and conditionals
data = [1, 2, 3, 4, 5]
if (n := len(data)) > 3:
    print(f"List has {n} items")  # Output: List has 5 items

# Walrus in while loop
while (line := input("Enter text (or 'quit'): ")) != "quit":
    print(f"You entered: {line}")

The walrus operator reduces code duplication when you need to both assign and use a value in the same expression. Use it judiciously—it can make code less readable if overused.

Common Pitfalls and Best Practices

Understanding how Python handles object references prevents subtle bugs. When you assign a mutable object (like a list) to multiple variables, they all reference the same object in memory. Changes through one variable affect all others.

The is operator checks if two variables reference the same object, while == checks if their values are equal. For immutable objects like integers and strings, Python may reuse the same object for efficiency, but you shouldn’t rely on this behavior.

# Mutable default argument problem
def add_item(item, items=[]):  # DON'T DO THIS
    items.append(item)
    return items

print(add_item(1))  # Output: [1]
print(add_item(2))  # Output: [1, 2] - Unexpected!

# Correct approach
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item(1))  # Output: [1]
print(add_item(2))  # Output: [2] - Correct

# Multiple references to the same object
list1 = [1, 2, 3]
list2 = list1  # Both reference the same list
list2.append(4)
print(list1)  # Output: [1, 2, 3, 4] - Changed!

# Creating a copy
list3 = [1, 2, 3]
list4 = list3.copy()  # or list3[:]
list4.append(4)
print(list3)  # Output: [1, 2, 3] - Unchanged

# Shallow vs deep copy for nested structures
import copy

nested = [[1, 2], [3, 4]]
shallow = nested.copy()
deep = copy.deepcopy(nested)

shallow[0].append(99)
print(nested)   # Output: [[1, 2, 99], [3, 4]] - Changed!
print(shallow)  # Output: [[1, 2, 99], [3, 4]]

deep[0].append(100)
print(nested)  # Output: [[1, 2, 99], [3, 4]] - Unchanged

# is vs ==
a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)  # True - same values
print(a is b)  # False - different objects
print(a is c)  # True - same object

Always use None as the default value for mutable default arguments, then create the mutable object inside the function. When you need a copy of a list or dictionary, use the .copy() method for shallow copies or copy.deepcopy() for nested structures.

Conclusion

Mastering variables in Python requires understanding dynamic typing, scope rules, and how Python manages object references. The flexibility of dynamic typing accelerates development but demands awareness of potential type-related issues. Remember the LEGB scope resolution order when debugging variable access problems, and be cautious with mutable objects to avoid reference-sharing bugs.

As you continue learning Python, you’ll apply these variable concepts when working with functions, classes, and data structures. Practice these patterns in real code, and you’ll develop an intuition for writing clean, bug-free Python. Next, explore functions to see how local scope and parameters work in practice, or dive into data structures to understand how variables interact with complex types like lists, sets, and dictionaries.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.