Skip to content

Add f-string consistent quotes formatting option#23098

Closed
matthewlloyd wants to merge 2 commits intoastral-sh:mainfrom
matthewlloyd:0.15.0-f-string-consistent-quotes-pr
Closed

Add f-string consistent quotes formatting option#23098
matthewlloyd wants to merge 2 commits intoastral-sh:mainfrom
matthewlloyd:0.15.0-f-string-consistent-quotes-pr

Conversation

@matthewlloyd
Copy link
Copy Markdown
Contributor

Introduces a new formatter option f-string-consistent-quotes that leverages Python 3.12's PEP 701 to use consistent quotes inside f-string expressions rather than alternating quote styles for compatibility.

When enabled and targeting Python 3.12+, the formatter will use the same quote style (following the quote-style setting) inside f-string expressions as in the outer f-string. This produces more consistent and readable code.

When disabled (default) or targeting Python versions below 3.12, the formatter will continue to alternate quotes for compatibility.

Implements: #14118

Original PR (closed): #16385

Introduces a new formatter option `f-string-consistent-quotes` that leverages Python 3.12's PEP 701 to use consistent quotes inside f-string expressions rather than alternating quote styles for compatibility.

When enabled and targeting Python 3.12+, the formatter will use the same quote style (following the `quote-style` setting) inside f-string expressions as in the outer f-string. This produces more consistent and readable code.

When disabled (default) or targeting Python versions below 3.12, the formatter will continue to alternate quotes for compatibility.
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Feb 5, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

Copy link
Copy Markdown
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thank you for reviving this work! Just wanted to note that Micha is out this month, and I'd love to get his input on this, so it may be a few weeks before a review.

I skimmed through quickly, and the changes look reasonable to me at a high level besides the small comments I left.

@ntBre ntBre added configuration Related to settings and configuration formatter Related to the formatter labels Feb 5, 2026
Copy link
Copy Markdown
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

Thank yous This looks good to me. I only have a small nit code wise.

But I want to do some bikeshedding on the option name. Similar to quote-style, I think I'd prefer an option with two string values. I also think it's important that the option isn't f-string specific because it also applies to t-strings. This will make naming even trickier

  • nested-interpolated-strings-quote-style = "preferred" | "alternate"
  • interpolated-strings-consistent-quote-style = true | false
  • nested-template-string-quote-style = "preferred" | "alternate"
  • interpolated-string-quote-style = "consistent" | "alternate"

But I must admit, I don't really like any of them. They all sound very obscure. So I'd be interested to get some more opinions.

Are there cases where the "consistent" styling would be different from the preferred quote style?

Comment on lines +71 to 79
// When f_string_consistent_quotes is enabled AND we're targeting Python 3.12+,
// use the preferred quote style consistently.
if supports_pep_701 && consistent_quotes && !preferred_quote_style.is_preserve() {
return preferred_quote_style;
}
// Otherwise, use alternating quotes for compatibility.
// This logic is even necessary when using preserve and the target python version doesn't support PEP701 because
// we might end up joining two f-strings that have different quote styles, in which case we need to alternate the quotes
// for inner strings to avoid a syntax error: `string = "this is my string with " f'"{params.get("mine")}"'`
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.

Nit: I'd probably restructure the code here as in this patch to remove some of the repeated conditions:

Index: crates/ruff_python_formatter/src/string/normalize.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs
--- a/crates/ruff_python_formatter/src/string/normalize.rs	(revision 5329113011ab9fdb93302c660f5a03052f03bbfe)
+++ b/crates/ruff_python_formatter/src/string/normalize.rs	(date 1772639527311)
@@ -10,10 +10,10 @@
 };
 use ruff_text_size::{Ranged, TextRange, TextSlice};
 
-use crate::QuoteStyle;
 use crate::context::InterpolatedStringState;
 use crate::prelude::*;
 use crate::string::{Quote, StringQuotes, TripleQuotes};
