Skip to content

Add perchance, a soft keyword for exception-tolerant expressions (PoC)#153767

Closed
ambv wants to merge 2 commits into
python:mainfrom
ambv:perchance
Closed

Add perchance, a soft keyword for exception-tolerant expressions (PoC)#153767
ambv wants to merge 2 commits into
python:mainfrom
ambv:perchance

Conversation

@ambv

@ambv ambv commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

This introduces an expression-level fallback for exceptions — the thing dict.get() does for one specific case, generalized to any fallible expression:

port = int(os.environ["PORT"]) perchance 8080

evaluates the left operand and, if it raises an exception matching the guard (default: Exception, never BaseException), yields the fallback instead.

An explicit guard is spelled

first = seq[0] perchance None from IndexError

accepting anything an except clause accepts.

The fallback and guard are lazily evaluated only when an exception occurs, so it short-circuits like or but over exceptions rather than falsiness, and chains left-associatively: d["a"] perchance d["b"] perchance "default". This is PEP 463 (exception-catching expressions) with the syntactic objections fixed: no colon inside the expression, no parentheses required, and it reads as English at exactly the point where the program acknowledges chance. The killer use case is comprehensions, where a fallible step today forces a named helper function or an inner try statement: [int(line) perchance None from ValueError for line in text.splitlines()].

There is also a statement form:

perchance FileNotFoundError, PermissionError: os.unlink(tmp)

which is sugar for try:/except (...): pass — the pattern the stdlib alone spells out ~1360 times. The exception list is mandatory: there is no bare perchance:, so swallowing everything requires writing perchance Exception: and owning it in review.

Design decisions worth noting: the exception object is not bindable (no as) — if you need it, you need a try statement; this is the lambda principle, keeping the expression form too small to hide logic in. The default guard is loaded via a new LOAD_COMMON_CONSTANT slot (CONSTANT_EXCEPTION) rather than a name lookup, so a local Exception = ... cannot change what gets caught. A swallowed exception rides along as __context__ if the fallback itself raises. Mixing with a conditional expression requires parentheses. perchance is a soft keyword like match: juxtaposed NAME NAME is a syntax error today, so no existing code changes meaning, and perchance remains usable as an identifier — x = perchance perchance 0 parses and means what you'd hope.

Implementation: new Perchance(expr value, expr fallback, expr? guard) node in Python.asdl; a left-recursive perchance_expression grammar rule next to if_expression; the statement form is desugared to Try directly in the parser action, so it needs no new statement node, symtable, or codegen support. Code generation mirrors codegen_try_except and uses the zero-cost exception tables: the happy path executes no extra instructions, with the handler reachable only through the exception table. Both forms are gated with CHECK_VERSION(16). Known wart for a real version: in raise f() perchance g() from cause, the from binds to the perchance guard, not the raise cause — parenthesize to disambiguate; dedicated invalid-syntax rules with helpful errors are left for later, as are docs and tests.

To sleep, perchance to dream.

To be clear, this is a joke. Please don't close this before Pablo reviews it.

This introduces an expression-level fallback for exceptions — the thing `dict.get()` does for one specific case, generalized to any fallible expression: `port = int(os.environ["PORT"]) perchance 8080` evaluates the left operand and, if it raises an exception matching the guard (default: `Exception`, never `BaseException`), yields the fallback instead. An explicit guard is spelled `first = seq[0] perchance None from IndexError`, accepting anything an `except` clause accepts. The fallback and guard are lazily evaluated only when an exception occurs, so it short-circuits like `or` but over exceptions rather than falsiness, and chains left-associatively: `d["a"] perchance d["b"] perchance "default"`. This is PEP 463 (exception-catching expressions) with the syntactic objections fixed: no colon inside the expression, no parentheses required, and it reads as English at exactly the point where the program acknowledges chance. The killer use case is comprehensions, where a fallible step today forces a named helper function or an inner try statement: `[int(line) perchance None from ValueError for line in text.splitlines()]`.

There is also a statement form, `perchance FileNotFoundError, PermissionError: os.unlink(tmp)`, which is sugar for `try:`/`except (...): pass` — the pattern the stdlib alone spells out ~1360 times. The exception list is mandatory: there is no bare `perchance:`, so swallowing everything requires writing `perchance Exception:` and owning it in review.

Design decisions worth noting: the exception object is not bindable (no `as`) — if you need it, you need a try statement; this is the lambda principle, keeping the expression form too small to hide logic in. The default guard is loaded via a new `LOAD_COMMON_CONSTANT` slot (`CONSTANT_EXCEPTION`) rather than a name lookup, so a local `Exception = ...` cannot change what gets caught. A swallowed exception rides along as `__context__` if the fallback itself raises. Mixing with a conditional expression requires parentheses. `perchance` is a soft keyword like `match`: juxtaposed NAME NAME is a syntax error today, so no existing code changes meaning, and `perchance` remains usable as an identifier — `x = perchance perchance 0` parses and means what you'd hope.

Implementation: new `Perchance(expr value, expr fallback, expr? guard)` node in Python.asdl; a left-recursive `perchance_expression` grammar rule next to `if_expression`; the statement form is desugared to `Try` directly in the parser action, so it needs no new statement node, symtable, or codegen support. Code generation mirrors `codegen_try_except` and uses the zero-cost exception tables: the happy path executes no extra instructions, with the handler reachable only through the exception table. Both forms are gated with `CHECK_VERSION(16)`. Known wart for a real version: in `raise f() perchance g() from cause`, the `from` binds to the perchance guard, not the raise cause — parenthesize to disambiguate; dedicated invalid-syntax rules with helpful errors are left for later, as are docs and tests.

To sleep, perchance to dream.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JelleZijlstra

Copy link
Copy Markdown
Member

Perchance not

The REPL highlights soft keywords only in contexts recognized by `is_soft_keyword_used()` in Lib/_pyrepl/utils.py, which knew about `match`, `case`, `type`, and `lazy` but not `perchance`. Add patterns for both forms: the expression form highlights when the previous token can end an expression (name, number, string, closing bracket, `...`, or a constant keyword) and the next token can start one, via a new `_starts_expression()` helper; the statement form highlights at line start. Identifier uses (`perchance = 5`, `f(perchance=3)`, `perchance.method()`) stay uncolored, and in `x = perchance perchance 0` only the second occurrence — the operator — is highlighted.

Also relax the `bracket_level == 0` gate for `perchance` alone: the other soft keywords can only start statements and thus cannot legally appear inside brackets, but the perchance expression form is most at home there, e.g. `[int(v) perchance None for v in vs]`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ambv ambv added skip issue skip news pending The issue will be closed if no feedback is provided labels Jul 15, 2026
@ThiefMaster

Copy link
Copy Markdown

There can only be one answer from whoever ends up reviewing this!

image

@zy1o

zy1o commented Jul 15, 2026

Copy link
Copy Markdown

Why not?
image

@ZeroIntensity ZeroIntensity left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍👍

@pablogsal

Copy link
Copy Markdown
Member

Rejected

@python python locked as too heated and limited conversation to collaborators Jul 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

awaiting merge DO-NOT-MERGE javascript Pull requests that update javascript code pending The issue will be closed if no feedback is provided skip issue skip news

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants