https://github.com/python/cpython/commit/1cdd590cb547597ab64b20c4b8a0c5b5b2f62b7a
commit: 1cdd590cb547597ab64b20c4b8a0c5b5b2f62b7a
branch: main
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-09-24T01:23:06+01:00
summary:
gh-157778: Include opening brace line in multiline f-string errors (#157779)
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/string.c
diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py
index 2fe959f6c14f77a..63006266bcd039e 100644
--- a/Lib/test/test_fstring.py
+++ b/Lib/test/test_fstring.py
@@ -617,6 +617,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/string.c b/Parser/lexer/string.c
index 945480ef86f7789..1fb684e337de972 100644
--- a/Parser/lexer/string.c
+++ b/Parser/lexer/string.c
@@ -355,9 +355,19 @@ _PyLexer_scan_string(struct tok_state *tok, struct token
*token, int c)
const ftstring_state *state = _PyLexer_CurrentFTString(tok);
if (state != NULL) {
- /* A matching quote belongs to the surrounding formatted
- * string, so the expression is missing its closing brace. */
+ /* A matching quote may have been intended to close the
+ * surrounding formatted string instead of opening a string
+ * inside a replacement field. */
if (state->quote == quote && state->quote_size == quote_size) {
+ int level = state->paren_level + state->replacement_depth
- 1;
+ 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",
+ _PyLexer_StringPrefix(state->kind), lineno));
+ }
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
"%c-string: expecting '}'",
_PyLexer_StringPrefix(state->kind)));
_______________________________________________
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]