Conditions
and and or do not return True and False — they return one of their operands. a or "default" is a common idiom, and it is also a bug when a is legitimately 0 or "".
Python has no switch; since 3.10 it has match, which is pattern matching rather than a switch and earns its place on structures, not on single values.
if score >= 90:
grade = "A"
elif score >= 80: # elif, not "else if"
grade = "B"
else:
grade = "C"
grade = "A" if score >= 90 else "B" # the conditional expression
if 0 <= score <= 100: # chained comparison — really means both
...
if name and name.strip(): # and/or short-circuit, and return an operand
...
Loop over the thing, not over an index
range is lazy: range(10**9) allocates nothing. zip stops at the shortest input, which silently loses data — strict=True turns that into an error, and you almost always want it.
for word in words: # not for i in range(len(words))
print(word)
for i, word in enumerate(words): # when you genuinely need the index
print(i, word)
for i, word in enumerate(words, start=1):
print(f"{i}. {word}")
for name, age in zip(names, ages): # two sequences in step
...
for name, age in zip(names, ages, strict=True): # 3.10+: raise if lengths differ
...
for i in range(5): # 0 1 2 3 4
for i in range(2, 10, 3): # 2 5 8
for _ in range(3): # "do this three times"
break, continue, and the else nobody knows
for ... else runs its else when the loop finished normally. It reads badly — think of it as "no break" — but it is the clean way to write a search: do the work in the loop, and handle "not found" in the else without a flag variable.
for item in items:
if item.broken:
break # leave the loop
if item.skip:
continue # next iteration
else:
print("finished without breaking") # runs only if break did NOT happen
while, and the walrus
:= (the walrus) exists so that a value you test is also a value you keep, without repeating the call or hoisting it out of the condition. Use it in a while head and in a comprehension filter; do not scatter it.
while queue: # an empty container is false: no len() needed
job = queue.pop()
handle(job)
while True:
line = read()
if not line:
break
handle(line)
while (line := read()): # := assigns and evaluates, in one place
handle(line)
match, for structure
match destructures as it matches, binds the names on the way and supports a guard. For one value against five constants, a dict lookup is still shorter and clearer.
match event:
case {"type": "click", "x": x, "y": y}:
handle_click(x, y)
case {"type": "key", "key": k} if k.isalpha():
type_letter(k)
case [first, *rest]:
handle_list(first, rest)
case _:
ignore()