Remove parentheses around multiple exception types on Python 3.14+ (#20768)

Summary
--

This PR implements the black preview style from
https://github.com/psf/black/pull/4720. As of Python 3.14, you're
allowed to omit the parentheses around groups of exceptions, as long as
there's no `as` binding:

**3.13**

```pycon
Python 3.13.4 (main, Jun  4 2025, 17:37:06) [Clang 20.1.4 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> try: ...
... except (Exception, BaseException): ...
...
Ellipsis
>>> try: ...
... except Exception, BaseException: ...
...
  File "<python-input-1>", line 2
    except Exception, BaseException: ...
           ^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized
```

**3.14**

```pycon
Python 3.14.0rc2 (main, Sep  2 2025, 14:20:56) [Clang 20.1.4 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> try: ...
... except Exception, BaseException: ...
...
Ellipsis
>>> try: ...
... except (Exception, BaseException): ...
...
Ellipsis
>>> try: ...
... except Exception, BaseException as e: ...
...
  File "<python-input-2>", line 2
    except Exception, BaseException as e: ...
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized when using 'as'
```

I think this ended up being pretty straightforward, at least once Micha
showed me where to start :)

Test Plan
--

New tests

At first I thought we were deviating from black in how we handle
comments within the exception type tuple, but I think this applies to
how we format all tuples, not specifically with the new preview style.
This commit is contained in:
Brent Westbrook 2025-10-14 11:17:45 -04:00 committed by GitHub
parent 1ed9b215b9
commit 591e9bbccb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 497 additions and 450 deletions

View file

@ -1,427 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/remove_except_types_parens.py
---
## Input
```python
# SEE PEP 758 FOR MORE DETAILS
# remains unchanged
try:
pass
except:
pass
# remains unchanged
try:
pass
except ValueError:
pass
try:
pass
except* ValueError:
pass
# parenthesis are removed
try:
pass
except (ValueError):
pass
try:
pass
except* (ValueError):
pass
# parenthesis are removed
try:
pass
except (ValueError) as e:
pass
try:
pass
except* (ValueError) as e:
pass
# remains unchanged
try:
pass
except (ValueError,):
pass
try:
pass
except* (ValueError,):
pass
# remains unchanged
try:
pass
except (ValueError,) as e:
pass
try:
pass
except* (ValueError,) as e:
pass
# remains unchanged
try:
pass
except ValueError, TypeError, KeyboardInterrupt:
pass
try:
pass
except* ValueError, TypeError, KeyboardInterrupt:
pass
# parenthesis are removed
try:
pass
except (ValueError, TypeError, KeyboardInterrupt):
pass
try:
pass
except* (ValueError, TypeError, KeyboardInterrupt):
pass
# parenthesis are not removed
try:
pass
except (ValueError, TypeError, KeyboardInterrupt) as e:
pass
try:
pass
except* (ValueError, TypeError, KeyboardInterrupt) as e:
pass
# parenthesis are removed
try:
pass
except (ValueError if True else TypeError):
pass
try:
pass
except* (ValueError if True else TypeError):
pass
# inner except: parenthesis are removed
# outer except: parenthsis are not removed
try:
try:
pass
except (TypeError, KeyboardInterrupt):
pass
except (ValueError,):
pass
try:
try:
pass
except* (TypeError, KeyboardInterrupt):
pass
except* (ValueError,):
pass
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -74,12 +74,12 @@
# parenthesis are removed
try:
pass
-except ValueError, TypeError, KeyboardInterrupt:
+except (ValueError, TypeError, KeyboardInterrupt):
pass
try:
pass
-except* ValueError, TypeError, KeyboardInterrupt:
+except* (ValueError, TypeError, KeyboardInterrupt):
pass
# parenthesis are not removed
@@ -109,7 +109,7 @@
try:
try:
pass
- except TypeError, KeyboardInterrupt:
+ except (TypeError, KeyboardInterrupt):
pass
except (ValueError,):
pass
@@ -117,7 +117,7 @@
try:
try:
pass
- except* TypeError, KeyboardInterrupt:
+ except* (TypeError, KeyboardInterrupt):
pass
except* (ValueError,):
pass
```
## Ruff Output
```python
# SEE PEP 758 FOR MORE DETAILS
# remains unchanged
try:
pass
except:
pass
# remains unchanged
try:
pass
except ValueError:
pass
try:
pass
except* ValueError:
pass
# parenthesis are removed
try:
pass
except ValueError:
pass
try:
pass
except* ValueError:
pass
# parenthesis are removed
try:
pass
except ValueError as e:
pass
try:
pass
except* ValueError as e:
pass
# remains unchanged
try:
pass
except (ValueError,):
pass
try:
pass
except* (ValueError,):
pass
# remains unchanged
try:
pass
except (ValueError,) as e:
pass
try:
pass
except* (ValueError,) as e:
pass
# remains unchanged
try:
pass
except ValueError, TypeError, KeyboardInterrupt:
pass
try:
pass
except* ValueError, TypeError, KeyboardInterrupt:
pass
# parenthesis are removed
try:
pass
except (ValueError, TypeError, KeyboardInterrupt):
pass
try:
pass
except* (ValueError, TypeError, KeyboardInterrupt):
pass
# parenthesis are not removed
try:
pass
except (ValueError, TypeError, KeyboardInterrupt) as e:
pass
try:
pass
except* (ValueError, TypeError, KeyboardInterrupt) as e:
pass
# parenthesis are removed
try:
pass
except ValueError if True else TypeError:
pass
try:
pass
except* ValueError if True else TypeError:
pass
# inner except: parenthesis are removed
# outer except: parenthsis are not removed
try:
try:
pass
except (TypeError, KeyboardInterrupt):
pass
except (ValueError,):
pass
try:
try:
pass
except* (TypeError, KeyboardInterrupt):
pass
except* (ValueError,):
pass
```
## Black Output
```python
# SEE PEP 758 FOR MORE DETAILS
# remains unchanged
try:
pass
except:
pass
# remains unchanged
try:
pass
except ValueError:
pass
try:
pass
except* ValueError:
pass
# parenthesis are removed
try:
pass
except ValueError:
pass
try:
pass
except* ValueError:
pass
# parenthesis are removed
try:
pass
except ValueError as e:
pass
try:
pass
except* ValueError as e:
pass
# remains unchanged
try:
pass
except (ValueError,):
pass
try:
pass
except* (ValueError,):
pass
# remains unchanged
try:
pass
except (ValueError,) as e:
pass
try:
pass
except* (ValueError,) as e:
pass
# remains unchanged
try:
pass
except ValueError, TypeError, KeyboardInterrupt:
pass
try:
pass
except* ValueError, TypeError, KeyboardInterrupt:
pass
# parenthesis are removed
try:
pass
except ValueError, TypeError, KeyboardInterrupt:
pass
try:
pass
except* ValueError, TypeError, KeyboardInterrupt:
pass
# parenthesis are not removed
try:
pass
except (ValueError, TypeError, KeyboardInterrupt) as e:
pass
try:
pass
except* (ValueError, TypeError, KeyboardInterrupt) as e:
pass
# parenthesis are removed
try:
pass
except ValueError if True else TypeError:
pass
try:
pass
except* ValueError if True else TypeError:
pass
# inner except: parenthesis are removed
# outer except: parenthsis are not removed
try:
try:
pass
except TypeError, KeyboardInterrupt:
pass
except (ValueError,):
pass
try:
try:
pass
except* TypeError, KeyboardInterrupt:
pass
except* (ValueError,):
pass
```

