What Is a Variable?
A variable is a named location in memory that holds a value. Think of it as a label you stick on a box — the label is the name, the box holds the data.
Memory Model:
─────────────────────────────────────────────────────────
Variable Name Memory Address Value
───────────── ──────────────── ──────
name → 0x7f3a2b10 "Alice"
age → 0x7f3a2b28 28
height → 0x7f3a2b40 1.68
is_student → 0x7f3a2b58 True
─────────────────────────────────────────────────────────
Python is dynamically typed — you never declare a type. Python figures it out from the value you assign.
# No type declarations needed — Python infers the type
name = "Alice" # str
age = 28 # int
height = 1.68 # float
is_student = True # bool
nothing = None # NoneType
# Check the type of anything with type()
print(type(name)) # <class 'str'>
print(type(age)) # <class 'int'>
print(type(height)) # <class 'float'>
print(type(is_student)) # <class 'bool'>
print(type(nothing)) # <class 'NoneType'>
Python's 5 Core Primitive Types
Type 1: int — Integers
Whole numbers with no size limit (Python handles big integers natively).
# Basic integers
score = 100
year = 2024
temp = -15
big_num = 1_000_000_000 # underscores for readability
# Python's integers are arbitrarily large — no overflow!
factorial_20 = 2432902008176640000
print(factorial_20) # works perfectly
# Binary, octal, hex literals
binary = 0b1010 # 10 in decimal
octal = 0o17 # 15 in decimal
hexadec = 0xFF # 255 in decimal
print(f"Binary 0b1010 = {binary}")
print(f"Octal 0o17 = {octal}")
print(f"Hex 0xFF = {hexadec}")
# Integer operations
a, b = 17, 5
print(a + b) # 22 addition
print(a - b) # 12 subtraction
print(a * b) # 85 multiplication
print(a / b) # 3.4 TRUE division → always float
print(a // b) # 3 FLOOR division → rounds down
print(a % b) # 2 MODULO (remainder)
print(a ** b) # 1419857 exponentiation (17^5)
Type 2: float — Floating Point Numbers
Decimal numbers — stored using IEEE 754 double precision (64-bit).
pi = 3.14159265358979
e = 2.718281828
gravity = -9.81
tiny = 1.5e-10 # scientific notation: 1.5 × 10⁻¹⁰
huge = 6.022e23 # Avogadro's number
# The famous floating point surprise
print(0.1 + 0.2) # 0.30000000000000004 ← NOT 0.3!
print(0.1 + 0.2 == 0.3) # False ← dangerous!
# Correct way to compare floats
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True ✅
print(abs(0.1 + 0.2 - 0.3) < 1e-9) # True ✅
# Why does this happen?
# Floats are stored in binary. 0.1 in binary is:
# 0.0001100110011001100... (repeating) — can't be exact!
# Special float values
print(float('inf')) # infinity
print(float('-inf')) # negative infinity
print(float('nan')) # not a number (result of invalid math)
# Useful float operations
import math
print(math.floor(3.7)) # 3 (round down)
print(math.ceil(3.2)) # 4 (round up)
print(round(3.14159, 2)) # 3.14 (round to 2 decimal places)
print(abs(-7.5)) # 7.5 (absolute value)
Type 3: str — Strings
Text data — an immutable sequence of Unicode characters.
# Four ways to create strings
single = 'Hello, World!'
double = "Hello, World!"
triple_s = '''Line one
Line two
Line three'''
triple_d = """Also
multi-line"""
# Strings are sequences — you can index and slice them
name = "Python"
# P y t h o n
# index 0 1 2 3 4 5
# -6 -5 -4 -3 -2 -1 (negative indexes from end)
print(name[0]) # P (first character)
print(name[-1]) # n (last character)
print(name[2:5]) # tho (slice: index 2 up to but not including 5)
print(name[:3]) # Pyt (from start to index 3)
print(name[3:]) # hon (from index 3 to end)
print(name[::-1]) # nohtyP (reverse!)
# Essential string methods
text = " Hello, World! "
print(text.strip()) # "Hello, World!" (remove whitespace)
print(text.lower()) # " hello, world! "
print(text.upper()) # " HELLO, WORLD! "
print(text.replace("World", "Python")) # " Hello, Python! "
print("Hello".startswith("He")) # True
print("World".endswith("ld")) # True
print("Python".find("th")) # 2 (index of substring)
print(",".join(["a", "b", "c"])) # "a,b,c"
print("a,b,c".split(",")) # ['a', 'b', 'c']
print("hello".capitalize()) # "Hello"
print("hello world".title()) # "Hello World"
# String formatting — 3 ways (f-strings are best!)
name, age = "Alice", 28
# Method 1: f-strings (Python 3.6+ — USE THIS)
print(f"Name: {name}, Age: {age}")
print(f"Next year: {age + 1}") # expressions inside {}
print(f"Pi = {3.14159:.2f}") # format spec: 2 decimal places
print(f"{'Left':<10}|{'Right':>10}") # alignment
# Method 2: .format()
print("Name: {}, Age: {}".format(name, age))
print("Name: {n}, Age: {a}".format(n=name, a=age))
# Method 3: % operator (old style, avoid)
print("Name: %s, Age: %d" % (name, age))
# Strings are IMMUTABLE — this fails:
word = "Hello"
# word[0] = "J" # ← TypeError! Can't change a string in place
word = "J" + word[1:] # Must create a new string: "Jello"
Type 4: bool — Booleans
Only two values: True or False. Subclass of int in Python!
# Basic booleans
is_active = True
is_deleted = False
# bool IS a subclass of int
print(int(True)) # 1
print(int(False)) # 0
print(True + True) # 2 (!!)
# Boolean operators
print(True and True) # True
print(True and False) # False
print(False or True) # True
print(not True) # False
# Comparison operators → produce booleans
x = 10
print(x > 5) # True
print(x == 10) # True (equality)
print(x != 5) # True (not equal)
print(x >= 10) # True
print(x <= 9) # False
# Chained comparisons (Pythonic!)
age = 25
print(18 <= age < 65) # True (legal working age)
print(0 < x < 100) # True
# Truthiness — almost everything is truthy in Python
# FALSY values (evaluate to False in boolean context):
falsy_values = [0, 0.0, "", [], {}, set(), None, False]
for v in falsy_values:
print(f"bool({v!r:10}) = {bool(v)}")
# Output:
# bool(0) = False
# bool(0.0) = False
# bool('') = False
# bool([]) = False
# bool({}) = False
# bool(set()) = False
# bool(None) = False
# bool(False) = False
Type 5: None — The Absence of Value
None is Python's null value — a singleton object of type NoneType.
# None represents "no value" or "not yet set"
result = None
user = None
# Check for None with `is` (not ==)
print(result is None) # True ← correct
print(result == None) # True ← works but avoid this style
# Common use: function with optional return
def find_user(user_id):
users = {1: "Alice", 2: "Bob"}
return users.get(user_id) # returns None if not found
user = find_user(99)
if user is None:
print("User not found")
else:
print(f"Found: {user}")
# None in function arguments (default value pattern)
def greet(name=None):
if name is None:
name = "World"
print(f"Hello, {name}!")
greet() # Hello, World!
greet("Alice") # Hello, Alice!
Type Conversion (Casting)
# ─── Explicit Conversion ──────────────────────────────────────────
# str → int
num = int("42") # 42
num = int(" 42 ") # 42 (strips whitespace)
# int("3.14") # ValueError! Can't convert decimal string
num = int(float("3.14")) # 3 (go via float first)
# str → float
f = float("3.14") # 3.14
f = float("1e5") # 100000.0
# int → str
s = str(42) # "42"
s = str(3.14) # "3.14"
# int/float → bool
print(bool(0)) # False
print(bool(42)) # True
print(bool(0.0)) # False
print(bool(3.14)) # True
# bool → int
print(int(True)) # 1
print(int(False)) # 0
# ─── Common Conversion Pitfalls ───────────────────────────────────
# Pitfall 1: int("3.14") fails — must go through float
try:
x = int("3.14")
except ValueError as e:
print(f"Error: {e}") # invalid literal for int()
x = int(float("3.14")) # Correct: 3
# Pitfall 2: Floor division with floats
print(10 / 3) # 3.3333... (true division)
print(10 // 3) # 3 (floor division — integer result even for floats)
print(10.0 // 3) # 3.0 (floor division returns float if input is float)
# Pitfall 3: String number comparisons
print("10" > "9") # False! (lexicographic: "1" < "9")
print(10 > 9) # True (numeric comparison)
# Always convert strings to numbers before numeric comparison
Variable Naming Rules & Conventions
# ✅ VALID names
my_variable = 1
_private = 2
__dunder__ = 3
CamelCase = 4
name123 = 5
# ❌ INVALID names (SyntaxError)
# 1variable = 1 ← can't start with digit
# my-var = 2 ← hyphen is subtraction operator
# class = 3 ← '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', ...]
# ─── PEP 8 Naming Conventions ────────────────────────────────────
user_name = "alice" # snake_case for variables and functions
MAX_RETRIES = 3 # UPPER_CASE for constants
class UserProfile: # PascalCase for class names
pass
_internal = True # leading underscore = "private by convention"
__very_priv = True # double underscore = name mangling (advanced)
# Multiple assignment
x = y = z = 0 # all three equal 0
a, b, c = 1, 2, 3 # tuple unpacking
first, *rest = [1,2,3,4,5] # star unpacking: first=1, rest=[2,3,4,5]
# Swap variables (Pythonic — no temp variable needed)
x, y = 10, 20
x, y = y, x
print(x, y) # 20 10
Memory Model: How Python Actually Stores Variables
# In Python, variables are REFERENCES (pointers) to objects in memory
# Not boxes containing values — labels pointing to objects!
a = 42
b = a # b points to the SAME object as a
print(id(a) == id(b)) # True — same memory address!
# Immutable types (int, float, str, bool, None):
# Changing a variable makes it point to a NEW object
a = 100 # a now points to a different object
print(id(a) == id(b)) # False — a was rebound
# Python caches small integers (-5 to 256) for performance
x = 256
y = 256
print(x is y) # True (same cached object)
x = 257
y = 257
print(x is y) # False (not cached — different objects)
# ↑ This is why you should NEVER use `is` to compare values!
# Always use == for value equality, `is` only for None/True/False
# Check memory usage
import sys
print(sys.getsizeof(1)) # 28 bytes (int)
print(sys.getsizeof(1.0)) # 24 bytes (float)
print(sys.getsizeof("")) # 49 bytes (empty string)
print(sys.getsizeof("hello")) # 54 bytes (5 chars + overhead)
Real-World Exercise: Temperature Converter
Build a complete temperature conversion utility using what you've learned:
def convert_temperature(value, from_unit, to_unit):
"""
Convert temperature between Celsius, Fahrenheit, and Kelvin.
Args:
value (float): Temperature value to convert
from_unit (str): Source unit — 'C', 'F', or 'K'
to_unit (str): Target unit — 'C', 'F', or 'K'
Returns:
float: Converted temperature, rounded to 2 decimal places
Raises:
ValueError: If invalid unit or below absolute zero
"""
from_unit = from_unit.upper()
to_unit = to_unit.upper()
valid_units = {'C', 'F', 'K'}
if from_unit not in valid_units or to_unit not in valid_units:
raise ValueError(f"Unit must be one of {valid_units}")
# Step 1: Convert to Celsius (common intermediate)
if from_unit == 'C':
celsius = float(value)
elif from_unit == 'F':
celsius = (float(value) - 32) * 5 / 9
elif from_unit == 'K':
celsius = float(value) - 273.15
# Validate: nothing below absolute zero
if celsius < -273.15:
raise ValueError(f"Temperature {celsius}°C is below absolute zero!")
# Step 2: Convert from Celsius to target
if to_unit == 'C':
result = celsius
elif to_unit == 'F':
result = celsius * 9 / 5 + 32
elif to_unit == 'K':
result = celsius + 273.15
return round(result, 2)
# ─── Test all conversions ──────────────────────────────────────────
test_cases = [
(100, 'C', 'F', 212.0), # Boiling point of water
(32, 'F', 'C', 0.0), # Freezing point
(0, 'C', 'K', 273.15), # Absolute scale
(98.6, 'F', 'C', 37.0), # Body temperature
(300, 'K', 'C', 26.85), # Room temperature in Kelvin
(-40, 'C', 'F', -40.0), # The one point where C == F!
]
print(f"{'From':>8} {'To':>6} {'Input':>8} {'Result':>8} {'Expected':>10} {'Pass':>6}")
print("-" * 52)
for value, from_u, to_u, expected in test_cases:
result = convert_temperature(value, from_u, to_u)
passed = "✅" if abs(result - expected) < 0.01 else "❌"
print(f"{from_u:>8} {to_u:>6} {value:>8} {result:>8} {expected:>10} {passed:>6}")
Output:
From To Input Result Expected Pass
----------------------------------------------------
C F 100 212.0 212.0 ✅
F C 32 0.0 0.0 ✅
C K 0 273.15 273.15 ✅
F C 98.6 37.0 37.0 ✅
K C 300 26.85 26.85 ✅
C F -40 -40.0 -40.0 ✅
Common Mistakes and How to Avoid Them
# ❌ Mistake 1: Integer division returning float when you want int
result = 10 / 2 # 5.0 (float — might cause issues)
result = 10 // 2 # 5 ✅ (int — if you want integer division)
# ❌ Mistake 2: Comparing floats with ==
print(0.1 + 0.2 == 0.3) # False — floating point error!
import math
print(math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9)) # True ✅
# ❌ Mistake 3: Using = instead of ==
x = 5
if x == 5: # comparison ✅
print("five")
# if x = 5: # assignment — SyntaxError ✅ (Python catches this)
# ❌ Mistake 4: Checking None with ==
value = None
if value == None: # works but bad style
pass
if value is None: # ✅ Pythonic — use `is` for None
pass
# ❌ Mistake 5: Mutable default argument (advanced gotcha)
def append_to(item, lst=[]): # ← lst is shared across ALL calls!
lst.append(item)
return lst
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] ← unexpected! Not [2]
print(append_to(3)) # [1, 2, 3] ← the list persists between calls!
# Fix: use None as default, create fresh list inside
def append_to_fixed(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
Quick Reference Card
| Type | Literal | Mutable? | Example |
|---|---|---|---|
int | 42, -7, 0b101 | No | age = 28 |
float | 3.14, 1e-10 | No | price = 9.99 |
str | "hi", 'hi', """hi""" | No | name = "Alice" |
bool | True, False | No | active = True |
NoneType | None | No | result = None |
Key Takeaways
- Python is dynamically typed — no type declarations; Python infers type from the value
type(x)reveals any variable's type at runtime- Integers are unlimited in Python — no overflow errors like C/Java
- Never compare floats with
==— usemath.isclose()instead - f-strings (
f"Hello {name}") are the modern, preferred way to format strings - Use
isonly forNone,True, andFalse— use==for everything else - Variables are references, not boxes — they point to objects in memory
- Truthiness rules:
0,"",[],{},None,Falseare all falsy