Skip to main content

Command Palette

Search for a command to run...

Python Variables: Your First Step to Programming Mastery

Published
9 min read

Why Variables Matter (And Why You Should Care)

Imagine organizing a massive library. You need labels, tags, and a system to find anything instantly. Variables are your labeling system for data in programming.

After diving deep into Python this week, I realized understanding variables isn't about memorization, it's about building mental models that support everything you'll learn next.

What IS a Variable? (The Truth About Memory)

The Beginner Explanation

A variable is a named container storing a value in memory.

The Real Story (This Changes Everything!)

Variables in Python are NOT boxes that hold values.

Variables are LABELS (references) that point to objects in memory.

# When you write this:
age = 25

# Python actually does this:
# 1. Creates an integer object with value 25 in memory
# 2. Creates a label 'age' that points to that object
# 3. If you reassign age, it points to a DIFFERENT object

Seeing Memory in Action

Python gives you a superpower: the id() function shows memory addresses!

x = 100
print(f"Value: {x}")
print(f"Memory address: {id(x)}")

# Every object has:
# - Identity: unique memory address (id())
# - Type: data type (type())
# - Value: the data itself

Output:

Value: 100
Memory address: 140715830258480  # Your address will differ

Everything is an Object

In Python, EVERYTHING is an object—even simple numbers:

python

x = 5
print(type(x))        # <class 'int'>
print(type(type(x)))  # <class 'type'> - types are objects too!

# Even functions are objects!
print(type(print))    # <class 'builtin_function_or_method'>

This "everything is an object" philosophy is core to Python's design.


Core Data Types

Python has several built-in types you'll use constantly:

1. Integers (int) — Whole Numbers

age = 30
population = 7_900_000_000  # Underscores for readability!
negative = -42

print(type(age))  # <class 'int'>

2. Floats (float) — Decimal Numbers

height = 5.9
temperature = -3.5
pi = 3.14159

print(type(height))  # <class 'float'>

3. Strings (str) — Text

name = "Alice"
message = 'Hello, World!'  # Single or double quotes
multiline = """This spans
multiple lines"""

print(type(name))  # <class 'str'>

4. Booleans (bool) — True or False

is_student = False
has_license = True

print(type(is_student))  # <class 'bool'>

⚠️ Critical: Integer + Float = Float

num1 = 10      # int    
num2 = 3.5     # float
result = num1 + num2
print(result)          # 13.5
print(type(result))    # <class 'float'> - automatically converted!

Key insight: Python uses implicit type coercion to preserve precision.

Division with a single “/” Always Returns Float

# Regular division ALWAYS gives float
result = 10 / 2
print(result)          # 5.0 (float, not 5!)
print(type(result))    # <class 'float'>

# Floor division for integers
result = 10 // 3
print(result)          # 3 (int)

Variable Naming Conventions

Good names = self-documenting code. Bad names = debugging nightmares.

The Rules

# ✅ VALID
user_name = "john"
_private_var = 42
user2 = "jane"
CONSTANT_VALUE = 100

# ❌ INVALID
2nd_user = "bob"      # Can't start with number
user-name = "alice"   # No hyphens
for = 10              # Reserved keyword
user name = "eve"     # No spaces

Python Convention: snake_case

# ✅ Python style (snake_case)
total_price = 99.99
user_age = 28
is_active = True

# ❌ Don't use (camelCase is for JavaScript/Java)
totalPrice = 99.99
userAge = 28

Constants Use UPPER_CASE

PI = 3.14159
MAX_CONNECTIONS = 100
API_KEY = "secret123"  # Though better in env vars!

Pro tip: Python doesn't enforce constants, but naming convention signals intent.


Variable Assignment & Multiple Assignment

Basic Assignment

x = 10
y = 20
x = 15  # Reassignment

Multiple Assignment

# Assign multiple variables at once
a, b, c = 1, 2, 3
print(a, b, c)  # 1 2 3

# Swap without temporary variable!
x, y = 5, 10
x, y = y, x  # Mind = blown 🤯
print(x, y)  # 10 5

