Using Assignment and Comparison Operators

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.

ToolPurpose
=
operator
Binds a name to an object (no copy).
+= -= *= //= **=
operators
Augmented forms; may mutate mutable targets in place.
a, b = seq
tuple unpacking
Assigns a sequence to matching names.
first, *rest = seq
starred unpacking
Captures the leftover elements into a list.
== != < <= > >=
operators
Value comparisons; can be chained.
is / is not
operators
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