From 1b767fc4db5a3cac8a448f8b3f9f98e693376cd6 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 16 Aug 2025 13:09:30 -0400 Subject: [PATCH 1/7] Deprecate support for instantiating abstract AST nodes --- Doc/library/ast.rst | 11 +++- Doc/whatsnew/3.15.rst | 8 +++ Include/internal/pycore_ast_state.h | 1 + Lib/test/test_ast/test_ast.py | 32 ++++++++--- ...-08-16-12-56-08.gh-issue-116021.hMN9yw.rst | 2 + Parser/asdl_c.py | 42 ++++++++++++++ Python/Python-ast.c | 56 +++++++++++++++++++ 7 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 319b2c81505f48..210fa584269eb4 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -43,7 +43,7 @@ Node classes .. class:: AST - This is the base of all AST node classes. The actual node classes are + This is the abstract base of all AST node classes. The actual node classes are derived from the :file:`Parser/Python.asdl` file, which is reproduced :ref:`above `. They are defined in the :mod:`!_ast` C module and re-exported in :mod:`ast`. @@ -161,6 +161,15 @@ Node classes match any of the fields of the AST node. This behavior is deprecated and will be removed in Python 3.15. +.. deprecated-removed:: next 3.20 + + In the :ref:`grammar above `, the AST node classes that + correspond to production rules with variants (aka "sums") are abstract + classes. Previous versions of Python allowed for the creation of direct + instances of these abstract node classes. This behavior is deprecated and + will be removed in Python 3.20. + + .. note:: The descriptions of the specific node classes displayed here were initially adapted from the fantastic `Green Tree diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 252d8966b7450f..1e0ee6c043ba9e 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -408,6 +408,14 @@ module_name Deprecated ========== +ast +--- + +* Creating instances of abstract AST nodes (such as :class:`ast.AST` + or :class:`!ast.expr`) is deprecated and will raise an error in Python 3.20. + (Contributed by Brian Schubert in :gh:`116021`.) + + hashlib ------- diff --git a/Include/internal/pycore_ast_state.h b/Include/internal/pycore_ast_state.h index d4ac419f51d6b2..9627b15bd69c5b 100644 --- a/Include/internal/pycore_ast_state.h +++ b/Include/internal/pycore_ast_state.h @@ -161,6 +161,7 @@ struct ast_state { PyObject *__module__; PyObject *_attributes; PyObject *_fields; + PyObject *abstract_types; PyObject *alias_type; PyObject *annotation; PyObject *arg; diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 1e6f60074308e2..38e8a279109194 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -84,7 +84,9 @@ def _assertTrueorder(self, ast_node, parent_pos): self.assertEqual(ast_node._fields, ast_node.__match_args__) def test_AST_objects(self): - x = ast.AST() + # Directly instantiating abstract node class AST is allowed (but deprecated) + with self.assertWarns(DeprecationWarning): + x = ast.AST() self.assertEqual(x._fields, ()) x.foobar = 42 self.assertEqual(x.foobar, 42) @@ -93,7 +95,7 @@ def test_AST_objects(self): with self.assertRaises(AttributeError): x.vararg - with self.assertRaises(TypeError): + with self.assertRaises(TypeError), self.assertWarns(DeprecationWarning): # "ast.AST constructor takes 0 positional arguments" ast.AST(2) @@ -109,15 +111,15 @@ def cleanup(): msg = "type object 'ast.AST' has no attribute '_fields'" # Both examples used to crash: - with self.assertRaisesRegex(AttributeError, msg): + with self.assertRaisesRegex(AttributeError, msg), self.assertWarns(DeprecationWarning): ast.AST(arg1=123) - with self.assertRaisesRegex(AttributeError, msg): + with self.assertRaisesRegex(AttributeError, msg), self.assertWarns(DeprecationWarning): ast.AST() - def test_AST_garbage_collection(self): + def test_node_garbage_collection(self): class X: pass - a = ast.AST() + a = ast.Module() a.x = X() a.x.a = a ref = weakref.ref(a.x) @@ -427,7 +429,12 @@ def _construct_ast_class(self, cls): elif typ is object: kwargs[name] = b'capybara' elif isinstance(typ, type) and issubclass(typ, ast.AST): - kwargs[name] = self._construct_ast_class(typ) + if typ._is_abstract(): + # Use an arbitrary concrete subclass + concrete = next(sub for sub in typ.__subclasses__() if not sub._is_abstract()) + kwargs[name] = self._construct_ast_class(concrete) + else: + kwargs[name] = self._construct_ast_class(typ) return cls(**kwargs) def test_arguments(self): @@ -573,6 +580,10 @@ def test_nodeclasses(self): x = ast.BinOp(1, 2, 3, foobarbaz=42) self.assertEqual(x.foobarbaz, 42) + # Directly instantiating abstract node types is allowed (but deprecated) + with self.assertWarns(DeprecationWarning): + ast.stmt() + def test_no_fields(self): # this used to fail because Sub._fields was None x = ast.Sub() @@ -580,7 +591,9 @@ def test_no_fields(self): def test_invalid_sum(self): pos = dict(lineno=2, col_offset=3) - m = ast.Module([ast.Expr(ast.expr(**pos), **pos)], []) + with self.assertWarns(DeprecationWarning): + # Creating instances of ast.expr is deprecated + m = ast.Module([ast.Expr(ast.expr(**pos), **pos)], []) with self.assertRaises(TypeError) as cm: compile(m, "", "exec") self.assertIn("but got expr()", str(cm.exception)) @@ -1140,6 +1153,9 @@ def do(cls): return if cls is ast.Index: return + # Don't attempt to create instances of abstract AST nodes + if cls._is_abstract(): + return yield cls for sub in cls.__subclasses__(): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst new file mode 100644 index 00000000000000..967d8faaef3422 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst @@ -0,0 +1,2 @@ +Support for creating instances of abstract AST nodes from the :mod:`ast` module +is deprecated and scheduled for removal in Python 3.20. Patch by Brian Schubert. diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index dba20226c3283a..7bf2616b2ca3d6 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -881,6 +881,21 @@ def visitModule(self, mod): return -1; } + int contains = PySet_Contains(state->abstract_types, (PyObject *)Py_TYPE(self)); + if (contains == -1) { + return -1; + } + else if (contains == 1) { + if (PyErr_WarnFormat( + PyExc_DeprecationWarning, 1, + "Instantiating abstract AST node class %T is deprecated. " + "This will become an error in Python 3.20", + self + ) < 0) { + return -1; + } + } + Py_ssize_t i, numfields = 0; int res = -1; PyObject *key, *value, *fields, *attributes = NULL, *remaining_fields = NULL; @@ -1443,6 +1458,23 @@ def visitModule(self, mod): return result; } +/* Helper for checking if a node class is abstract in the tests. */ +PyObject * +ast_is_abstract(PyObject *cls, void *Py_UNUSED(ignored)) { + struct ast_state *state = get_ast_state(); + if (state == NULL) { + return NULL; + } + int contains = PySet_Contains(state->abstract_types, cls); + if (contains == -1) { + return NULL; + } + else if (contains == 1) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; +} + static PyMemberDef ast_type_members[] = { {"__dictoffset__", Py_T_PYSSIZET, offsetof(AST_object, dict), Py_READONLY}, {NULL} /* Sentinel */ @@ -1454,6 +1486,7 @@ def visitModule(self, mod): PyDoc_STR("__replace__($self, /, **fields)\\n--\\n\\n" "Return a copy of the AST node with new values " "for the specified fields.")}, + {"_is_abstract", _PyCFunction_CAST(ast_is_abstract), METH_CLASS | METH_NOARGS, NULL}, {NULL} }; @@ -1887,6 +1920,13 @@ def visitModule(self, mod): if (!state->AST_type) { return -1; } + state->abstract_types = PySet_New(NULL); + if (!state->abstract_types) { + return -1; + } + if (PySet_Add(state->abstract_types, state->AST_type) < 0) { + return -1; + } if (add_ast_fields(state) < 0) { return -1; } @@ -1928,6 +1968,7 @@ def visitSum(self, sum, name): (name, name, len(sum.attributes)), 1) else: self.emit("if (add_attributes(state, state->%s_type, NULL, 0) < 0) return -1;" % name, 1) + self.emit("if (PySet_Add(state->abstract_types, state->%s_type) < 0) return -1;" % name, 1) self.emit_defaults(name, sum.attributes, 1) simple = is_simple(sum) for t in sum.types: @@ -2289,6 +2330,7 @@ def generate_module_def(mod, metadata, f, internal_h): "%s_type" % type for type in metadata.types ) + module_state.add("abstract_types") state_strings = sorted(state_strings) module_state = sorted(module_state) diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 660bc598a4862c..0b0e3facee338d 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -178,6 +178,7 @@ void _PyAST_Fini(PyInterpreterState *interp) Py_CLEAR(state->__module__); Py_CLEAR(state->_attributes); Py_CLEAR(state->_fields); + Py_CLEAR(state->abstract_types); Py_CLEAR(state->alias_type); Py_CLEAR(state->annotation); Py_CLEAR(state->arg); @@ -5165,6 +5166,21 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) return -1; } + int contains = PySet_Contains(state->abstract_types, (PyObject *)Py_TYPE(self)); + if (contains == -1) { + return -1; + } + else if (contains == 1) { + if (PyErr_WarnFormat( + PyExc_DeprecationWarning, 1, + "Instantiating abstract AST node class %T is deprecated. " + "This will become an error in Python 3.20", + self + ) < 0) { + return -1; + } + } + Py_ssize_t i, numfields = 0; int res = -1; PyObject *key, *value, *fields, *attributes = NULL, *remaining_fields = NULL; @@ -5727,6 +5743,23 @@ ast_type_replace(PyObject *self, PyObject *args, PyObject *kwargs) return result; } +/* Helper for checking if a node class is abstract in the tests. */ +PyObject * +ast_is_abstract(PyObject *cls, void *Py_UNUSED(ignored)) { + struct ast_state *state = get_ast_state(); + if (state == NULL) { + return NULL; + } + int contains = PySet_Contains(state->abstract_types, cls); + if (contains == -1) { + return NULL; + } + else if (contains == 1) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; +} + static PyMemberDef ast_type_members[] = { {"__dictoffset__", Py_T_PYSSIZET, offsetof(AST_object, dict), Py_READONLY}, {NULL} /* Sentinel */ @@ -5738,6 +5771,7 @@ static PyMethodDef ast_type_methods[] = { PyDoc_STR("__replace__($self, /, **fields)\n--\n\n" "Return a copy of the AST node with new values " "for the specified fields.")}, + {"_is_abstract", _PyCFunction_CAST(ast_is_abstract), METH_CLASS | METH_NOARGS, NULL}, {NULL} }; @@ -6170,6 +6204,13 @@ init_types(void *arg) if (!state->AST_type) { return -1; } + state->abstract_types = PySet_New(NULL); + if (!state->abstract_types) { + return -1; + } + if (PySet_Add(state->abstract_types, state->AST_type) < 0) { + return -1; + } if (add_ast_fields(state) < 0) { return -1; } @@ -6180,6 +6221,7 @@ init_types(void *arg) " | FunctionType(expr* argtypes, expr returns)"); if (!state->mod_type) return -1; if (add_attributes(state, state->mod_type, NULL, 0) < 0) return -1; + if (PySet_Add(state->abstract_types, state->mod_type) < 0) return -1; state->Module_type = make_type(state, "Module", state->mod_type, Module_fields, 2, "Module(stmt* body, type_ignore* type_ignores)"); @@ -6229,6 +6271,7 @@ init_types(void *arg) if (!state->stmt_type) return -1; if (add_attributes(state, state->stmt_type, stmt_attributes, 4) < 0) return -1; + if (PySet_Add(state->abstract_types, state->stmt_type) < 0) return -1; if (PyObject_SetAttr(state->stmt_type, state->end_lineno, Py_None) == -1) return -1; if (PyObject_SetAttr(state->stmt_type, state->end_col_offset, Py_None) == @@ -6414,6 +6457,7 @@ init_types(void *arg) if (!state->expr_type) return -1; if (add_attributes(state, state->expr_type, expr_attributes, 4) < 0) return -1; + if (PySet_Add(state->abstract_types, state->expr_type) < 0) return -1; if (PyObject_SetAttr(state->expr_type, state->end_lineno, Py_None) == -1) return -1; if (PyObject_SetAttr(state->expr_type, state->end_col_offset, Py_None) == @@ -6558,6 +6602,8 @@ init_types(void *arg) "expr_context = Load | Store | Del"); if (!state->expr_context_type) return -1; if (add_attributes(state, state->expr_context_type, NULL, 0) < 0) return -1; + if (PySet_Add(state->abstract_types, state->expr_context_type) < 0) return + -1; state->Load_type = make_type(state, "Load", state->expr_context_type, NULL, 0, "Load"); @@ -6582,6 +6628,7 @@ init_types(void *arg) "boolop = And | Or"); if (!state->boolop_type) return -1; if (add_attributes(state, state->boolop_type, NULL, 0) < 0) return -1; + if (PySet_Add(state->abstract_types, state->boolop_type) < 0) return -1; state->And_type = make_type(state, "And", state->boolop_type, NULL, 0, "And"); if (!state->And_type) return -1; @@ -6599,6 +6646,7 @@ init_types(void *arg) "operator = Add | Sub | Mult | MatMult | Div | Mod | Pow | LShift | RShift | BitOr | BitXor | BitAnd | FloorDiv"); if (!state->operator_type) return -1; if (add_attributes(state, state->operator_type, NULL, 0) < 0) return -1; + if (PySet_Add(state->abstract_types, state->operator_type) < 0) return -1; state->Add_type = make_type(state, "Add", state->operator_type, NULL, 0, "Add"); if (!state->Add_type) return -1; @@ -6693,6 +6741,7 @@ init_types(void *arg) "unaryop = Invert | Not | UAdd | USub"); if (!state->unaryop_type) return -1; if (add_attributes(state, state->unaryop_type, NULL, 0) < 0) return -1; + if (PySet_Add(state->abstract_types, state->unaryop_type) < 0) return -1; state->Invert_type = make_type(state, "Invert", state->unaryop_type, NULL, 0, "Invert"); @@ -6723,6 +6772,7 @@ init_types(void *arg) "cmpop = Eq | NotEq | Lt | LtE | Gt | GtE | Is | IsNot | In | NotIn"); if (!state->cmpop_type) return -1; if (add_attributes(state, state->cmpop_type, NULL, 0) < 0) return -1; + if (PySet_Add(state->abstract_types, state->cmpop_type) < 0) return -1; state->Eq_type = make_type(state, "Eq", state->cmpop_type, NULL, 0, "Eq"); if (!state->Eq_type) return -1; @@ -6796,6 +6846,8 @@ init_types(void *arg) if (!state->excepthandler_type) return -1; if (add_attributes(state, state->excepthandler_type, excepthandler_attributes, 4) < 0) return -1; + if (PySet_Add(state->abstract_types, state->excepthandler_type) < 0) return + -1; if (PyObject_SetAttr(state->excepthandler_type, state->end_lineno, Py_None) == -1) return -1; @@ -6886,6 +6938,7 @@ init_types(void *arg) if (!state->pattern_type) return -1; if (add_attributes(state, state->pattern_type, pattern_attributes, 4) < 0) return -1; + if (PySet_Add(state->abstract_types, state->pattern_type) < 0) return -1; state->MatchValue_type = make_type(state, "MatchValue", state->pattern_type, MatchValue_fields, 1, @@ -6936,6 +6989,8 @@ init_types(void *arg) "type_ignore = TypeIgnore(int lineno, string tag)"); if (!state->type_ignore_type) return -1; if (add_attributes(state, state->type_ignore_type, NULL, 0) < 0) return -1; + if (PySet_Add(state->abstract_types, state->type_ignore_type) < 0) return + -1; state->TypeIgnore_type = make_type(state, "TypeIgnore", state->type_ignore_type, TypeIgnore_fields, 2, @@ -6949,6 +7004,7 @@ init_types(void *arg) if (!state->type_param_type) return -1; if (add_attributes(state, state->type_param_type, type_param_attributes, 4) < 0) return -1; + if (PySet_Add(state->abstract_types, state->type_param_type) < 0) return -1; state->TypeVar_type = make_type(state, "TypeVar", state->type_param_type, TypeVar_fields, 3, "TypeVar(identifier name, expr? bound, expr? default_value)"); From b72a3628fca8016d1957553bda6a7810d1d14329 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 16 Aug 2025 13:52:55 -0400 Subject: [PATCH 2/7] Missing static --- Parser/asdl_c.py | 2 +- Python/Python-ast.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index 7bf2616b2ca3d6..2b28f18ea4835f 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -1459,7 +1459,7 @@ def visitModule(self, mod): } /* Helper for checking if a node class is abstract in the tests. */ -PyObject * +static PyObject * ast_is_abstract(PyObject *cls, void *Py_UNUSED(ignored)) { struct ast_state *state = get_ast_state(); if (state == NULL) { diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 0b0e3facee338d..3bf58d994a74fa 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5744,7 +5744,7 @@ ast_type_replace(PyObject *self, PyObject *args, PyObject *kwargs) } /* Helper for checking if a node class is abstract in the tests. */ -PyObject * +static PyObject * ast_is_abstract(PyObject *cls, void *Py_UNUSED(ignored)) { struct ast_state *state = get_ast_state(); if (state == NULL) { From c8dda97b2fc108fb0f3b257ba373ca769954c242 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 16 Aug 2025 13:56:13 -0400 Subject: [PATCH 3/7] Capture warning in test_sys --- Lib/test/test_sys.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index f89237931b7185..bbf4f20107bdcf 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1877,7 +1877,8 @@ def test_pythontypes(self): check = self.check_sizeof # _ast.AST import _ast - check(_ast.AST(), size('P')) + with self.assertWarns(DeprecationWarning): + check(_ast.AST(), size('P')) try: raise TypeError except TypeError as e: From e550c2a784b736e212117ef06c29b3ee64075b75 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 18 Aug 2025 17:06:38 -0400 Subject: [PATCH 4/7] Convert _is_abstract to module function, address misc review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_ast/test_ast.py | 37 ++++++++++++++++-------- Parser/asdl_c.py | 54 +++++++++++++++++++---------------- Python/Python-ast.c | 52 +++++++++++++++++---------------- 3 files changed, 83 insertions(+), 60 deletions(-) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 38e8a279109194..6aa9c1899cc9ba 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -1,4 +1,5 @@ import _ast_unparse +import _ast import ast import builtins import contextlib @@ -111,9 +112,15 @@ def cleanup(): msg = "type object 'ast.AST' has no attribute '_fields'" # Both examples used to crash: - with self.assertRaisesRegex(AttributeError, msg), self.assertWarns(DeprecationWarning): + with ( + self.assertRaisesRegex(AttributeError, msg), + self.assertWarns(DeprecationWarning), + ): ast.AST(arg1=123) - with self.assertRaisesRegex(AttributeError, msg), self.assertWarns(DeprecationWarning): + with ( + self.assertRaisesRegex(AttributeError, msg), + self.assertWarns(DeprecationWarning), + ): ast.AST() def test_node_garbage_collection(self): @@ -429,12 +436,15 @@ def _construct_ast_class(self, cls): elif typ is object: kwargs[name] = b'capybara' elif isinstance(typ, type) and issubclass(typ, ast.AST): - if typ._is_abstract(): + if _ast._is_abstract(typ): # Use an arbitrary concrete subclass - concrete = next(sub for sub in typ.__subclasses__() if not sub._is_abstract()) - kwargs[name] = self._construct_ast_class(concrete) + concrete = next((sub for sub in typ.__subclasses__() + if not _ast._is_abstract(sub)), None) + msg = f"abstract node class {typ} has no concrete subclasses" + self.assertIsNotNone(concrete, msg) else: - kwargs[name] = self._construct_ast_class(typ) + concrete = typ + kwargs[name] = self._construct_ast_class(concrete) return cls(**kwargs) def test_arguments(self): @@ -581,8 +591,8 @@ def test_nodeclasses(self): self.assertEqual(x.foobarbaz, 42) # Directly instantiating abstract node types is allowed (but deprecated) - with self.assertWarns(DeprecationWarning): - ast.stmt() + self.assertWarns(DeprecationWarning, ast.stmt) + self.assertWarns(DeprecationWarning, ast.expr_context) def test_no_fields(self): # this used to fail because Sub._fields was None @@ -593,7 +603,8 @@ def test_invalid_sum(self): pos = dict(lineno=2, col_offset=3) with self.assertWarns(DeprecationWarning): # Creating instances of ast.expr is deprecated - m = ast.Module([ast.Expr(ast.expr(**pos), **pos)], []) + e = ast.expr(**pos) + m = ast.Module([ast.Expr(e, **pos)], []) with self.assertRaises(TypeError) as cm: compile(m, "", "exec") self.assertIn("but got expr()", str(cm.exception)) @@ -1145,8 +1156,10 @@ class CopyTests(unittest.TestCase): def iter_ast_classes(): """Iterate over the (native) subclasses of ast.AST recursively. - This excludes the special class ast.Index since its constructor - returns an integer. + This excludes: + * abstract AST nodes + * the special class ast.Index, since its constructor returns + an integer. """ def do(cls): if cls.__module__ != 'ast': @@ -1154,7 +1167,7 @@ def do(cls): if cls is ast.Index: return # Don't attempt to create instances of abstract AST nodes - if cls._is_abstract(): + if _ast._is_abstract(cls): return yield cls diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index 2b28f18ea4835f..2b03fd1760127e 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -887,11 +887,10 @@ def visitModule(self, mod): } else if (contains == 1) { if (PyErr_WarnFormat( - PyExc_DeprecationWarning, 1, - "Instantiating abstract AST node class %T is deprecated. " - "This will become an error in Python 3.20", - self - ) < 0) { + PyExc_DeprecationWarning, 1, + "Instantiating abstract AST node class %T is deprecated. " + "This will become an error in Python 3.20", self) < 0) + { return -1; } } @@ -1458,23 +1457,6 @@ def visitModule(self, mod): return result; } -/* Helper for checking if a node class is abstract in the tests. */ -static PyObject * -ast_is_abstract(PyObject *cls, void *Py_UNUSED(ignored)) { - struct ast_state *state = get_ast_state(); - if (state == NULL) { - return NULL; - } - int contains = PySet_Contains(state->abstract_types, cls); - if (contains == -1) { - return NULL; - } - else if (contains == 1) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; -} - static PyMemberDef ast_type_members[] = { {"__dictoffset__", Py_T_PYSSIZET, offsetof(AST_object, dict), Py_READONLY}, {NULL} /* Sentinel */ @@ -1486,7 +1468,6 @@ def visitModule(self, mod): PyDoc_STR("__replace__($self, /, **fields)\\n--\\n\\n" "Return a copy of the AST node with new values " "for the specified fields.")}, - {"_is_abstract", _PyCFunction_CAST(ast_is_abstract), METH_CLASS | METH_NOARGS, NULL}, {NULL} }; @@ -2001,6 +1982,30 @@ def emit_defaults(self, name, fields, depth): class ASTModuleVisitor(PickleVisitor): def visitModule(self, mod): + self.emit(""" +/* Helper for checking if a node class is abstract in the tests. */ +static PyObject * +ast_is_abstract(PyObject *Py_UNUSED(module), PyObject *cls) { + struct ast_state *state = get_ast_state(); + if (state == NULL) { + return NULL; + } + int contains = PySet_Contains(state->abstract_types, cls); + if (contains == -1) { + return NULL; + } + else if (contains == 1) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; +} + +static struct PyMethodDef astmodule_methods[] = { + {"_is_abstract", ast_is_abstract, METH_O, NULL}, + {NULL} /* Sentinel */ +}; +""".strip(), 0, reflow=False) + self.emit("", 0) self.emit("static int", 0) self.emit("astmodule_exec(PyObject *m)", 0) self.emit("{", 0) @@ -2041,7 +2046,8 @@ def visitModule(self, mod): .m_name = "_ast", // The _ast module uses a per-interpreter state (PyInterpreterState.ast) .m_size = 0, - .m_slots = astmodule_slots, + .m_methods = astmodule_methods, + .m_slots = astmodule_slots }; PyMODINIT_FUNC diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 3bf58d994a74fa..83a43b3c7a9fe5 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5172,11 +5172,10 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) } else if (contains == 1) { if (PyErr_WarnFormat( - PyExc_DeprecationWarning, 1, - "Instantiating abstract AST node class %T is deprecated. " - "This will become an error in Python 3.20", - self - ) < 0) { + PyExc_DeprecationWarning, 1, + "Instantiating abstract AST node class %T is deprecated. " + "This will become an error in Python 3.20", self) < 0) + { return -1; } } @@ -5743,23 +5742,6 @@ ast_type_replace(PyObject *self, PyObject *args, PyObject *kwargs) return result; } -/* Helper for checking if a node class is abstract in the tests. */ -static PyObject * -ast_is_abstract(PyObject *cls, void *Py_UNUSED(ignored)) { - struct ast_state *state = get_ast_state(); - if (state == NULL) { - return NULL; - } - int contains = PySet_Contains(state->abstract_types, cls); - if (contains == -1) { - return NULL; - } - else if (contains == 1) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; -} - static PyMemberDef ast_type_members[] = { {"__dictoffset__", Py_T_PYSSIZET, offsetof(AST_object, dict), Py_READONLY}, {NULL} /* Sentinel */ @@ -5771,7 +5753,6 @@ static PyMethodDef ast_type_methods[] = { PyDoc_STR("__replace__($self, /, **fields)\n--\n\n" "Return a copy of the AST node with new values " "for the specified fields.")}, - {"_is_abstract", _PyCFunction_CAST(ast_is_abstract), METH_CLASS | METH_NOARGS, NULL}, {NULL} }; @@ -18033,6 +18014,28 @@ obj2ast_type_param(struct ast_state *state, PyObject* obj, type_param_ty* out, } +/* Helper for checking if a node class is abstract in the tests. */ +static PyObject * +ast_is_abstract(PyObject *Py_UNUSED(module), PyObject *cls) { + struct ast_state *state = get_ast_state(); + if (state == NULL) { + return NULL; + } + int contains = PySet_Contains(state->abstract_types, cls); + if (contains == -1) { + return NULL; + } + else if (contains == 1) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; +} + +static struct PyMethodDef astmodule_methods[] = { + {"_is_abstract", ast_is_abstract, METH_O, NULL}, + {NULL} /* Sentinel */ +}; + static int astmodule_exec(PyObject *m) { @@ -18458,7 +18461,8 @@ static struct PyModuleDef _astmodule = { .m_name = "_ast", // The _ast module uses a per-interpreter state (PyInterpreterState.ast) .m_size = 0, - .m_slots = astmodule_slots, + .m_methods = astmodule_methods, + .m_slots = astmodule_slots }; PyMODINIT_FUNC From 90b742869e284530a75a94fb5c31dcf563b44cc2 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 8 Sep 2025 17:23:00 -0400 Subject: [PATCH 5/7] Retarget removal for Python 3.19 3.14 + 5 = 3.19 --- Doc/library/ast.rst | 4 ++-- Doc/whatsnew/3.15.rst | 2 +- .../2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst | 2 +- Parser/asdl_c.py | 2 +- Python/Python-ast.c | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 210fa584269eb4..9d3a83d25cd6d9 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -161,13 +161,13 @@ Node classes match any of the fields of the AST node. This behavior is deprecated and will be removed in Python 3.15. -.. deprecated-removed:: next 3.20 +.. deprecated-removed:: next 3.19 In the :ref:`grammar above `, the AST node classes that correspond to production rules with variants (aka "sums") are abstract classes. Previous versions of Python allowed for the creation of direct instances of these abstract node classes. This behavior is deprecated and - will be removed in Python 3.20. + will be removed in Python 3.19. .. note:: diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 1e0ee6c043ba9e..a0139599b6980d 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -412,7 +412,7 @@ ast --- * Creating instances of abstract AST nodes (such as :class:`ast.AST` - or :class:`!ast.expr`) is deprecated and will raise an error in Python 3.20. + or :class:`!ast.expr`) is deprecated and will raise an error in Python 3.19. (Contributed by Brian Schubert in :gh:`116021`.) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst index 967d8faaef3422..cfb9f974f244e9 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst @@ -1,2 +1,2 @@ Support for creating instances of abstract AST nodes from the :mod:`ast` module -is deprecated and scheduled for removal in Python 3.20. Patch by Brian Schubert. +is deprecated and scheduled for removal in Python 3.19. Patch by Brian Schubert. diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index 2b03fd1760127e..8807a5a7f527a4 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -889,7 +889,7 @@ def visitModule(self, mod): if (PyErr_WarnFormat( PyExc_DeprecationWarning, 1, "Instantiating abstract AST node class %T is deprecated. " - "This will become an error in Python 3.20", self) < 0) + "This will become an error in Python 3.19", self) < 0) { return -1; } diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 83a43b3c7a9fe5..4c327ec4f64a79 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5174,7 +5174,7 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) if (PyErr_WarnFormat( PyExc_DeprecationWarning, 1, "Instantiating abstract AST node class %T is deprecated. " - "This will become an error in Python 3.20", self) < 0) + "This will become an error in Python 3.19", self) < 0) { return -1; } From 16db374f697ccd749f97f3a33958e630026575d9 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 8 Sep 2025 17:28:35 -0400 Subject: [PATCH 6/7] Add pending-removal-in-3.19.rst mention --- Doc/deprecations/pending-removal-in-3.19.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Doc/deprecations/pending-removal-in-3.19.rst b/Doc/deprecations/pending-removal-in-3.19.rst index 25f9cba390de68..54ef70d512dcf6 100644 --- a/Doc/deprecations/pending-removal-in-3.19.rst +++ b/Doc/deprecations/pending-removal-in-3.19.rst @@ -1,6 +1,11 @@ Pending removal in Python 3.19 ------------------------------ +* :mod:`ast`: + + * Creating instances of abstract AST nodes (such as :class:`ast.AST` + or :class:`!ast.expr`) is deprecated and will raise an error in Python 3.19. + * :mod:`ctypes`: * Implicitly switching to the MSVC-compatible struct layout by setting From 66586e724190c5604f5a8ec7b541810f4e0c3dfb Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 13 Oct 2025 09:53:17 -0400 Subject: [PATCH 7/7] Retarget removal for Python 3.20 --- Doc/deprecations/pending-removal-in-3.19.rst | 5 ----- Doc/deprecations/pending-removal-in-3.20.rst | 5 +++++ Doc/library/ast.rst | 4 ++-- Doc/whatsnew/3.15.rst | 2 +- .../2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst | 2 +- Parser/asdl_c.py | 2 +- Python/Python-ast.c | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Doc/deprecations/pending-removal-in-3.19.rst b/Doc/deprecations/pending-removal-in-3.19.rst index 54ef70d512dcf6..25f9cba390de68 100644 --- a/Doc/deprecations/pending-removal-in-3.19.rst +++ b/Doc/deprecations/pending-removal-in-3.19.rst @@ -1,11 +1,6 @@ Pending removal in Python 3.19 ------------------------------ -* :mod:`ast`: - - * Creating instances of abstract AST nodes (such as :class:`ast.AST` - or :class:`!ast.expr`) is deprecated and will raise an error in Python 3.19. - * :mod:`ctypes`: * Implicitly switching to the MSVC-compatible struct layout by setting diff --git a/Doc/deprecations/pending-removal-in-3.20.rst b/Doc/deprecations/pending-removal-in-3.20.rst index 8bc863b185d921..c32ef51ce475a4 100644 --- a/Doc/deprecations/pending-removal-in-3.20.rst +++ b/Doc/deprecations/pending-removal-in-3.20.rst @@ -21,3 +21,8 @@ Pending removal in Python 3.20 - :mod:`tkinter.ttk` (Contributed by Hugo van Kemenade in :gh:`76007`.) + +* :mod:`ast`: + + * Creating instances of abstract AST nodes (such as :class:`ast.AST` + or :class:`!ast.expr`) is deprecated and will raise an error in Python 3.20. diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 9d3a83d25cd6d9..210fa584269eb4 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -161,13 +161,13 @@ Node classes match any of the fields of the AST node. This behavior is deprecated and will be removed in Python 3.15. -.. deprecated-removed:: next 3.19 +.. deprecated-removed:: next 3.20 In the :ref:`grammar above `, the AST node classes that correspond to production rules with variants (aka "sums") are abstract classes. Previous versions of Python allowed for the creation of direct instances of these abstract node classes. This behavior is deprecated and - will be removed in Python 3.19. + will be removed in Python 3.20. .. note:: diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index a02fa421562aa2..93f7e779f46544 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -609,7 +609,7 @@ ast --- * Creating instances of abstract AST nodes (such as :class:`ast.AST` - or :class:`!ast.expr`) is deprecated and will raise an error in Python 3.19. + or :class:`!ast.expr`) is deprecated and will raise an error in Python 3.20. (Contributed by Brian Schubert in :gh:`116021`.) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst index cfb9f974f244e9..967d8faaef3422 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-16-12-56-08.gh-issue-116021.hMN9yw.rst @@ -1,2 +1,2 @@ Support for creating instances of abstract AST nodes from the :mod:`ast` module -is deprecated and scheduled for removal in Python 3.19. Patch by Brian Schubert. +is deprecated and scheduled for removal in Python 3.20. Patch by Brian Schubert. diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index 8807a5a7f527a4..2b03fd1760127e 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -889,7 +889,7 @@ def visitModule(self, mod): if (PyErr_WarnFormat( PyExc_DeprecationWarning, 1, "Instantiating abstract AST node class %T is deprecated. " - "This will become an error in Python 3.19", self) < 0) + "This will become an error in Python 3.20", self) < 0) { return -1; } diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 4c327ec4f64a79..83a43b3c7a9fe5 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5174,7 +5174,7 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw) if (PyErr_WarnFormat( PyExc_DeprecationWarning, 1, "Instantiating abstract AST node class %T is deprecated. " - "This will become an error in Python 3.19", self) < 0) + "This will become an error in Python 3.20", self) < 0) { return -1; }