This is called tuple unpacking and it's incredibly powerful:

# Extended unpacking with *
first, *middle, last = [1, 2, 3, 4, 5]
print(first)   # 1
print(middle)  # [2, 3, 4]
print(last)    # 5

🚨 Critical Concept: Variables are References, Not Boxes

This is THE concept that separates beginners from intermediate programmers.

The Box Analogy is WRONG

# What beginners think happens:
a = [1, 2, 3]  # Box 'a' contains [1, 2, 3]
b = a          # Box 'b' gets a COPY of [1, 2, 3]

# What ACTUALLY happens:
a = [1, 2, 3]  # Create a list object, 'a' points to it
b = a          # 'b' points to THE SAME object

Proof: Modifying One Changes Both

a = [1, 2, 3]
b = a          # Both point to same object!
b.append(4)

print(a)  # [1, 2, 3, 4] - a changed too!
print(b)  # [1, 2, 3, 4]

# Verify they're the same object
print(id(a) == id(b))  # True

Visualization:

    a ────┐
          ↓
      [1, 2, 3, 4]  ← Single object in memory
          ↑
    b ────┘

Why Don't Integers Behave This Way?

x = 10
y = x
y = 20

print(x)  # 10 - x didn't change!

This leads us to...


Mutable vs Immutable Objects

This explains EVERYTHING about Python's behavior!

Immutable Types (Cannot Change After Creation)

  • int, float, str, tuple, bool, frozenset

python

x = 10
print(id(x))  # Let's say 140715830258480

x = 11  # Creates NEW object, doesn't modify old one
print(id(x))  # Different ID! 140715830258512

Mutable Types (Can Be Modified In-Place)

  • list, dict, set
my_list = [1, 2, 3]
print(id(my_list))  # Let's say 2131234567890

my_list.append(4)  # Modifies SAME object
print(id(my_list))  # SAME ID! 2131234567890

Why This Matters

# Immutable example
def try_to_change(x):
    x = 999  # Creates new local variable

num = 10
try_to_change(num)
print(num)  # 10 - unchanged!

# Mutable example
def try_to_change_list(lst):
    lst.append(999)  # Modifies original object!

numbers = [1, 2, 3]
try_to_change_list(numbers)
print(numbers)  # [1, 2, 3, 999] - changed!

The is vs == Operator

Understanding references unlocks this:

list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1

# == compares VALUES
print(list1 == list2)  # True - same values

# is compares IDENTITY (same object?)
print(list1 is list2)  # False - different objects
print(list1 is list3)  # True - same object!

print(id(list1))  # e.g., 12345678
print(id(list2))  # e.g., 87654321 (different!)
print(id(list3))  # e.g., 12345678 (same as list1!)

Python's Small Integer Cache

Python optimizes by reusing small integers (-5 to 256):

a = 256
b = 256
print(a is b)  # True - Python reuses same object!

a = 257
b = 257
print(a is b)  # False - separate objects
               # (might be True in interactive mode)

Why? Memory efficiency—small numbers are used constantly.


Type Conversion (Casting)

Convert between types explicitly:

# int → float
x = 10
y = float(x)
print(y)  # 10.0

# float → int (truncates, doesn't round!)
x = 9.99
y = int(x)
print(y)  # 9 (not 10!)

# int → str
x = 42
y = str(x)
print(y)  # "42" (string)

# str → int
x = "100"
y = int(x)
print(y)  # 100 (integer)

⚠️ Invalid Conversions Raise Errors

# This will crash!
# int("hello")  # ValueError

# Always validate first
user_input = "123"
if user_input.isdigit():
    number = int(user_input)
    print(number)  # 100

Variable Scope

Where can you access a variable?

Local Scope

python

def my_function():
    local_var = "I'm local"
    print(local_var)  # ✅ Works

my_function()
# print(local_var)  # ❌ NameError!

Global Scope

python

global_var = "I'm global"

