Intermediate18 min

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 valid
  • del — removes a name binding, not necessarily the object
  • assert — a debug check that is stripped when Python runs with -O, so never use it to validate user input or enforce security
  • global / nonlocal — rebind an outer name; usually a sign the design could be cleaner
  • yield — turns a function into a generator
  • with — 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 key
  • IndexError — index out of range
  • AttributeError — attribute does not exist
  • FileNotFoundError, PermissionError — subclasses of OSError
  • ZeroDivisionError, 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.