Creating Variables
A variable is created the moment you assign a value to it. No declaration keyword needed.
# Assignment creates the variable
name = "Alice"
age = 30
height = 1.75
is_admin = False
nothing = None
# Print them
print(name) # Alice
print(age) # 30
print(height) # 1.75
print(is_admin) # False
print(nothing) # None
# Reassign to a new value (or even a different type)
age = 31 # update
age = "thirty" # change type entirely (valid in Python, unusual in practice)
# Using a variable before assigning it raises NameError
# print(undefined_var) # NameError: name 'undefined_var' is not defined
Dynamic Typing
Python determines a variable's type at runtime based on the value assigned. The same variable can hold different types at different times.
x = 42
print(type(x)) # <class 'int'>
x = 3.14
print(type(x)) # <class 'float'>
x = "hello"
print(type(x)) # <class 'str'>
x = [1, 2, 3]
print(type(x)) # <class 'list'>
# isinstance() - check type (handles inheritance too)
n = 42
print(isinstance(n, int)) # True
print(isinstance(n, float)) # False
print(isinstance(n, (int, float))) # True - check against multiple types
# Python variables are labels/references, not boxes
a = [1, 2, 3]
b = a # b points to the SAME list
b.append(4)
print(a) # [1, 2, 3, 4] - a is also affected!
# To get an independent copy:
c = a.copy() # shallow copy
c.append(5)
print(a) # [1, 2, 3, 4] - a unchanged
Naming Rules
# VALID names
user_name = "Alice" # snake_case - preferred
userName = "Alice" # camelCase - valid but not Pythonic
_private = "hidden" # underscore prefix = private by convention
__dunder__ = "special" # double underscores = dunder (magic)
x = 10 # single letter (ok for math/loops)
i = 0 # loop counter
my_var_2024 = True # letters, digits, underscores - any order
# INVALID names
# 2fast = True # SyntaxError: cannot start with a digit
# my-var = True # SyntaxError: hyphens not allowed
# my var = True # SyntaxError: spaces not allowed
# class = True # SyntaxError: 'class' is a reserved keyword
# Python reserved keywords - cannot use as variable names
import keyword
print(keyword.kwlist)
# ['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
# 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except',
# 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is',
# 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try',
# 'while', 'with', 'yield']
Multiple Assignment
# Assign multiple variables on one line
x, y, z = 1, 2, 3
print(x, y, z) # 1 2 3
# Swap values without a temp variable
a, b = 10, 20
a, b = b, a # swap
print(a, b) # 20 10
# Unpack from a list or tuple
first, second, third = [10, 20, 30]
print(first) # 10
# Star unpacking - collect the rest
head, *tail = [1, 2, 3, 4, 5]
print(head) # 1
print(tail) # [2, 3, 4, 5]
first, *middle, last = [1, 2, 3, 4, 5]
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5
# Assign the same value to multiple variables
a = b = c = 0
print(a, b, c) # 0 0 0
# Walrus operator := (Python 3.8+) - assign inside an expression
data = [1, 2, 3, 4, 5]
if (n := len(data)) > 3:
print(f"List has {n} items") # List has 5 items
Augmented Assignment
Augmented assignment operators combine an operation with assignment. They are shorthand for x = x op value.
x = 10
x += 5 # x = x + 5 -> 15
x -= 3 # x = x - 3 -> 12
x *= 2 # x = x * 2 -> 24
x /= 4 # x = x / 4 -> 6.0 (always float)
x //= 2 # x = x // 2 -> 3.0 (floor division)
x **= 3 # x = x ** 3 -> 27.0 (power)
x %= 5 # x = x % 5 -> 2.0 (modulo)
# Works on strings too
greeting = "Hello"
greeting += ", World!"
print(greeting) # Hello, World!
# Works on lists
items = [1, 2]
items += [3, 4] # extends the list
print(items) # [1, 2, 3, 4]
# Note: Python has no ++ or -- operators
# Use x += 1 instead of x++
Constants
Python has no built-in constant mechanism. By convention, variables named in ALL_CAPS are treated as constants - not meant to be changed.
# Convention: UPPER_SNAKE_CASE signals "do not change this"
MAX_CONNECTIONS = 100
DEFAULT_TIMEOUT = 30
PI = 3.14159265358979
BASE_URL = "https://api.example.com"
DEBUG = False
# Python does not prevent reassignment - it is a convention only
MAX_CONNECTIONS = 200 # technically allowed, but violates convention
# For true immutability, use a frozen data class or named tuple
from typing import Final
# Final (Python 3.8+) - type checkers enforce it, runtime does not
MAX_SIZE: Final = 1000
# MAX_SIZE = 2000 # mypy/pylance warns: Cannot assign to final variable
# Module-level constants go in a separate constants.py or config.py
# and are imported where needed:
# from config import MAX_CONNECTIONS, BASE_URL
Deleting Variables
x = 42
print(x) # 42
del x # delete the variable (removes the name binding)
# print(x) # NameError: name 'x' is not defined
# del can remove list items, dict keys
fruits = ["apple", "banana", "cherry"]
del fruits[1] # removes "banana"
print(fruits) # ['apple', 'cherry']
data = {"a": 1, "b": 2}
del data["a"]
print(data) # {'b': 2}
# del does not necessarily free memory immediately
# Python's garbage collector handles that when there are no more references
Scope Introduction
Variables have a scope - where in the code they are accessible. A full scope deep-dive is in the Functions lesson, but here are the basics.
# Module level (global) - accessible anywhere in the file
global_var = "I am global"
def my_function():
local_var = "I am local" # only accessible inside this function
print(global_var) # can read globals
print(local_var) # can read locals
my_function()
print(global_var) # works
# print(local_var) # NameError - local_var doesn't exist here
# global keyword - modify a global variable inside a function
count = 0
def increment():
global count # declare intention to modify the global
count += 1
increment()
print(count) # 1
# Best practice: avoid global variables; pass values as arguments instead