def my_function():
    print(global_var)  # ✅ Can read global

my_function()
print(global_var)  # ✅ Works here too

Modifying Global Variables

python

counter = 0

def increment():
    global counter  # Must declare!
    counter += 1

increment()
print(counter)  # 1

The LEGB Rule

Python searches for variables in this order:

  1. Local - inside current function

  2. Enclosing - in any enclosing functions

  3. Global - module level

  4. Built-in - Python's built-in namespace

python

x = "global"

def outer():
    x = "enclosing"

    def inner():
        x = "local"
        print(x)  # "local"

    inner()
    print(x)  # "enclosing"

outer()
print(x)  # "global"

Modern Python: Type Hints (Python 3.5+)

Not enforced at runtime, but improves readability and catches errors with tools:

# Basic type hints
name: str = "Alice"
age: int = 30
height: float = 5.9
is_student: bool = True

# Function with type hints
def greet(name: str, age: int) -> str:
    return f"Hello {name}, you are {age} years old"

# Collections (Python 3.9+)
numbers: list[int] = [1, 2, 3]
scores: dict[str, int] = {"Alice": 95}

# Optional values (can be None)
from typing import Optional
middle_name: Optional[str] = None

Why use them?

  • IDEs give better autocomplete

  • Static type checkers (mypy) catch bugs

  • Self-documenting code

  • Required in many professional codebases


Best Practices: Production-Ready Code

1. Descriptive Names Over Brevity

# ❌ Don't
t = 99.99
u = "John"
c = 0

# ✅ Do
total_price = 99.99
username = "John"
item_count = 0

2. Avoid Single Letters (Except Loops)

# ❌ Unclear
for x in items:
    process(x)

# ✅ Clear
for item in items:
    process(item)

# ✅ OK for math/coordinates
x, y = calculate_position()

3. Use Constants for Magic Numbers

# ❌ What does 86400 mean?
seconds = days * 86400

# ✅ Clear intent
SECONDS_PER_DAY = 86400
seconds = days * SECONDS_PER_DAY

4. Minimize Global Variables

# ❌ Risky
total = 0

def add_to_total(x):
    global total
    total += x

# ✅ Better - use return values
def add_to_total(current_total, x):
    return current_total + x

total = add_to_total(total, 5)

Common Pitfalls (Learn from My Mistakes!)

Pitfall #1: Mutable Default Arguments

# ❌ THIS WILL SURPRISE YOU
def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2] - Wait, what?!
print(add_item(3))  # [1, 2, 3] - They all share the same list!

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

print(add_item(1))  # [1]
print(add_item(2))  # [2] - Works as expected!

Why? Default arguments are evaluated ONCE when function is defined!

Pitfall #2: Copying Mutable Objects

# ❌ Not a real copy!
original = [1, 2, 3]
copy = original
copy.append(4)
print(original)  # [1, 2, 3, 4] - Changed!

# ✅ Shallow copy
import copy
original = [1, 2, 3]
copy = original.copy()  # or list(original)
copy.append(4)
print(original)  # [1, 2, 3] - Unchanged!

# ✅ Deep copy (for nested structures)
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)

Pitfall #3: Shadowing Built-ins

# ❌ Don't do this!
list = [1, 2, 3]  # Now list() doesn't work!
str = "hello"     # Now str() doesn't work!

# ✅ Use descriptive names
items_list = [1, 2, 3]
name_str = "hello"

Pitfall #4: Integer Division Surprise

# ❌ Expecting integer result
average = 100 / 3  # 33.333... (float)

# ✅ Be explicit
average = 100 // 3  # 33 (floor division)
# or
average = round(100 / 3)  # 33 (rounded)

Summary: Key Takeaways

  1. Variables are references/labels, not containers

  2. Every object has: identity (id), type, value

  3. Mutable vs immutable determines behavior

  4. Use is for identity, == for value comparison

  5. Follow snake_case naming convention

  6. Scope follows LEGB rule

  7. Beware mutable default arguments!

  8. Type hints improve code quality