View file

@ -1,7 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py
snapshot_kind: text
---
## Input
```python
@ -173,9 +172,61 @@ else:
finally:
pass
try:
pass
# These parens can be removed on 3.14+ but not earlier
except (BaseException, Exception, ValueError):
pass
# But black won't remove these parentheses
except (ZeroDivisionError,):
pass
except ( # We wrap these and preserve the parens
BaseException, Exception, ValueError):
pass
except (
BaseException,
# Same with this comment
Exception,
ValueError
):
pass
try:
pass
# They can also be omitted for `except*`
except* (BaseException, Exception, ValueError):
pass
# But parentheses are still required in the presence of an `as` binding
try:
pass
except (BaseException, Exception, ValueError) as e:
pass
try:
pass
except* (BaseException, Exception, ValueError) as e:
pass
```
## Outputs
### Output 1
```
indent-style = space
line-width = 88
indent-width = 4
quote-style = Double
line-ending = LineFeed
magic-trailing-comma = Respect
docstring-code = Disabled
docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.13
source_type = Python
```
## Output
```python
try:
pass
@ -364,10 +415,50 @@ else:
finally:
pass
try:
pass
# These parens can be removed on 3.14+ but not earlier
except (BaseException, Exception, ValueError):
pass
# But black won't remove these parentheses
except (ZeroDivisionError,):
pass
except ( # We wrap these and preserve the parens
BaseException,
Exception,
ValueError,
):
pass
except (
BaseException,
# Same with this comment
Exception,
ValueError,
):
pass
try:
pass
# They can also be omitted for `except*`
except* (BaseException, Exception, ValueError):
pass
# But parentheses are still required in the presence of an `as` binding
try:
pass
except (BaseException, Exception, ValueError) as e:
pass
try:
pass
except* (BaseException, Exception, ValueError) as e:
pass
```
## Preview changes
#### Preview changes
```diff
--- Stable
+++ Preview
@ -392,3 +483,294 @@ finally:
def f():
```
### Output 2
```
indent-style = space
line-width = 88
indent-width = 4
quote-style = Double
line-ending = LineFeed
magic-trailing-comma = Respect
docstring-code = Disabled
docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.14
source_type = Python
```
```python
try:
pass
except:
pass
try:
pass
except KeyError: # should remove brackets and be a single line
pass
try: # try
pass
# end of body
# before except
except (Exception, ValueError) as exc: # except line
pass
# before except 2
except KeyError as key: # except line 2
pass
# in body 2
# before else
else:
pass
# before finally
finally:
pass
# with line breaks
try: # try
pass
# end of body
# before except
except (Exception, ValueError) as exc: # except line
pass
# before except 2
except KeyError as key: # except line 2
pass
# in body 2
# before else
else:
pass
# before finally
finally:
pass
# with line breaks
try:
pass
except:
pass
try:
pass
except (
Exception,
Exception,
Exception,
Exception,
Exception,
Exception,
Exception,
) as exc: # splits exception over multiple lines
pass
try:
pass
except:
a = 10 # trailing comment1
b = 11 # trailing comment2
# try/except*, mostly the same as try
try: # try
pass
# end of body
# before except
except* (Exception, ValueError) as exc: # except line
pass
# before except 2
except* KeyError as key: # except line 2
pass
# in body 2
# before else
else:
pass
# before finally
finally:
pass
# try and try star are statements with body
# Minimized from https://github.com/python/cpython/blob/99b00efd5edfd5b26bf9e2a35cbfc96277fdcbb1/Lib/getpass.py#L68-L91
try:
try:
pass
finally:
print(1) # issue7208
except A:
pass
try:
f() # end-of-line last comment
except RuntimeError:
raise
try:
def f():
pass
# a
except:
def f():
pass
# b
else:
def f():
pass
# c
finally:
def f():
pass
# d
try:
pass # a
except ZeroDivisionError:
pass # b
except:
pass # c
else:
pass # d
finally:
pass # e
try: # 1 preceding: any, following: first in body, enclosing: try
print(1) # 2 preceding: last in body, following: fist in alt body, enclosing: try
except (
ZeroDivisionError
): # 3 preceding: test, following: fist in alt body, enclosing: try
print(2) # 4 preceding: last in body, following: fist in alt body, enclosing: exc
except: # 5 preceding: last in body, following: fist in alt body, enclosing: try
print(2) # 6 preceding: last in body, following: fist in alt body, enclosing: exc
else: # 7 preceding: last in body, following: fist in alt body, enclosing: exc
print(3) # 8 preceding: last in body, following: fist in alt body, enclosing: try
finally: # 9 preceding: last in body, following: fist in alt body, enclosing: try
print(3) # 10 preceding: last in body, following: any, enclosing: try
try:
pass
except (
ZeroDivisionError
# comment
):
pass
try:
pass
finally:
pass
try:
pass
except ZeroDivisonError:
pass
else:
pass
finally:
pass
try:
pass
# These parens can be removed on 3.14+ but not earlier
except (BaseException, Exception, ValueError):
pass
# But black won't remove these parentheses
except (ZeroDivisionError,):
pass
except ( # We wrap these and preserve the parens
BaseException,
Exception,
ValueError,
):
pass
except (
BaseException,
# Same with this comment
Exception,
ValueError,
):
pass
try:
pass
# They can also be omitted for `except*`
except* (BaseException, Exception, ValueError):
pass
# But parentheses are still required in the presence of an `as` binding
try:
pass
except (BaseException, Exception, ValueError) as e:
pass
try:
pass
except* (BaseException, Exception, ValueError) as e:
pass
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -117,16 +117,19 @@
def f():
pass
# a
+
except:
def f():
pass
# b
+
else:
def f():
pass
# c
+
finally:
def f():
@@ -190,7 +193,7 @@
try:
pass
# These parens can be removed on 3.14+ but not earlier
-except (BaseException, Exception, ValueError):
+except BaseException, Exception, ValueError:
pass
# But black won't remove these parentheses
except (ZeroDivisionError,):
@@ -212,7 +215,7 @@
try:
pass
# They can also be omitted for `except*`
-except* (BaseException, Exception, ValueError):
+except* BaseException, Exception, ValueError:
pass
# But parentheses are still required in the presence of an `as` binding
```