+use crate::{FStringConsistentQuotes, QuoteStyle};
 
 pub(crate) struct StringNormalizer<'a, 'src> {
     preferred_quote_style: Option<QuoteStyle>,
@@ -68,18 +68,25 @@
                 .is_enabled();
 
             if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() {
-                // When f_string_consistent_quotes is enabled AND we're targeting Python 3.12+,
-                // use the preferred quote style consistently.
-                if supports_pep_701 && consistent_quotes && !preferred_quote_style.is_preserve() {
-                    return preferred_quote_style;
-                }
-                // Otherwise, use alternating quotes for compatibility.
                 // This logic is even necessary when using preserve and the target python version doesn't support PEP701 because
                 // we might end up joining two f-strings that have different quote styles, in which case we need to alternate the quotes
                 // for inner strings to avoid a syntax error: `string = "this is my string with " f'"{params.get("mine")}"'`
-                if !preferred_quote_style.is_preserve() || !supports_pep_701 {
+                if !supports_pep_701 {
                     return QuoteStyle::from(parent_flags.quote_style().opposite());
                 }
+
+                if !preferred_quote_style.is_preserve() {
+                    match self.context.options().f_string_consistent_quotes() {
+                        FStringConsistentQuotes::Disabled => {
+                            return QuoteStyle::from(parent_flags.quote_style().opposite());
+                        }
+                        // When f_string_consistent_quotes is enabled AND we're targeting Python 3.12+,
+                        // use the preferred quote style consistently.
+                        FStringConsistentQuotes::Enabled => {
+                            return QuoteStyle::from(parent_flags.quote_style());
+                        }
+                    }
+                }
             }
         }
 

@@ -1,5 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
assertion_line: 267
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.

Can you try upgrading your cargo insta version? I believe cargo insta stopped adding assertion line metadata to snapshot but I'm not a 100% sure. If so, please revert the assertion_line changes.

quoted_in_expr = f"Result: {get_value(key='test', default='none')}"

# Multiple f-strings in one expression
multiple = f"First: {x}" + f"Second: {y}"
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.

Let's also add some tests for t-strings

@zsol
Copy link
Copy Markdown
Member

zsol commented Mar 4, 2026

So I'd be interested to get some more opinions.

nested-string-quotes = alternating | uniform (or maybe unescaped-uniform)

?

@MichaReiser
Copy link
Copy Markdown
Member

Having thought about this more, I think there's one remaining design question.

To me it's unclear whether consistent is indeed desired because the outer-most f-string picks the quotes that result in the fewest escapes only for itself. It doesn't consider any nested f-strings. This raises the question whether the option should be about whether it's consistent but whether the formatter should strictly alternate (which could result in unnecessary escapes) or pick the quote style with the fewest escapes (this should be the same as consistent in almost all cases but isn't guaranteed to always match consistent).

I feel like we want the preferred quotes over strictly enforcing consistency. If it's consistency that we want, then I think we'd have to change the quote selection no not only make a local selection, but pick the quote style that results in the fewest quotes for the entire f-string (seems unnecessary complicated to me)

@matthewlloyd
Copy link
Copy Markdown
Contributor Author

Hi Micha, thanks for the review. Let's resolve the behavior question first, then naming and code nits.

I think there are two clean choices:

A: Always use preferred quotes for nested strings rather than strictly enforcing consistency with the outermost quotes. Given the existing escape minimization behavior and the potential complexity of optimizing escapes in nested strings too, I agree with you this seems the simplest and most logical choice.

B: Add an option to always use preferred quotes for all strings - outermost and nested - disabling escape minimization entirely. This would subsume A. For example, it could be named quote-style-strict = true.

Rationales for B:

  • The option doesn't need to be named with reference to f-strings or t-strings. If Python introduces new forms of string nesting, the option would automatically apply to those too.
  • It's simpler to implement, test, document, and reason about.
  • Developers who want consistent nested quotes likely want consistent outermost quotes too, even at the cost of an occasional extra escape - they likely prefer the use and meaning of ", ', \" and \' to be consistent across their codebase, and to avoid noisy diffs caused by escape-optimizing quote flips.

While I actually prefer B, A is fine if you prefer to keep the scope small.

@MichaReiser
Copy link
Copy Markdown
Member

