Assignment in Python binds a name to an object; it does not copy the value. Writing a = [1, 2]; b = a gives you two names that point at the same list, so appending to b also changes a. This is the single most important thing to internalise about Python: variables are labels, not boxes. When you need an independent copy use list(a), a.copy(), or copy.deepcopy() for nested structures.
Assignment has three idiomatic variations. Chained assignment x = y = 0 binds several names to the same object at once. Tuple unpacking a, b = 1, 2 assigns a sequence to matching names in one step — and lets you swap with a, b = b, a, no temporary variable required. Starred unpacking first, *rest = [1, 2, 3, 4] captures the leftover items in a list, which is perfect for splitting a header from the rest of a row.
Comparison operators evaluate to booleans and can be chained mathematically. Python reads 0 <= x < 10 exactly like a maths book — it tests 0 <= x and x < 10 and returns True only if both hold, evaluating x only once. This is shorter and faster than 0 <= x and x < 10, and impossible to write incorrectly.
Equality (==) and identity (is) answer different questions. == asks "do these two objects look the same?" and calls the object's __eq__ method. is asks "are these two names pointing at the exact same object?". For the singletons None, True and False use is; for everything else use ==. Never compare floats with == — use math.isclose(a, b) with a tolerance that suits your domain.
Since Python 3.8, assignment expressions (the walrus operator :=) let you assign a value inside a larger expression. They pay off in while-loop headers (while (chunk := f.read(1024)):) and list comprehensions where an expensive computation would otherwise be repeated. Use them where they make the code clearer; otherwise stay with a plain assignment on its own line.
Augmented assignment and in-place mutation
The augmented forms (+=, -=, *=, etc.) call __iadd__ first, falling back to __add__. For immutable types this is identical to the plain form; for mutable types it modifies the object in place. That is why nums += [4] extends an existing list but nums = nums + [4] rebinds nums to a new list.
Chained comparisons and the walrus operator
Chained comparisons extend beyond numbers: "a" < word < "zz" checks alphabetical order; start <= index < stop is the canonical range check. The walrus operator is useful whenever you want the loop condition to depend on a freshly computed value without recomputing it inside the body.
The operators and helpers you will reach for every day.
| Tool | Purpose |
|---|---|
=operator | Binds a name to an object (no copy). |
+= -= *= //= **=operators | Augmented forms; may mutate mutable targets in place. |
a, b = seqtuple unpacking | Assigns a sequence to matching names. |
first, *rest = seqstarred unpacking | Captures the leftover elements into a list. |
== != < <= > >=operators | Value comparisons; can be chained. |
is / is notoperators | Identity test; prefer for None, True, False. |
math.isclose()function | Safe float equality with absolute/relative tolerance. |
:=walrus operator | Assigns inside an expression (while/for/list comp). |
Using Assignment and Comparison Operators code example
The example demonstrates unpacking, chained comparison, identity checks and the walrus operator.
# Lesson: Using Assignment and Comparison Operators
# Goal: compare the ways values can be bound, moved and tested in Python.
from math import isclose
def classify(score: int) -> str:
'''Return a grade using a chained comparison.'''
if 0 <= score < 50:
return "fail"
if 50 <= score < 75:
return "pass"
if 75 <= score <= 100:
return "distinction"
raise ValueError(f"score {score} out of range")
# chained and multiple assignment
low = high = best = 0
raw_scores = [91, 62, 48, 77, 85]
first, *middle, last = raw_scores
print("first=", first, "last=", last, "middle=", middle)
# tuple swap without a temporary variable
a, b = 1, 2
a, b = b, a
print("after swap: a=", a, "b=", b)
# walrus in a loop header
pending = list(raw_scores)
while (score := pending.pop() if pending else None) is not None:
print(f"{score} -> {classify(score)}")
# identity vs equality
empty = []
also_empty = []
print("empty == also_empty:", empty == also_empty) # True, same value
print("empty is also_empty:", empty is also_empty) # False, different objects
# safe float comparison
print("close:", isclose(0.1 + 0.2, 0.3, rel_tol=1e-9))
While reading, note where each style earns its keep:
1) Chained comparisons inside classify() read like a maths range check.
2) Starred unpacking keeps first/last separate without slicing.
3) The walrus condition reads the next value once per loop iteration.
4) `==` agrees on content; `is` disagrees because the lists are distinct objects.
Two snippets contrasting identity with equality and practising unpacking.
# Example A: mutable vs rebinding with += and =
lhs = [1, 2, 3]
reference = lhs
lhs += [4] # in-place -> reference sees the change
print(reference) # -> [1, 2, 3, 4]
lhs = lhs + [5] # rebinding -> reference unchanged
print(reference) # -> [1, 2, 3, 4]
# Example B: swap + nested unpacking
pair = ("Ada", (1815, 1852))
name, (born, died) = pair
print(f"{name} lived {died - born} years")
Core guarantees about identity, equality and chaining.
assert (None is None) and (None != False)
assert (0 <= 10 < 100) is True
assert [1, 2] == [1, 2] and [1, 2] is not [1, 2]
assert classify(82) == "distinction"
Running the script prints:
first= 91 last= 85 middle= [62, 48, 77]
after swap: a= 2 b= 1
85 -> distinction
77 -> distinction
48 -> fail
62 -> pass
91 -> distinction
empty == also_empty: True
empty is also_empty: False
close: True