In Python, a variable is a name bound to an object — every value is an instance of some class, types are determined at runtime (dynamic typing), and the language enforces type compatibility without silent coercion (strong typing).
Start here — no prior experience needed
What you'll learn in this tab: How to create variables, what Python's 12 built-in types are, and how to choose between them. By the end you'll be able to write the first few lines of any Python program — storing names, numbers, and collections of data.
How to read this tab: Read each section once, then look at the code example and predict what each line does before reading the comments. If you don't understand a line, that's fine — move on and come back. This is your first pass, not a test.
A variable is a label on a box. Python doesn't care what's in the box — you can swap the contents for anything. The label just tells you where to find it.
What this section is: The core mechanics of creating and naming variables in Python. Focus on the naming conventions at the bottom — Python's style is lowercase with underscores (user_score, not userScore). This matters from day one because everyone who reads your code expects it.
Variables
In Python, you create a variable by writing a name, an equals sign, and a value. No type declaration needed — Python figures out the type from the value you assign. You can reassign a variable to a completely different type at any time.
# Create a variable — no declaration keyword
name = "Priya" # str
age = 28 # int
height = 5.4 # float
is_active = True # bool
nothing = None # NoneType
# Reassign — Python allows changing type
age = "twenty-eight" # now a str — perfectly valid
# Multiple assignment
x, y, z = 1, 2, 3 # tuple unpacking
a = b = c = 0 # same value, three names
# Check the type
print(type(name)) # <class 'str'>
print(type(age)) # <class 'int'> (before reassignment)
# Variable names: lowercase, underscores, descriptive
user_score = 95 # good
userScore = 95 # camelCase — works but not Python style
GRAVITY = 9.8 # UPPERCASE = conventional constantThis is the most important table on this page. Don't memorise all 12 types right now — focus on the Mutable? column. The types marked Yes (list, dict, set) behave differently when you pass them to functions. You'll feel why this matters in the next few pages. For now, just notice which are mutable and which aren't.
The built-in types
| Type | Example | Mutable? | Notes |
|---|---|---|---|
int | 42, -7, 0 | No | Arbitrary precision — no overflow |
float | 3.14, 1e10 | No | IEEE 754 double precision |
complex | 2+3j | No | Built-in complex numbers |
bool | True, False | No | Subclass of int: True==1, False==0 |
str | "hello", 'world' | No | Unicode text, immutable |
bytes | b"data" | No | Raw binary data |
list | [1, 2, 3] | Yes | Ordered, indexed, heterogeneous |
tuple | (1, 2, 3) | No | Immutable list — use for fixed data |
dict | {"a": 1} | Yes | Key-value, insertion-ordered (3.7+) |
set | {1, 2, 3} | Yes | Unordered, unique values |
frozenset | frozenset({1,2}) | No | Immutable set |
NoneType | None | No | The absence of a value |
# Numbers
x = 42 # int
y = 3.14 # float
z = 2 + 3j # complex
# Python ints have no overflow
big = 10 ** 100 # a googol — works fine
# Strings — single or double quotes, same thing
s1 = "hello"
s2 = 'hello'
s3 = """Multi
line string"""
# f-strings (Python 3.6+) — the preferred way
name = "Priya"
msg = f"Hello, {name}! Age: {age}"
# Collections
fruits = ["mango", "apple", "banana"] # list — mutable
point = (10, 20) # tuple — immutable
scores = {"Priya": 95, "Rahul": 88} # dict
unique = {1, 2, 3, 2, 1} # set — {1, 2, 3}
# None — Python's null equivalent
result = None
print(result is None) # True — use `is`, not ==print(type(42)), then print(type("hello")), then print(type([1,2,3])). The output shows you the type of each value in real time.
✅ Beginner tab complete — check your understanding
- I can create a variable with
name = valueand reassign it to a different type - I know the difference between a
list(mutable) and atuple(immutable) - I know the difference between a
dict(key-value pairs) and aset(unique values) - I know that
Nonerepresents "no value" and that I check it withis None - I know Python variable names use lowercase_underscores, not camelCase
The concepts that make Python click
What you'll learn in this tab: Why Python's type system is both flexible and strict. How mutability actually works in memory — and why it produces the bugs that trip up every beginner. The difference between a variable as a "name" and a variable as a "box". And how to add type hints to your code so editors can catch mistakes before you run it.
Recommended for: Read this tab after you've done 2–3 Beginner lessons and written some code. The mutability section especially makes much more sense once you've passed a list to a function and been surprised by the result.
What this section is: The terminology behind Python's type system. Two terms matter here: dynamically typed (types are checked when code runs, not before) and strongly typed (Python won't secretly convert one type to another). You'll use these terms when talking to other developers and when reading Python documentation.
How Python's type system works
Python is dynamically typed — types are checked at runtime, not compile time. Python is also strongly typed — it will not silently coerce incompatible types. "5" + 3 raises a TypeError in Python (unlike JavaScript where it gives "53"). Every object in Python carries its type at runtime — you can always check with type() or isinstance().
What this section is: An introduction to type hints — optional annotations that tell tools (and humans) what type a variable should be. Important: Python does not enforce these at runtime. They are purely for editors, static analysis tools like mypy, and documentation. You don't need to use them immediately, but you'll see them everywhere in professional code.
Type hints (PEP 484, Python 3.5+)
Python 3.5 added optional type annotations. They are not enforced at runtime but are used by static analysis tools (mypy, pyright, Pyrefly) and IDEs. The Python interpreter completely ignores them at runtime — x: int = "hello" runs without error.
from typing import Optional, Union
# Function with type hints
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}! " * times).strip()
# Variable annotations
score: int = 95
label: str
# Optional — the value may be None
def find_user(id: int) -> Optional[str]:
if id == 1: return "Priya"
return None
# Union — one of several types (Python 3.10+ shorthand: str | int)
def process(value: Union[str, int]) -> str:
return str(value)
# Modern syntax (3.10+)
def divide(a: float, b: float) -> float | None:
return a / b if b != 0 else None
# Type annotations don't change runtime behaviour
x: int = "hello" # No error at runtime!
print(type(x)) # <class 'str'>This is the most important section on this page. Mutability is the concept that causes the most unexpected bugs for Python beginners. Read this section slowly. Run the code examples — especially the mutable default argument gotcha at the bottom. If you only understand one thing from this entire page, make it this: when you write b = a where a is a list, both names point to the same list.
Mutability in depth
Mutability is one of Python's most important concepts. Immutable objects (int, str, tuple, frozenset) cannot be changed after creation — what looks like "changing a string" always creates a new string. Mutable objects (list, dict, set) can be modified in-place. This matters enormously when passing objects to functions and when using objects as dictionary keys (only immutable objects are hashable and can be keys).
# Immutable — "changing" creates a new object
s = "hello"
id_before = id(s)
s = s + " world" # new string object
print(id(s) == id_before) # False — different object
# Mutable — modified in place
lst = [1, 2, 3]
id_before = id(lst)
lst.append(4)
print(id(lst) == id_before) # True — same object
# Tuple vs list — same data, different mutability
point_list = [10, 20] # mutable
point_tuple = (10, 20) # immutable
# Only immutable objects can be dict keys / set members
d = {(1, 2): "a point"} # tuple key — OK
# d = {[1, 2]: "a point"} # TypeError: list is unhashable
# Gotcha: mutable default argument
def add_item(item, lst=[]): # DON'T do this
lst.append(item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] — shared list!
def add_item_safe(item, lst=None): # correct pattern
if lst is None: lst = []
lst.append(item)
return lstWhat this section is: A more precise model of what a variable actually is in Python — a name bound to an object, not a box containing a value. This distinction explains why a = b on a list means they share the same object. The is vs == distinction at the end is essential: always use == to compare values, and is only when checking for None.
Names, objects, and identity
In Python, variables are names bound to objects — not boxes containing values. When you write a = b, you bind the name a to the same object that b points to. For mutable objects, this means both a and b can see mutations. is checks identity (same object), == checks equality (same value).
a = [1, 2, 3]
b = a # b and a point to the SAME list
b.append(4)
print(a) # [1, 2, 3, 4] — a sees the change!
c = a.copy() # c is a new list with the same values
c.append(5)
print(a) # [1, 2, 3, 4] — a unchanged
# Identity vs equality
x = [1, 2, 3]
y = [1, 2, 3]
print(x == y) # True — same values
print(x is y) # False — different objects
# Small int caching: CPython caches -5 to 256
a = 256; b = 256
print(a is b) # True — same cached object
a = 257; b = 257
print(a is b) # False — not cached (implementation detail)"5" + 3 to "53". Python raises TypeError.is is not ==. is checks object identity (same memory address). == checks equality (same value). Always use == for value comparison. Use is only for None, True, False, and explicit identity checks.def f(x: int) -> str: return x will not raise an error at runtime even if you pass a string and return an int. Type hints are documentation + static analysis only.✅ Intermediate tab complete — check your understanding
- I can explain the difference between dynamically typed and strongly typed
- I understand why
b = aon a list means both names see the same mutations - I know never to use a mutable object (like
[]or{}) as a default argument - I know to use
==for value comparison andisonly forNone - I understand what a type hint like
def greet(name: str) -> strmeans, even if I'm not using them yet
Under the hood — CPython's object model
What you'll learn in this tab: How Python's object model works at the CPython implementation level — why id() returns a memory address, how reference counting decides when memory is freed, the complete type class hierarchy, and the implementation details behind integer and string interning.
When to read this tab: When you're genuinely curious about why Python behaves the way it does — not just how to use it. This content will not help you write better programs immediately, but it will help you understand error messages, debug unexpected behaviour, and speak credibly with experienced engineers. Read it after you've finished Stage 2 of the course.
What this section is: The formal definition of what an "object" is in Python's data model. Every value has three things: an identity (id()), a type (type()), and a value. Understanding this makes is, reference counting, and garbage collection make intuitive sense. The key insight: id() in CPython returns the memory address — which is why is is an identity check, not an equality check.
Python's data model: everything is an object
In Python, every value is an instance of some class — integers, strings, functions, classes themselves, and even None. The Python data model (Python Language Reference §3) defines the abstract base for all objects: every object has an identity (id() returns the memory address in CPython), a type (type()), and a value. The id() of an object is unique and constant for its lifetime. CPython's implementation uses reference counting: each object stores how many names point to it. When the count reaches zero, the object is freed.
What this section is: The complete Python type hierarchy — who inherits from whom. The key fact: bool is a subclass of int (which is why True + True == 2 in Python). And type is its own metaclass — type(type) is type is True. You don't need to memorise the hierarchy, but knowing it exists helps when you encounter isinstance() checks in production code.
The type hierarchy
Python's type hierarchy (Python Language Reference §3.2): None · NotImplemented · Ellipsis → numbers (int → bool, complex, float) → sequences (immutable: str, bytes, tuple; mutable: list, bytearray) → set types (frozenset, set) → mappings (dict) → callables (functions, methods, classes) → modules → types. All types are instances of type; type itself is an instance of type (the metaclass of everything).
What this section is — and a warning. CPython (the standard Python implementation) caches small integers and certain strings as an optimisation. This is an implementation detail, not part of the Python language specification. Never write code that relies on this. Code that uses is to compare integers or strings may work in CPython but fail in PyPy, Jython, or other implementations. The only safe uses of is are: is None, is True, is False.
Integer interning and string interning
CPython interns (caches) small integers from -5 to 256 — all accesses to these integers reuse the same object. CPython also interns string literals that look like identifiers (alphanumeric, starting with letter or underscore) at compile time. This is an implementation detail of CPython, not guaranteed by the Python language specification. Code that relies on is for integers or strings is implementation-specific and may break in PyPy or other implementations.
✅ Expert tab complete
- I can explain why
id(x)returns a memory address in CPython - I know that
boolis a subclass ofint, which is whyTrue == 1 - I understand reference counting — when no names point to an object, it is freed
- I know never to rely on integer or string interning in production code