https://github.com/python/cpython/commit/3b9fa19e56b8209808aa8e4ac5572fe6abffe5f7
commit: 3b9fa19e56b8209808aa8e4ac5572fe6abffe5f7
branch: 3.15
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-09-24T00:59:40Z
summary:

[3.15] gh-157778: Include opening brace line in multiline f-string errors 
(GH-157779) (#158047)

gh-157778: Include opening brace line in multiline f-string errors (#157779)

(cherry picked from commit 1cdd590cb547597ab64b20c4b8a0c5b5b2f62b7a)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-09-19-12-00-00.gh-issue-157778.fstring-brace.rst
M Lib/test/test_fstring.py
M Parser/lexer/lexer.c

diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py
index 4cda471b29db980..7595bffdd8bfa1a 100644
--- a/Lib/test/test_fstring.py
+++ b/Lib/test/test_fstring.py
@@ -619,6 +619,34 @@ def test_unterminated_string(self):
                              r"""f'{("x}'""",
                              ])
 
+    def test_unclosed_multiline_replacement_field(self):
+        for prefix in ('f', 't', 'rf', 'rt'):
+            for quote in ('"', "'"):
+                triple = quote * 3
+                cases = (
+                    # The apparent closing quotes open a string in the field.
+                    ('var = "abc"\na = PREFIXQUOTE {var} extern "C" { QUOTE\n'
+                     'b = QUOTE string QUOTE', 2),
+                    # Parentheses and dictionaries inside the field must not
+                    # change which opening brace the diagnostic identifies.
+                    ('a = PREFIXQUOTE{\n(QUOTE', 1),
+                    ('a = PREFIXQUOTE{\n{0: QUOTE', 1),
+                    # Use the innermost format field or formatted string.
+                    ('a = PREFIXQUOTE{0:\n{1\nQUOTE', 2),
+                    ('a = f"{\nPREFIXQUOTE{1\nQUOTE', 2),
+                    # Account for parentheses outside the formatted string.
+                    ('a = (PREFIXQUOTE{1\nQUOTE', 1),
+                )
+                for source, lineno in cases:
+                    source = source.replace('PREFIX', prefix).replace('QUOTE', 
triple)
+                    with self.subTest(source=source):
+                        with self.assertRaises(SyntaxError) as cm:
+                            compile(source, '<test>', 'exec')
+                        self.assertEqual(
+                            cm.exception.msg,
+                            f"{prefix[-1]}-string: expecting '}}' to close 
'{{' "
+                            f"on line {lineno}")
+
     @unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
     def test_mismatched_parens(self):
         self.assertAllRaise(SyntaxError, r"closing parenthesis '\}' "
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-19-12-00-00.gh-issue-157778.fstring-brace.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-19-12-00-00.gh-issue-157778.fstring-brace.rst
new file mode 100644
index 000000000000000..cb1bc8942d1b1b4
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-19-12-00-00.gh-issue-157778.fstring-brace.rst
@@ -0,0 +1,3 @@
+Include the line number of the opening brace in the :exc:`SyntaxError` for
+an unclosed f-string or t-string replacement field when a matching quote
+on a later line is interpreted as the start of an unterminated string.
diff --git a/Parser/lexer/lexer.c b/Parser/lexer/lexer.c
index 58cba8207757822..0c0f909d6368ee5 100644
--- a/Parser/lexer/lexer.c
+++ b/Parser/lexer/lexer.c
@@ -1144,6 +1144,16 @@ tok_get_normal_mode(struct tok_state *tok, 
tokenizer_mode* current_tok, struct t
                     tokenizer_mode *the_current_tok = TOK_GET_MODE(tok);
                     if (the_current_tok->quote == quote &&
                         the_current_tok->quote_size == quote_size) {
+                        int level = tok->level - 
the_current_tok->curly_bracket_depth
+                                    + 
the_current_tok->curly_bracket_expr_start_depth;
+                        assert(level >= 0 && level < tok->level);
+                        assert(tok->parenstack[level] == '{');
+                        int lineno = tok->parenlinenostack[level];
+                        if (lineno != tok->lineno) {
+                            return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
+                                "%c-string: expecting '}' to close '{' on line 
%d",
+                                TOK_GET_STRING_PREFIX(tok), lineno));
+                        }
                         return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
                             "%c-string: expecting '}'", 
TOK_GET_STRING_PREFIX(tok)));
                     }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to