Skip to main content
Variables, Types, and How Python Really Handles Values

Python Foundations

Variables, Types, and How Python Really Handles Values

Reading12 min read

Variables, Types, and How Python Really Handles Values

Most bugs that confuse Python beginners — and plenty that confuse experienced engineers — come from one misunderstanding: thinking a variable is a box that holds a value. In Python, a variable is a name that refers to an object. Get this mental model right and a whole category of "why did that change?!" bugs disappears.

Names Point at Objects

a = [1, 2, 3]
b = a          # b now refers to the SAME list object
b.append(4)
print(a)       # [1, 2, 3, 4]  ← a "changed" too

Nothing was copied. a and b are two labels on one list. You can prove it with is, which checks identity (same object), not equality (same value):

print(a is b)          # True
print(a == [1, 2, 3, 4])  # True — equal value
print(a is [1, 2, 3, 4])  # False — different object

If you actually want a separate list, copy it: b = a.copy() or b = list(a). For nested structures (a list of dicts, say) you need copy.deepcopy(a), because a shallow copy still shares the inner objects.

Mutable vs Immutable

This is where the model pays off:

Immutable (can't change in place)Mutable (can change in place)
int, float, bool, str, tuple, frozenset, Nonelist, dict, set, most custom objects

With immutable values, "changing" a variable really means pointing the name at a new object:

x = 10
y = x
x += 1         # creates a new int 11; x now points to it
print(y)       # 10 — y still points at the old object

So aliasing only bites with mutable objects. This is exactly why test fixtures that return a shared dict can leak state between tests: test 1 mutates the dict, test 2 sees the mutation.

The Core Built-in Types

count = 42                 # int — arbitrary precision, no overflow
price = 19.99              # float — binary floating point
name = "checkout"          # str — immutable text
passed = True              # bool — a subclass of int (True == 1)
nothing = None             # the "no value" singleton

Two gotchas worth knowing early:

Floats are approximate. 0.1 + 0.2 == 0.3 is False. In assertions on computed money or measurements, compare with a tolerance (math.isclose(a, b), or pytest.approx), or use decimal.Decimal for currency.

Check for None with is. Write if result is None:, not if result == None:. None is a singleton, and is can't be fooled by a class that overrides ==.

Dynamic Typing, Strong Typing

Python is dynamically typed: a name can point at an int now and a string later. It's also strongly typed: it won't silently convert between unrelated types.

total = "Items: " + 3      # TypeError — no silent conversion
total = "Items: " + str(3) # fine

That TypeError is a feature. JavaScript would happily produce "Items: 3" in some cases and nonsense in others; Python makes you say what you mean.

Truthiness

Every object has a truth value. Empty things are falsy: 0, 0.0, "", [], {}, set(), None. Everything else is truthy. That gives you idiomatic checks:

if not failures:
    print("All tests passed")

But be careful when 0 is a valid value: if not retry_count: treats "zero retries" the same as "not set". When the distinction matters, write if retry_count is None:.

Checking Types at Runtime

type(x) tells you the exact type; isinstance(x, (int, float)) is what you usually want, because it respects subclasses. You'll rarely need either in normal code — type hints (Lesson 13) catch most type mistakes before the code runs.

💬 Discussion

Have you ever had a test that passed alone but failed when run with others? Could shared mutable state have been the cause?

Q
Knowledge Check
1 / 3

After `a = {"env": "qa"}; b = a; b["env"] = "prod"`, what is `a["env"]`?

Next Lesson

Strings and f-Strings — the Data You Handle Most

Why Python Rules Test Automation — and Setting It Up Right