Skip to content

Commit 591e9bb

Browse files
authored
Remove parentheses around multiple exception types on Python 3.14+ (#20768)
Summary -- This PR implements the black preview style from psf/black#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.
1 parent 1ed9b21 commit 591e9bb

File tree

8 files changed

+497
-450
lines changed

8 files changed

+497
-450
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[
2+
{
3+
"target_version": "3.13"
4+
},
5+
{
6+
"target_version": "3.14"
7+
}
8+
]

crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,40 @@ def f():
166166

167167
finally:
168168
pass
169+
170+
171+
try:
172+
pass
173+
# These parens can be removed on 3.14+ but not earlier
174+
except (BaseException, Exception, ValueError):
175+
pass
176+
# But black won't remove these parentheses
177+
except (ZeroDivisionError,):
178+
pass
179+
except ( # We wrap these and preserve the parens
180+
BaseException, Exception, ValueError):
181+
pass
182+
except (
183+
BaseException,
184+
# Same with this comment
185+
Exception,
186+
ValueError
187+
):
188+
pass
189+
190+
try:
191+
pass
192+
# They can also be omitted for `except*`
193+
except* (BaseException, Exception, ValueError):
194+
pass
195+
196+
# But parentheses are still required in the presence of an `as` binding
197+
try:
198+
pass
199+
except (BaseException, Exception, ValueError) as e:
200+
pass
201+
202+
try:
203+
pass
204+
except* (BaseException, Exception, ValueError) as e:
205+
pass

crates/ruff_python_formatter/src/cli.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use anyhow::{Context, Result};
66
use clap::{Parser, ValueEnum, command};
77

88
use ruff_formatter::SourceCode;
9-
use ruff_python_ast::PySourceType;
9+
use ruff_python_ast::{PySourceType, PythonVersion};
1010
use ruff_python_parser::{ParseOptions, parse};
1111
use ruff_python_trivia::CommentRanges;
1212
use ruff_text_size::Ranged;
@@ -42,13 +42,19 @@ pub struct Cli {
4242
pub print_comments: bool,
4343
#[clap(long, short = 'C')]
4444
pub skip_magic_trailing_comma: bool,
45+
#[clap(long)]
46+
pub target_version: PythonVersion,
4547
}
4648

4749
pub fn format_and_debug_print(source: &str, cli: &Cli, source_path: &Path) -> Result<String> {
4850
let source_type = PySourceType::from(source_path);
4951

5052
// Parse the AST.
51-
let parsed = parse(source, ParseOptions::from(source_type)).context("Syntax error in input")?;
53+
let parsed = parse(
54+
source,
55+
ParseOptions::from(source_type).with_target_version(cli.target_version),
56+
)
57+
.context("Syntax error in input")?;
5258

5359
let options = PyFormatOptions::from_extension(source_path)
5460
.with_preview(if cli.preview {
@@ -60,7 +66,8 @@ pub fn format_and_debug_print(source: &str, cli: &Cli, source_path: &Path) -> Re
6066
MagicTrailingComma::Ignore
6167
} else {
6268
MagicTrailingComma::Respect
63-
});
69+
})
70+
.with_target_version(cli.target_version);
6471

6572
let source_code = SourceCode::new(source);
6673
let comment_ranges = CommentRanges::from(parsed.tokens());

crates/ruff_python_formatter/src/expression/expr_tuple.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ pub enum TupleParentheses {
3939
///
4040
/// ```python
4141
/// return len(self.nodeseeeeeeeee), sum(
42-
// len(node.parents) for node in self.node_map.values()
43-
// )
42+
/// len(node.parents) for node in self.node_map.values()
43+
/// )
4444
/// ```
4545
OptionalParentheses,
4646

crates/ruff_python_formatter/src/other/except_handler_except_handler.rs

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use ruff_formatter::FormatRuleWithOptions;
22
use ruff_formatter::write;
3-
use ruff_python_ast::ExceptHandlerExceptHandler;
3+
use ruff_python_ast::{ExceptHandlerExceptHandler, Expr, PythonVersion};
44

5+
use crate::expression::expr_tuple::TupleParentheses;
56
use crate::expression::maybe_parenthesize_expression;
67
use crate::expression::parentheses::Parenthesize;
78
use crate::prelude::*;
9+
use crate::preview::is_remove_parens_around_except_types_enabled;
810
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
911
use crate::statement::suite::SuiteKind;
1012

@@ -57,7 +59,7 @@ impl FormatNodeRule<ExceptHandlerExceptHandler> for FormatExceptHandlerExceptHan
5759
clause_header(
5860
ClauseHeader::ExceptHandler(item),
5961
dangling_comments,
60-
&format_with(|f| {
62+
&format_with(|f: &mut PyFormatter| {
6163
write!(
6264
f,
6365
[
@@ -69,21 +71,50 @@ impl FormatNodeRule<ExceptHandlerExceptHandler> for FormatExceptHandlerExceptHan
6971
]
7072
)?;
7173

72-
if let Some(type_) = type_ {
73-
write!(
74-
f,
75-
[
76-
space(),
77-
maybe_parenthesize_expression(
78-
type_,
79-
item,
80-
Parenthesize::IfBreaks
74+
match type_.as_deref() {
75+
// For tuples of exception types without an `as` name and on 3.14+, the
76+
// parentheses are optional.
77+
//
78+
// ```py
79+
// try:
80+
// ...
81+
// except BaseException, Exception: # Ok
82+
// ...
83+
// ```
84+
Some(Expr::Tuple(tuple))
85+
if f.options().target_version() >= PythonVersion::PY314
86+
&& is_remove_parens_around_except_types_enabled(
87+
f.context(),
8188
)
82-
]
83-
)?;
84-
if let Some(name) = name {
85-
write!(f, [space(), token("as"), space(), name.format()])?;
89+
&& name.is_none() =>
90+
{
91+
write!(
92+
f,
93+
[
94+
space(),
95+
tuple
96+
.format()
97+
.with_options(TupleParentheses::NeverPreserve)
98+
]
99+
)?;
86100
}
101+
Some(type_) => {
102+
write!(
103+
f,
104+
[
105+
space(),
106+
maybe_parenthesize_expression(
107+
type_,
108+
item,
109+
Parenthesize::IfBreaks
110+
)
111+
]
112+
)?;
113+
if let Some(name) = name {
114+
write!(f, [space(), token("as"), space(), name.format()])?;
115+
}
116+
}
117+
_ => {}
87118
}
88119

89120
Ok(())

crates/ruff_python_formatter/src/preview.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,12 @@ pub(crate) const fn is_blank_line_before_decorated_class_in_stub_enabled(
2727
) -> bool {
2828
context.is_preview()
2929
}
30+
31+
/// Returns `true` if the
32+
/// [`remove_parens_around_except_types`](https://github.com/astral-sh/ruff/pull/20768) preview
33+
/// style is enabled.
34+
pub(crate) const fn is_remove_parens_around_except_types_enabled(
35+
context: &PyFormatContext,
36+
) -> bool {
37+
context.is_preview()
38+
}

0 commit comments

Comments
 (0)