Keywords, operators, and exceptions reference
Python's 35 reserved keywords, the full operator set with precedence, and the built-in exception hierarchy.
Why this matters in AI / ML / GenAI
This is the lookup page. Which exception should I catch? What does the walrus operator do? Why is `is` wrong here? Keeping these straight is what separates code that fails loudly and correctly from code that swallows errors and corrupts a training run.
The 35 keywords
Reserved words that cannot be used as names:
False None True and as assert async await break class continue def del elif else except finally for from global if import in is lambda nonlocal not or pass raise return try while with yield
Ones that are frequently misunderstood:
pass— a do-nothing placeholder that keeps a block syntactically validdel— removes a name binding, not necessarily the objectassert— a debug check that is stripped when Python runs with -O, so never use it to validate user input or enforce securityglobal/nonlocal— rebind an outer name; usually a sign the design could be cleaneryield— turns a function into a generatorwith— guarantees cleanup through the context manager protocol
match and case (3.10+) are soft keywords: they work as pattern matching but are still usable as variable names.
Operators and precedence
Arithmetic: + - * / // (floor) % (modulo) ** (power). / always returns a float, even for 4 / 2.
Comparison: == != < > <= >=. These chain: 0 <= x <= 1 is valid and reads naturally.
Logical: and or not. They short-circuit and return an operand, not a boolean — "" or "default" gives "default".
Identity vs equality: is compares object identity, == compares value. Use is only with None, True, and False. x is 1000 may be False even when x == 1000, because small integers are cached and large ones are not.
Membership: in, not in.
Bitwise: & | ^ ~ << >>. In pandas and NumPy these are the element-wise boolean operators, and you must parenthesise: df[(df.a > 1) & (df.b < 2)].
Walrus := assigns inside an expression: while (line := f.readline()):.
Precedence, highest first: **, unary -, * / // %, + -, comparisons, not, and, or. When in doubt, add parentheses — clarity beats cleverness.
The exception hierarchy
Everything inherits from BaseException. Catch Exception, never BaseException, because the latter swallows KeyboardInterrupt and SystemExit and makes your program unkillable.
Common ones:
ValueError— right type, wrong value (int("abc"))TypeError— wrong type ("a" + 1)KeyError— missing dict keyIndexError— index out of rangeAttributeError— attribute does not existFileNotFoundError,PermissionError— subclasses ofOSErrorZeroDivisionError,StopIteration,ImportError,TimeoutError
KeyError and IndexError both inherit LookupError; catching the parent handles both.
Catch specific exceptions. A bare except: hides typos, keyboard interrupts, and real bugs, and it is the single worst habit in Python error handling.
Use raise ... from err to preserve the original cause when re-raising, and define your own class RetryableError(Exception) so callers can distinguish what is worth retrying — which matters a great deal when wrapping flaky LLM APIs.
Copy-paste examples
Copy into your own editor, or load one into the compiler below and press Run.
Keywords that surprise people
assert disappears under -O. Never use it for validation.
import keyword
print("total keywords:", len(keyword.kwlist))
print(keyword.kwlist)
print("\nsoft keywords:", keyword.softkwlist)
def placeholder():
pass # valid empty body
data = {"a": 1, "b": 2}
del data["a"]
print("\nafter del:", data)
x = 5
assert x > 0, "x must be positive" # stripped by python -O
print("assert passed (but do not rely on it in production)")
counter = 0
def increment():
global counter
counter += 1
increment(); increment()
print("global counter:", counter)
def countdown(n):
while n > 0:
yield n
n -= 1
print("generator:", list(countdown(3)))Operators, precedence, and the is trap
Run this — the identity results depend on integer caching.
print("7 / 2 =", 7 / 2, "(always float)")
print("7 // 2 =", 7 // 2, "| -7 // 2 =", -7 // 2, "(floors toward -inf)")
print("7 % 3 =", 7 % 3, "| -7 % 3 =", -7 % 3)
print("2 ** 10 =", 2 ** 10)
print("2 ** 3 ** 2 =", 2 ** 3 ** 2, "(** is right-associative)")
print("\nchained comparison: 0 <= 5 <= 10 ->", 0 <= 5 <= 10)
print("\nlogical operators return an operand, not a bool:")
print(" '' or 'default' ->", repr("" or "default"))
print(" 'a' and 'b' ->", repr("a" and "b"))
print(" 0 or [] ->", repr(0 or []))
a, b = 256, 256
c, d = 1000, 1000
print("\n256 is 256 ->", a is b, "(small ints are cached)")
print("1000 is 1000 ->", c is d, "(may be False — never rely on this)")
print("1000 == 1000 ->", c == d, "<- always use == for values")
print("\nuse 'is' only with None/True/False:", None is None)
print("\nbitwise: 12 & 10 =", 12 & 10, "| 12 | 10 =", 12 | 10, "| 1 << 4 =", 1 << 4)The walrus operator and match
Both reduce repetition in read-then-check patterns.
values = [3, 14, 7, 22, 5]
if (count := len(values)) > 3:
print(f"{count} values — assigned and tested in one expression")
filtered = [y for x in values if (y := x * 2) > 10]
print("walrus in a comprehension:", filtered)
def classify(event):
match event:
case {"type": "error", "code": code} if code >= 500:
return f"server error {code}"
case {"type": "error", "code": code}:
return f"client error {code}"
case {"type": "ok", "latency": latency} if latency > 1000:
return "slow success"
case {"type": "ok"}:
return "success"
case _:
return "unknown"
for event in [
{"type": "error", "code": 503},
{"type": "error", "code": 404},
{"type": "ok", "latency": 4200},
{"type": "ok", "latency": 120},
{"type": "weird"},
]:
print(f" {str(event):<40} -> {classify(event)}")Catching the right exception
Specific handlers, LookupError for both Key and Index, and the else clause.
def safe_parse(raw, data, index):
try:
number = int(raw)
value = data[index]
result = number / value
except ValueError as err:
return f"ValueError: {err}"
except LookupError as err: # covers KeyError and IndexError
return f"LookupError: {err!r}"
except ZeroDivisionError:
return "ZeroDivisionError: divisor was zero"
except Exception as err: # last resort, never bare except
return f"unexpected {type(err).__name__}: {err}"
else:
return f"ok: {result:.3f}" # runs only when nothing raised
finally:
pass # cleanup always runs
cases = [("10", [2, 5], 0), ("abc", [2], 0), ("10", [2], 9), ("10", [0], 0)]
for raw, data, index in cases:
print(f"{str((raw, data, index)):<22} -> {safe_parse(raw, data, index)}")
print("\nhierarchy check:")
for exc in [KeyError, IndexError, FileNotFoundError, ZeroDivisionError]:
parents = [c.__name__ for c in exc.__mro__[1:4]]
print(f" {exc.__name__:<20} -> {' -> '.join(parents)}")Custom exceptions and raise from
Signalling what is retryable is essential when wrapping flaky APIs.
class LLMError(Exception):
"""Base class so callers can catch everything from this client."""
class RetryableError(LLMError):
"""Transient: rate limits, timeouts, 5xx."""
class FatalError(LLMError):
"""Do not retry: bad key, malformed request."""
def call_model(status):
try:
if status == 429:
raise TimeoutError("rate limited")
if status == 401:
raise PermissionError("invalid api key")
return {"ok": True}
except TimeoutError as err:
raise RetryableError(f"retry after backoff (status {status})") from err
except PermissionError as err:
raise FatalError(f"fix your credentials (status {status})") from err
for status in [200, 429, 401]:
try:
print(f"status {status}: {call_model(status)}")
except RetryableError as err:
print(f"status {status}: RETRY -> {err} | caused by {type(err.__cause__).__name__}")
except FatalError as err:
print(f"status {status}: ABORT -> {err} | caused by {type(err.__cause__).__name__}")
print("\nboth subclass LLMError:", issubclass(RetryableError, LLMError))Write a retry loop that respects exception types
Try it — in-browser Python
Change the failure list so the first attempt succeeds, or so all three fail.
Output
Python runs in your browser. First run downloads the runtime.
Press Run (or Ctrl+Enter) to execute.
CPython in WebAssembly. Stdlib works. NumPy, pandas, scikit-learn and Matplotlib load on demand, and charts render below. No input(), no GPU, no network installs.
Takeaways
- Use `is` only with None/True/False; `==` compares values and is what you almost always want.
- Never use assert for validation — it vanishes under python -O.
- Catch specific exceptions, use `raise ... from err`, and define custom types to mark what is retryable.