From caae28f98564dd2c218cfe3e0f58fe1cf89b409f Mon Sep 17 00:00:00 2001 From: emerard <113128214+emerardd@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:47:04 +0800 Subject: [PATCH] fix(parser): preserve tab indentation across continuation lines --- Lib/test/test_syntax.py | 24 +++++++++++++++++++ ...26-10-08-00-00-00.gh-issue-144018.tabs.rst | 3 +++ Parser/lexer/layout.c | 4 +++- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-08-00-00-00.gh-issue-144018.tabs.rst diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 39ab53bb1f8b5fe..117c433ec627a6b 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -3273,6 +3273,30 @@ def fib(n): except SyntaxError: self.fail("Indented statement over multiple lines is valid") + @support.subTests("indent", ("\t", " \t", "\t ", " ")) + @support.subTests("continuations", (1, 3)) + @support.subTests("newline", ("\n", "\r\n")) + @support.subTests("first_statement", (False, True)) + def test_continuation_tab_indentation(self, indent, continuations, + newline, first_statement): + lines = ["if True:"] + if not first_statement: + lines.append(f"{indent}pass") + lines.extend([f"{indent}\\"] * continuations) + lines.extend(["pass", f"{indent}pass", "pass", ""]) + compile(newline.join(lines), "", "exec") + + @support.subTests("indents", ( + (" ", "\t"), ("\t", " "), + )) + @support.subTests("continuations", (1, 3)) + def test_continuation_inconsistent_tabs(self, indents, continuations): + previous, continued = indents + lines = ["if True:", f"{previous}pass"] + lines.extend([f"{continued}\\"] * continuations) + lines.extend(["pass", ""]) + self.assertRaises(TabError, compile, "\n".join(lines), "", "exec") + def test_continuation_bad_indentation(self): # Check that code that breaks indentation across multiple lines raises a syntax error diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-08-00-00-00.gh-issue-144018.tabs.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-08-00-00-00.gh-issue-144018.tabs.rst new file mode 100644 index 000000000000000..3e226a17f70f823 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-08-00-00-00.gh-issue-144018.tabs.rst @@ -0,0 +1,3 @@ +Fix tab indentation checks for lines joined by explicit line continuation. +Consistent tab indentation is accepted and ambiguous mixtures of tabs and +spaces are rejected. diff --git a/Parser/lexer/layout.c b/Parser/lexer/layout.c index 6091f9adeac958c..b5e8f8cf32bdc53 100644 --- a/Parser/lexer/layout.c +++ b/Parser/lexer/layout.c @@ -84,6 +84,7 @@ _PyLexer_BeginLine(struct tok_state *tok) int altcol = 0; tok->layout.at_bol = 0; int cont_line_col = 0; + int cont_line_altcol = 0; for (;;) { c = tok_nextc(tok); if (c == ' ') { @@ -101,6 +102,7 @@ _PyLexer_BeginLine(struct tok_state *tok) // using backslashes. This means that if we found a backslash // preceded by whitespace, **the first one we find** determines // the level of indentation of whatever comes next. + cont_line_altcol = cont_line_col ? cont_line_altcol : altcol; cont_line_col = cont_line_col ? cont_line_col : col; if ((c = _PyLexer_ContinueLine(tok)) == -1) { return -1; @@ -136,7 +138,7 @@ _PyLexer_BeginLine(struct tok_state *tok) } if (!blankline && tok->level == 0) { col = cont_line_col ? cont_line_col : col; - altcol = cont_line_col ? cont_line_col : altcol; + altcol = cont_line_col ? cont_line_altcol : altcol; if (update_indentation(tok, col, altcol) < 0) { return -1; }