From 7fdeed53a45d1e685a397752e7cf616e295cdc69 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 5 Mar 2026 16:25:03 +0000 Subject: [PATCH 1/3] Fix crash in AST unparser when unparsing dict comprehension unpacking --- Doc/conf.py | 1 + Lib/_ast_unparse.py | 11 ++++++++--- Lib/test/test_future_stmt/test_future.py | 1 + Lib/test/test_unparse.py | 4 ++++ .../2026-03-05-16-16-17.gh-issue-143055.qDUFlY.rst | 2 ++ Python/ast_unparse.c | 12 +++++++++--- 6 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-03-05-16-16-17.gh-issue-143055.qDUFlY.rst diff --git a/Doc/conf.py b/Doc/conf.py index d7effe2572ec44..f147f9b083c04a 100644 --- a/Doc/conf.py +++ b/Doc/conf.py @@ -555,6 +555,7 @@ # mapping unique short aliases to a base URL and a prefix. # https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html extlinks = { + "oss-fuzz": ("https://issues.oss-fuzz.com/issues/%s", "#%s"), "pypi": ("https://pypi.org/project/%s/", "%s"), "source": (SOURCE_URI, "%s"), } diff --git a/Lib/_ast_unparse.py b/Lib/_ast_unparse.py index 0c3b1d3a9108a3..c894feab943e4a 100644 --- a/Lib/_ast_unparse.py +++ b/Lib/_ast_unparse.py @@ -738,9 +738,14 @@ def visit_SetComp(self, node): def visit_DictComp(self, node): with self.delimit("{", "}"): - self.traverse(node.key) - self.write(": ") - self.traverse(node.value) + if node.value: + self.traverse(node.key) + self.write(": ") + self.traverse(node.value) + else: + self.write("**") + self.set_precedence(_Precedence.EXPR, node.key) + self.traverse(node.key) for gen in node.generators: self.traverse(gen) diff --git a/Lib/test/test_future_stmt/test_future.py b/Lib/test/test_future_stmt/test_future.py index 71f1e616116d81..55a9d52c78c3ef 100644 --- a/Lib/test/test_future_stmt/test_future.py +++ b/Lib/test/test_future_stmt/test_future.py @@ -349,6 +349,7 @@ def test_annotations(self): eq("(i ** 2 + j for i in (1, 2, 3) for j in (1, 2, 3))") eq("{i: 0 for i in (1, 2, 3)}") eq("{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))}") + eq("{**x for x in ()}") eq("[(x, y) for x, y in (a, b)]") eq("[(x,) for x, in (a,)]") eq("Python3 > Python2 > COBOL") diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py index 35e4652a87b423..fab6fadb72e836 100644 --- a/Lib/test/test_unparse.py +++ b/Lib/test/test_unparse.py @@ -403,6 +403,10 @@ def test_set_comprehension(self): def test_dict_comprehension(self): self.check_ast_roundtrip("{x: x*x for x in range(10)}") + def test_dict_comprehension_unpacking(self): + self.check_ast_roundtrip("{**x for x in ()}") + self.check_ast_roundtrip("{**x for x in range(10)}") + def test_class_decorators(self): self.check_ast_roundtrip(class_decorator) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-05-16-16-17.gh-issue-143055.qDUFlY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-05-16-16-17.gh-issue-143055.qDUFlY.rst new file mode 100644 index 00000000000000..9b55459bffdea8 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-05-16-16-17.gh-issue-143055.qDUFlY.rst @@ -0,0 +1,2 @@ +Fix crash in AST unparser when unparsing dict comprehension unpacking. +Found by OSS Fuzz in :oss-fuzz:`489790200`. diff --git a/Python/ast_unparse.c b/Python/ast_unparse.c index c25699978cf651..24fc5cb734c3f3 100644 --- a/Python/ast_unparse.c +++ b/Python/ast_unparse.c @@ -464,9 +464,15 @@ static int append_ast_dictcomp(PyUnicodeWriter *writer, expr_ty e) { APPEND_CHAR('{'); - APPEND_EXPR(e->v.DictComp.key, PR_TEST); - APPEND_STR(": "); - APPEND_EXPR(e->v.DictComp.value, PR_TEST); + if (e->v.DictComp.value) { + APPEND_EXPR(e->v.DictComp.key, PR_TEST); + APPEND_STR(": "); + APPEND_EXPR(e->v.DictComp.value, PR_TEST); + } + else { + APPEND_STR("**"); + APPEND_EXPR(e->v.DictComp.key, PR_EXPR); + } APPEND(comprehensions, e->v.DictComp.generators); APPEND_CHAR_FINISH('}'); } From 78adfb853e68b181186b03351f716ed0e1a19ab0 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 5 Mar 2026 16:36:32 +0000 Subject: [PATCH 2/3] I was overthinking it. --- Lib/_ast_unparse.py | 1 - Python/ast_unparse.c | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/_ast_unparse.py b/Lib/_ast_unparse.py index c894feab943e4a..916bb25d74dee9 100644 --- a/Lib/_ast_unparse.py +++ b/Lib/_ast_unparse.py @@ -744,7 +744,6 @@ def visit_DictComp(self, node): self.traverse(node.value) else: self.write("**") - self.set_precedence(_Precedence.EXPR, node.key) self.traverse(node.key) for gen in node.generators: self.traverse(gen) diff --git a/Python/ast_unparse.c b/Python/ast_unparse.c index 24fc5cb734c3f3..6050c351cff68f 100644 --- a/Python/ast_unparse.c +++ b/Python/ast_unparse.c @@ -471,7 +471,7 @@ append_ast_dictcomp(PyUnicodeWriter *writer, expr_ty e) } else { APPEND_STR("**"); - APPEND_EXPR(e->v.DictComp.key, PR_EXPR); + APPEND_EXPR(e->v.DictComp.key, PR_TEST); } APPEND(comprehensions, e->v.DictComp.generators); APPEND_CHAR_FINISH('}'); From 727721adb3c52f39ed649cef9e1c06c48ec454eb Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 5 Mar 2026 16:53:19 +0000 Subject: [PATCH 3/3] test list comp too --- Lib/test/test_future_stmt/test_future.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_future_stmt/test_future.py b/Lib/test/test_future_stmt/test_future.py index 55a9d52c78c3ef..faa3a2bfe121dc 100644 --- a/Lib/test/test_future_stmt/test_future.py +++ b/Lib/test/test_future_stmt/test_future.py @@ -350,6 +350,7 @@ def test_annotations(self): eq("{i: 0 for i in (1, 2, 3)}") eq("{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))}") eq("{**x for x in ()}") + eq("[*x for x in ()]") eq("[(x, y) for x, y in (a, b)]") eq("[(x,) for x, in (a,)]") eq("Python3 > Python2 > COBOL")