I'm not sure I correctly understand what you propose in option B but changing quote selection to disregard escape minimization feels out of scope of this PR and I consider very unlikely to be something we want to change. Even if it's only for f-/t-string. I think it's important that they are formatted just like any other string, which includes escape minimization.

The options I suggested are:

A: The same as your option A. Each f-string (including nested) always pick their preferred quote style if the new setting introduced in this PR is true instead of alternating. The quote selection tries to minimize escapes only in the current f-string, disregarding any escapes in nested f-strings. In most cases, this is the same as always using the same quotes.

B: Always use the quotes from the outer f-string but change the quote-style selection to select the quote style to also consider nested f-string so that the selected quote style requires the minimal escapes across all nested f-strings (and not just the outermost).

I'm leaning towards option A because:

  • It's easier to implement. It only requires skipping the alternating logic if the new option is true
  • It requires the least escape characters

@matthewlloyd
Copy link
Copy Markdown
Contributor Author

I'm not sure I correctly understand what you propose in option B but changing quote selection to disregard escape minimization feels out of scope of this PR

OK, yes it would be. I will open a separate Feature Request after this is merged to gauge support for that simplifying proposal.

A: The same as your option A. Each f-string (including nested) always pick their preferred quote style if the new setting introduced in this PR is true instead of alternating. The quote selection tries to minimize escapes only in the current f-string, disregarding any escapes in nested f-strings. In most cases, this is the same as always using the same quotes.

Great, we are agreed on this.

Next, naming. Updating your earlier suggestions to reflect preferred instead of consistent:

  1. nested-interpolated-strings-quote-style = "preferred" | "alternate"
  2. interpolated-strings-preferred-quote-style = true | false
  3. nested-template-string-quote-style = "preferred" | "alternate"
  4. interpolated-string-quote-style = "preferred" | "alternate"

Let's eliminate (2) and (4): the options apply only to nested interpolated strings rather than all interpolated strings as the names suggest (many users might think f"{1+2+3}" is an interpolated string), and escape minimization means we won't always use the user's configured preferred quotes anyway.

In (3), "template" suggests t-strings, but this will also apply to f-strings.

That leaves:

  • nested-interpolated-strings-quote-style = "preferred" | "alternate"

Are there any other kinds of nested string other than interpolated ones, for the purposes of the formatter? Presumably strings inside strings like "'is this nested'" don't count since they aren't relevant to the formatter. If not, we can simplify this to just:

  • nested-string-quote-style = "preferred" | "alternate"

This is similar to @zsol's suggestion, which we should update since escape minimization could mean our option (A) won't be "uniform", to:

  • nested-string-quotes = "preferred" | "alternating"

I think alternating is clearer than alternate since the latter might suggest different or alternative quotes. quotes is shorter than quote-style and doesn't seem ambiguous, so this is my favorite so far.

@MichaReiser
Copy link
Copy Markdown
Member

OK, yes it would be. I will open a separate Feature Request after this is merged to gauge support for that simplifying proposal.

If we think this is what users want, I would rather know before merging this PR. I don't think we should support three options here.

quotes is shorter than quote-style and doesn't seem ambiguous, so this is my favorite so far.

I prefer quote-style to ensure consistency with the main quote-style option

nested-string-quote-style = "preferred" | "alternate"

This is my preferred spelling. I'm not too opinionated on alternate vs alternating but using alternating seems fine to me

@matthewlloyd
Copy link
Copy Markdown
Contributor Author

If we think this is what users want, I would rather know before merging this PR. I don't think we should support three options here.

OK, I've requested some feedback here: #14118 (comment). Let's put this PR on hold while waiting for that.

I prefer quote-style to ensure consistency with the main quote-style option

nested-string-quote-style = "preferred" | "alternate"

This is my preferred spelling. I'm not too opinionated on alternate vs alternating but using alternating seems fine to me

Agreed, quote-style is more consistent. How about we settle on:

nested-string-quote-style = "alternating" (default) | "preferred"

@matthewlloyd
Copy link
Copy Markdown
Contributor Author

Replaced by a new PR implementing the option variant we chose: #24312

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configuration Related to settings and configuration formatter Related to the formatter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Allow Configurable Quote Styling for f-Strings in Python 3.12+

4 participants