Variables & Types
Python is dynamically typed, yet types still rule everything. Learn what a variable really is — a name, not a box — the core built-in types, and how to inspect them.
What you'll learn
- The difference between a variable and the value it points to
- The six built-in types you will use almost all of the time
- How to inspect a value's type with type() and isinstance()
- Why 'is' and '==' ask two different questions
Before you start
It is tempting to picture a variable as a little box with a value tucked inside it. That picture is comforting, and in Python it is wrong — and the wrongness explains a whole family of surprises that catch people arriving from other languages. A variable here is not a box. It is a name, and the name points at an object that lives somewhere in memory.
That one shift — from boxes holding values to names pointing at objects — is worth slowing down for, because once you see it, behaviour that looked like a bug turns out to be the rule working exactly as designed.
A name points at an object
Watch what happens when we bind a second name to the same list and then change the list through that second name:
x = [1, 2, 3]
y = x # y points at the SAME list as x
y.append(4)
print("x =", x)
print("y =", y)
print("same object?", x is y)
x = [1, 2, 3, 4]
y = [1, 2, 3, 4]
same object? True
Notice that appending through y changed what you see through x. If a variable were a box, that could not happen — x would have its own copy. But = never copies; it only binds another name to the object already there. So x and y are two names for one list, and x is y confirms it. To get a genuinely separate list you have to ask for one, with y = x.copy() or y = list(x).
y = x binds a second name to the same list — it never copies. Mutating through either name changes the one object.
The core built-in types
A handful of types carry the vast majority of real Python. These six show up everywhere:
| Type | Example | Mutable? |
|---|---|---|
int | 42, -1000 | no |
float | 3.14, 1e-9 | no |
str | "hello" | no |
bool | True, False | no |
list | [1, 2, 3] | yes |
dict | {"a": 1} | yes |
You will also soon meet tuple ((1, 2), an immutable list), set ({1, 2}, unordered with unique elements), and None, the single object that stands for “no value at all.” You can always ask a value what it is:
n = 42
pi = 3.14159
greeting = "hello"
ok = True
items = [10, 20, 30]
user = {"name": "Aarav", "age": 28}
for value in (n, pi, greeting, ok, items, user):
print(f"{type(value).__name__:<6} {value!r}")
int 42
float 3.14159
str 'hello'
bool True
list [10, 20, 30]
dict {'name': 'Aarav', 'age': 28}
Two small tools earned their place there. type(value).__name__ gives the type’s name as a plain string, and the !r inside the f-string calls repr() — the unambiguous representation, which is why 'hello' prints with its quotes. When you are inspecting values, repr tells you what something really is rather than how it prints.
Dynamically typed, but every value has a type
Python is dynamically typed: you never declare what type a variable holds, and the same name may point at an int now and a str a line later. But do not mistake that for “types don’t matter.” Every value has a firm type, and operations only run on compatible ones — the check simply happens when the line runs, not before:
x = 10
print(type(x))
x = "now I'm a string"
print(type(x))
result = "5" + 5
<class 'int'>
<class 'str'>
Traceback (most recent call last):
...
TypeError: can only concatenate str (not "int") to str
The name x happily changed type; what failed was asking to add a string and an integer. Because the error surfaces only at runtime, larger codebases add type hints — annotations that let mypy or pyright catch exactly this mistake before the program runs. That is a later lesson all its own.
Converting between types
When data crosses into your program from outside — a web form, a file, an API — it usually arrives as text, and you convert it deliberately:
age = int("28") # str -> int
price = float("9.99") # str -> float
flag = bool("") # "" is False; ANY non-empty string is True
text = str(42) # int -> str
print(age, price, flag, text)
int("twenty-eight") # cannot be parsed as a number
28 9.99 False 42
Traceback (most recent call last):
...
ValueError: invalid literal for int() with base 10: 'twenty-eight'
Pause and predict
Before the last section, try this in your head: what does isinstance(True, int) return — and why might it surprise you? Hold a guess, then read on.
isinstance, not type ==
Prefer isinstance(x, T) over type(x) == T, because isinstance respects inheritance — and that matters the moment you start using classes:
isinstance(True, int) # True — bool is a subclass of int
type(True) == int # False — exact type only
There is the answer to the prediction: True really is an int underneath (Python’s bool is a subclass of int), so isinstance says yes while the stricter type(...) == says no. Reaching for isinstance keeps your checks working as your types grow.
In one breath
- A variable is a name that points at an object;
=binds, it never copies. ==compares values;iscompares identity — keepisforNone.- Six types do most of the work:
int,float,str,bool(immutable) andlist,dict(mutable). - Python is dynamically typed, but every value has a type and type errors surface at runtime.
- Convert with
int(),float(),str(),bool()— and remember any non-empty string is truthy.
Practice
Quick check
What’s next
You know what a variable is and the types it can point at. Next we look hard at numbers — where 0.1 + 0.2 famously does not equal 0.3, and why that is mathematics, not a Python bug.
Practice this in an interview
All questions`==` calls `__eq__` and tests value equality. `is` tests object identity — whether two names point to the exact same object in memory. The surprise comes from CPython's interning: small integers (-5 to 256) and many short strings are cached, so `is` returns True even for separately created objects, but this is an implementation detail you must never rely on.
Immutable types — int, float, bool, str, bytes, tuple, frozenset — cannot be changed after creation; operations return new objects. Mutable types — list, dict, set, bytearray — can be changed in place. Mutability determines hashability (only immutables can be dict keys/set members), function side-effect behaviour, and thread-safety considerations.
Default argument values are evaluated once when the function is defined, not each time it is called. If the default is a mutable object like a list or dict, all calls that use the default share the same object — so mutations in one call persist into the next. The fix is to use None as the default and create the mutable object inside the function body.
Python variables are names — they store references (pointers) to objects, not the objects themselves. Assignment binds a name to an object; it never copies the object. Understanding this explains why mutating an object through one name is visible through all other names that reference the same object.