https://github.com/python/cpython/commit/aa0e4109a8681cbe6fb65e4072b0058e02adaef9
commit: aa0e4109a8681cbe6fb65e4072b0058e02adaef9
branch: main
author: Lysandros Nikolaou <[email protected]>
committer: lysnikolaou <[email protected]>
date: 2026-10-25T10:08:26-07:00
summary:

gh-153568: Use a managed dict for AST nodes (#160616)

AST nodes had an explicit `__dict__` slot, so every node built by
`ast.parse` allocated its own dict. With `Py_TPFLAGS_MANAGED_DICT`,
node attributes are stored as inline values with shared keys instead.

`ast.parse` is ~15.8% faster, walking trees is ~15.9% faster, and
the memory used by the AST trees is reduced by ~51.6%.

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-23-45-00.gh-issue-153568.astinl.rst
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-10-24-19-07-34.gh-issue-153568.6sVdqs.rst
M Lib/test/test_ast/test_ast.py
M Lib/test/test_sys.py
M Parser/asdl_c.py
M Python/Python-ast.c

diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py
index b1483ad6bf2d401..08d4cfb9ae6c8a2 100644
--- a/Lib/test/test_ast/test_ast.py
+++ b/Lib/test/test_ast/test_ast.py
@@ -103,6 +103,13 @@ def test_AST_objects(self):
             # "ast.AST constructor takes 0 positional arguments"
             ast.AST(2)
 
+    @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
+    def test_nodes_have_inline_values(self):
+        tree = ast.parse("x = f(y, z=1)")
+        for node in ast.walk(tree):
+            with self.subTest(node=type(node).__name__):
+                self.assertTrue(_testinternalcapi.has_inline_values(node))
+
     def test_AST_fields_NULL_check(self):
         # See: https://github.com/python/cpython/issues/126105
         old_value = ast.AST._fields
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 3de66a5508e69a4..201c209d8214150 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -2042,7 +2042,7 @@ def test_pythontypes(self):
         check = self.check_sizeof
         # _ast.AST
         import _ast
-        check(_ast.Module(), size('3P'))
+        check(_ast.Module(), size('2P'))
         try:
             raise TypeError
         except TypeError as e:
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-23-45-00.gh-issue-153568.astinl.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-23-45-00.gh-issue-153568.astinl.rst
new file mode 100644
index 000000000000000..a043d20b7a3408b
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-23-45-00.gh-issue-153568.astinl.rst
@@ -0,0 +1,2 @@
+Speed up :func:`ast.parse` and reduce the memory used by AST nodes by storing
+node attributes as inline values instead of a separate per-node dictionary.
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-24-19-07-34.gh-issue-153568.6sVdqs.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-24-19-07-34.gh-issue-153568.6sVdqs.rst
new file mode 100644
index 000000000000000..98628f6a74208f7
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-24-19-07-34.gh-issue-153568.6sVdqs.rst
@@ -0,0 +1,3 @@
+Speed up :func:`ast.parse` by ~15.8% and reduce the memory used by ~51.6% by
+storing node attributes as inline values instead of in a per-node
+dictionary.
diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py
index 2b83e97353bbf1f..edbb749f5ee231b 100755
--- a/Parser/asdl_c.py
+++ b/Parser/asdl_c.py
@@ -913,7 +913,6 @@ def visitModule(self, mod):
 
 typedef struct {
     PyObject_HEAD
-    PyObject *dict;
 } AST_object;
 
 static void
@@ -923,7 +922,7 @@ def visitModule(self, mod):
     /* bpo-31095: UnTrack is needed before calling any callbacks */
     PyTypeObject *tp = Py_TYPE(self);
     PyObject_GC_UnTrack(self);
-    Py_CLEAR(self->dict);
+    PyObject_ClearManagedDict(op);
     freefunc free_func = PyType_GetSlot(tp, Py_tp_free);
     assert(free_func != NULL);
     free_func(self);
@@ -935,15 +934,13 @@ def visitModule(self, mod):
 {
     AST_object *self = (AST_object*)op;
     Py_VISIT(Py_TYPE(self));
-    Py_VISIT(self->dict);
-    return 0;
+    return PyObject_VisitManagedDict(op, visit, arg);
 }
 
 static int
 ast_clear(PyObject *op)
 {
-    AST_object *self = (AST_object*)op;
-    Py_CLEAR(self->dict);
+    PyObject_ClearManagedDict(op);
     return 0;
 }
 
@@ -1419,11 +1416,6 @@ def visitModule(self, mod):
     return result;
 }
 
