From 2d2affbcd4a09ae1ebd89ebd2b4818e3d9b57592 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Fri, 23 Oct 2026 15:29:41 -0700 Subject: [PATCH] gh-160491: Fix location of invalid escapes in f-strings and t-strings (GH-160493) Literal parts of f-strings and t-strings are decoded only after the whole string has been parsed, so a decode error took the location of the last token. The resulting SyntaxError pointed at the closing quote, and for multi-line strings it could be on the wrong line. Raise the error at the literal part being decoded instead. (cherry picked from commit 2258026edd865a247459d727ac777908594dc44a) Co-authored-by: Lysandros Nikolaou --- Lib/test/test_fstring.py | 18 +++++++++++++++++ ...-10-23-20-15-24.gh-issue-160491.yIQ_Vf.rst | 3 +++ Parser/action_helpers.c | 2 +- Parser/pegen.h | 1 + Parser/pegen_errors.c | 20 +++++++++++++++++-- 5 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-20-15-24.gh-issue-160491.yIQ_Vf.rst diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 0f87deebf7a8dd..44527fb56f8fb0 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1046,6 +1046,24 @@ def test_misformed_unicode_character_name(self): r"'\N{GREEK CAPITAL LETTER DELTA'", ]) + def test_invalid_escape_location(self): + cases = [ + (r'f"\xZZ"', (1, 3, 1, 7)), + (r'f"{x}\xZZ"', (1, 6, 1, 10)), + (r'f"a{x}b\N{NOPE}{y}"', (1, 7, 1, 16)), + (r'f"ok" f"\xZZ"', (1, 9, 1, 13)), + ('f"""\\xZZ\nabc"""', (1, 5, 2, 4)), + ('f"""abc\ndef\\xZZ"""', (1, 5, 2, 8)), + ('x = (1,\n f"\\xZZ")', (2, 5, 2, 9)), + ] + for source, location in cases: + with self.subTest(source=source): + with self.assertRaisesRegex(SyntaxError, r'\(unicode error\)') as cm: + compile(source, '', 'exec') + error = cm.exception + self.assertEqual((error.lineno, error.offset, + error.end_lineno, error.end_offset), location) + def test_backslashes_in_expression_part(self): self.assertEqual(f"{( 1 + diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-20-15-24.gh-issue-160491.yIQ_Vf.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-20-15-24.gh-issue-160491.yIQ_Vf.rst new file mode 100644 index 00000000000000..2227224ba0fa2c --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-20-15-24.gh-issue-160491.yIQ_Vf.rst @@ -0,0 +1,3 @@ +Fix the location reported for invalid escape sequences in f-strings and +t-strings. The :exc:`SyntaxError` now points at the literal part that +contains the escape, instead of at the closing quote. diff --git a/Parser/action_helpers.c b/Parser/action_helpers.c index 3c111307240097..c91aaa512c9392 100644 --- a/Parser/action_helpers.c +++ b/Parser/action_helpers.c @@ -1302,7 +1302,7 @@ _PyPegen_decode_fstring_part(Parser* p, int is_raw, expr_ty constant, Token* tok is_raw = is_raw || strchr(bstr, '\\') == NULL; PyObject *str = _PyPegen_decode_string(p, is_raw, bstr, len, token); if (str == NULL) { - _Pypegen_raise_decode_error(p); + _Pypegen_raise_decode_error_known_location(p, constant); return NULL; } if (_PyArena_AddPyObject(p->arena, str) < 0) { diff --git a/Parser/pegen.h b/Parser/pegen.h index 56d5d1b3bc8bde..b4ac4edde549ab 100644 --- a/Parser/pegen.h +++ b/Parser/pegen.h @@ -175,6 +175,7 @@ typedef enum { } TARGETS_TYPE; int _Pypegen_raise_decode_error(Parser *p); +int _Pypegen_raise_decode_error_known_location(Parser *p, expr_ty a); int _Pypegen_tokenizer_error(Parser *p); void *_PyPegen_raise_error(Parser *p, PyObject *errtype, int use_mark, const char *errmsg, ...); void *_PyPegen_raise_error_known_location(Parser *p, PyObject *errtype, diff --git a/Parser/pegen_errors.c b/Parser/pegen_errors.c index 4d4f9a11865d88..e9039df9133f80 100644 --- a/Parser/pegen_errors.c +++ b/Parser/pegen_errors.c @@ -81,6 +81,12 @@ _Pypegen_tokenizer_error(Parser *p) int _Pypegen_raise_decode_error(Parser *p) +{ + return _Pypegen_raise_decode_error_known_location(p, NULL); +} + +int +_Pypegen_raise_decode_error_known_location(Parser *p, expr_ty a) { assert(PyErr_Occurred()); const char *errtype = NULL; @@ -98,12 +104,22 @@ _Pypegen_raise_decode_error(Parser *p) PyErr_Fetch(&type, &value, &tback); errstr = PyObject_Str(value); if (errstr) { - RAISE_SYNTAX_ERROR("(%s) %U", errtype, errstr); + if (a != NULL) { + RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "(%s) %U", errtype, errstr); + } + else { + RAISE_SYNTAX_ERROR("(%s) %U", errtype, errstr); + } Py_DECREF(errstr); } else { PyErr_Clear(); - RAISE_SYNTAX_ERROR("(%s) unknown error", errtype); + if (a != NULL) { + RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "(%s) unknown error", errtype); + } + else { + RAISE_SYNTAX_ERROR("(%s) unknown error", errtype); + } } Py_XDECREF(type); Py_XDECREF(value);