PEP: 758 Title: Allow except and except* expressions without parentheses
Author: Pablo Galindo <pablogsal@python.org>, Brett Cannon
<brett@python.org> PEP-Delegate: TBD Status: Draft Type: Standards Track
Created: 30-Sep-2024 Python-Version: 3.14 Post-History: 02-Oct-2024

Abstract

This PEP[1] proposes to allow unparenthesized except and except* blocks
in Python's exception handling syntax. Currently, when catching multiple
exceptions, parentheses are required around the exception types. This
was a Python 2 remnant. This PEP suggests allowing the omission of these
parentheses, simplifying the syntax, making it more consistent with
other parts of the syntax that make parentheses optional, and improving
readability in certain cases.

Motivation

The current syntax for catching multiple exceptions requires parentheses
in the except expression (equivalently for the except* expression). For
example:

    try:
        ...
    except (ExceptionA, ExceptionB, ExceptionC):
        ...

While this syntax is clear and unambiguous, it can be seen as
unnecessarily verbose in some cases, especially when catching a large
number of exceptions. By allowing the omission of parentheses, we can
simplify the syntax:

    try:
        ...
    except ExceptionA, ExceptionB, ExceptionC:
        ...

This change would bring the syntax more in line with other
comma-separated lists in Python, such as function arguments, generator
expressions inside of a function call, and tuple literals, where
parentheses are optional.

The same change would apply to except* expressions. For example:

    try:
        ...
    except* ExceptionA, ExceptionB, ExceptionC:
        ...

Both forms will also allow the use of the as clause to capture the
exception instance as before:

    try:
        ...
    except ExceptionA, ExceptionB, ExceptionC as e:
        ...

Rationale

The decision to allow unparenthesized except blocks is based on the
following considerations:

1.  Simplicity: Removing the requirement for parentheses simplifies the
    syntax, making it more consistent with other parts of the language.
2.  Readability: In cases where many exceptions are being caught, the
    removal of parentheses can improve readability by reducing visual
    clutter.
3.  Consistency: This change makes the except clause more consistent
    with other parts of Python where unambiguous, comma-separated lists
    don't require parentheses.

Specification

The syntax for the except clause will be modified to allow an
unparenthesized list of exception types. The grammar will be updated as
follows:

    except_block:
        | 'except' expressions ['as' NAME] ':' block
        | 'except' ':' block

    except_star_block
        | 'except' '*' expressions ['as' NAME] ':' block 

This allows both the current parenthesized syntax and the new
unparenthesized syntax:

    try:
        ...
    except (ExceptionA, ExceptionB):  # Still valid
        ...
    except ExceptionC, ExceptionD:    # New syntax
        ...

The semantics of exception handling remain unchanged. The interpreter
will catch any of the listed exceptions, regardless of whether they are
parenthesized or not.

Backwards Compatibility

This change is fully backwards compatible. All existing code using
parenthesized except and except* blocks will continue to work without
modification. The new syntax is purely additive and does not break any
existing code.

It's worth noting that in Python 2 the unparenthesized syntax was
allowed with two elements, but had different semantics, in which the
first element of the list was used as the exception type and the second
element as the capture variable. This change does not reintroduce the
Python 2 semantics, and the unparenthesized syntax will behave
identically to the parenthesized version.

Security Implications

There are no known security implications for this change. The semantics
of exception handling remain the same, and this is purely a syntactic
change.

How to Teach This

For new Python users, the unparenthesized syntax can be taught as the
standard way to catch multiple exceptions:

    try:
        risky_operation()
    except ValueError, TypeError, OSError:
        handle_errors()

For experienced users, it can be introduced as a new, optional syntax
that can be used interchangeably with the parenthesized version.
Documentation should note that both forms are equivalent:

    # These are equivalent:
    except (ValueError, TypeError):
        ...

    except ValueError, TypeError:
        ...

It should be emphasized that this is purely a syntactic change and does
not affect the behaviour of exception handling.

Reference Implementation

A proof-of-concept implementation is available at
https://github.com/pablogsal/cpython/commits/notuples/. This
implementation modifies the Python parser to accept the new syntax and
ensures that it behaves identically to the parenthesized version.

Rejected Ideas

1.  Allowing mixed parenthesized and unparenthesized syntax:

        try:
            ...
        except (ValueError, TypeError), OSError:
           ...

    This was rejected due to the potential for confusion and to maintain
    a clear distinction between the two styles.

Footnotes

Copyright

This document is placed in the public domain or under the
CC0-1.0-Universal license, whichever is more permissive.

[1] Originally named "Parenthetically Speaking, We Don't Need 'Em"