-static PyMemberDef ast_type_members[] = {
-    {"__dictoffset__", Py_T_PYSSIZET, offsetof(AST_object, dict), Py_READONLY},
-    {NULL}  /* Sentinel */
-};
-
 static PyMethodDef ast_type_methods[] = {
     {"__reduce__", ast_type_reduce, METH_NOARGS, NULL},
     {"__replace__", _PyCFunction_CAST(ast_type_replace), METH_VARARGS | 
METH_KEYWORDS,
@@ -1668,7 +1660,6 @@ def visitModule(self, mod):
     {Py_tp_setattro, PyObject_GenericSetAttr},
     {Py_tp_traverse, ast_traverse},
     {Py_tp_clear, ast_clear},
-    {Py_tp_members, ast_type_members},
     {Py_tp_methods, ast_type_methods},
     {Py_tp_getset, ast_type_getsets},
     {Py_tp_init, ast_type_init},
@@ -1682,7 +1673,8 @@ def visitModule(self, mod):
     "ast.AST",
     sizeof(AST_object),
     0,
-    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
+    Py_TPFLAGS_MANAGED_DICT,
     AST_type_slots
 };
 
diff --git a/Python/Python-ast.c b/Python/Python-ast.c
index 0d89f7e87f1fa2d..2c020e6c2070fc4 100644
--- a/Python/Python-ast.c
+++ b/Python/Python-ast.c
@@ -1414,7 +1414,6 @@ add_ast_annotations(struct ast_state *state)
 
 typedef struct {
     PyObject_HEAD
-    PyObject *dict;
 } AST_object;
 
 static void
@@ -1424,7 +1423,7 @@ ast_dealloc(PyObject *op)
     /* bpo-31095: UnTrack is needed before calling any callbacks */
     PyTypeObject *tp = Py_TYPE(self);
     PyObject_GC_UnTrack(self);
-    Py_CLEAR(self->dict);
+    PyObject_ClearManagedDict(op);
     freefunc free_func = PyType_GetSlot(tp, Py_tp_free);
     assert(free_func != NULL);
     free_func(self);
@@ -1436,15 +1435,13 @@ ast_traverse(PyObject *op, visitproc visit, void *arg)
 {
     AST_object *self = (AST_object*)op;
     Py_VISIT(Py_TYPE(self));
-    Py_VISIT(self->dict);
-    return 0;
+    return PyObject_VisitManagedDict(op, visit, arg);
 }
 
 static int
 ast_clear(PyObject *op)
 {
-    AST_object *self = (AST_object*)op;
-    Py_CLEAR(self->dict);
+    PyObject_ClearManagedDict(op);
     return 0;
 }
 
@@ -1920,11 +1917,6 @@ ast_type_replace(PyObject *self, PyObject *args, 
PyObject *kwargs)
     return result;
 }
 
-static PyMemberDef ast_type_members[] = {
-    {"__dictoffset__", Py_T_PYSSIZET, offsetof(AST_object, dict), Py_READONLY},
-    {NULL}  /* Sentinel */
-};
-
 static PyMethodDef ast_type_methods[] = {
     {"__reduce__", ast_type_reduce, METH_NOARGS, NULL},
     {"__replace__", _PyCFunction_CAST(ast_type_replace), METH_VARARGS | 
METH_KEYWORDS,
@@ -2169,7 +2161,6 @@ static PyType_Slot AST_type_slots[] = {
     {Py_tp_setattro, PyObject_GenericSetAttr},
     {Py_tp_traverse, ast_traverse},
     {Py_tp_clear, ast_clear},
-    {Py_tp_members, ast_type_members},
     {Py_tp_methods, ast_type_methods},
     {Py_tp_getset, ast_type_getsets},
     {Py_tp_init, ast_type_init},
@@ -2183,7 +2174,8 @@ static PyType_Spec AST_type_spec = {
     "ast.AST",
     sizeof(AST_object),
     0,
-    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
+    Py_TPFLAGS_MANAGED_DICT,
     AST_type_slots
 };
 

_______________________________________________
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]

Reply via email to