Skip to content

gh-116021: Deprecate support for instantiating abstract AST nodes #137865

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

brianschubert
Copy link
Member

@brianschubert brianschubert commented Aug 16, 2025

Demonstration:

>>> ast.stmt()
<python-input-1>:1: DeprecationWarning: Instantiating abstract AST node class ast.stmt is deprecated. This will become an error in Python 3.20
stmt()

>>> class MyStmt(ast.stmt): pass
... 
>>> MyStmt()
MyStmt()

This tracks which AST nodes are abstract using an internal set that gets built during node type initialization. I tried a few different approaches, and this seemed the simplest. A nice thing about this approach is that the check for abstract-ness depends on the exact node type, so this avoids most inheritance subtleties that could interfere with existing user-defined subclasses.


📚 Documentation preview 📚: https://cpython-previews--137865.org.readthedocs.build/

Comment on lines -1880 to +1881
check(_ast.AST(), size('P'))
with self.assertWarns(DeprecationWarning):
check(_ast.AST(), size('P'))
Copy link
Member Author

@brianschubert brianschubert Aug 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should probably be rewritten, but I'm not sure I understand the goal of this test well enough to do that. Suggestions welcome!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's to check that objects in Python/ have the correct types. I don't know why it's actually in test_sys though. I think we should split this test and move each check to the module it belongs (ast.AST's size should be checked in test_ast).

For now, leave it here. A separate issue should be opened.

Copy link
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be easier to have a module function checking if a node is abstract? say _ast.is_abstract(node) (in the C module, not the Python module). That way we don't need an additional method on AST objects (which could be shadowed).

Comment on lines -1880 to +1881
check(_ast.AST(), size('P'))
with self.assertWarns(DeprecationWarning):
check(_ast.AST(), size('P'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's to check that objects in Python/ have the correct types. I don't know why it's actually in test_sys though. I think we should split this test and move each check to the module it belongs (ast.AST's size should be checked in test_ast).

For now, leave it here. A separate issue should be opened.

Comment on lines +889 to +896
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;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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;
}
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;
}

for PEP-8

@@ -1443,6 +1458,23 @@ def visitModule(self, mod):
return result;
}
/* Helper for checking if a node class is abstract in the tests. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use clinic here, but this would create a new file as we don't use it at all. Maybe we can do it in a follow-up PR though.

@@ -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},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the signature of the function from PyObject (*)(PyObject *, void *) to PyObject (*)(PyObject *, PyObject *) and remove the cast. The second argument is always NULL so you can name it dummy.

@@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
with self.assertRaisesRegex(AttributeError, msg), self.assertWarns(DeprecationWarning):
with (
self.assertRaisesRegex(AttributeError, msg),
self.assertWarns(DeprecationWarning),
):

and ditto below, just to warp at 80 chars.

Comment on lines +432 to +437
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe ensure that "concrete" exists? also you could do concrete = typ and remove the else: and only call self._construct_ast_class once.

@@ -573,14 +580,20 @@ 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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless we need a variable, and unless we need to fit on 80 chars, I would suggest using self.assertWarns(DeprecationWarning, ast.stmt). In 3.20, we can just do self.assertRaises(..., ast.stmt). This reduces the overall file length without necessarily reducing readability IMO.

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)], [])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just wrap ast.expr() as being the deprecated one? just to be sure we're not catching another deprecation warning

@@ -1140,6 +1153,9 @@ def do(cls):
return
if cls is ast.Index:
return
# Don't attempt to create instances of abstract AST nodes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also update the docstring saying that we also skip abstract classes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants