In the current implementation of the ast module, AST node constructors (ast.AST subclasses) accept arbitrary keyword arguments and do not enforce any type validation on those arguments. As noted in the existing Discourse thread:
Values passed to fields are not type-checked. For example, one could pass a non-AST object or a wrong type for a field that expects a list of AST nodes.
Invalid or semantically broken ASTs can be constructed, causing errors only later (e.g., during compile, unparse, or traversal), which makes debugging harder.
While there has already been work to improve AST constructors (e.g., setting sensible defaults for fields, warning for unknown kwargs, adding __text_signature__) via PR [gh‑105858], Discussions the current design still lacks type validation of passed-in values.
Type checking at construction time would catch many mistakes earlier, improving both safety and developer experience, especially for tools that build or manipulate ASTs.
I propose adding optional runtime type-checking inside AST node constructors (__init__) for ast.AST subclasses. The main goals:
Use _field_typesto check that values passed to fields match the expected types.
For example: if a field expects list[stmt], ensure the caller passed an iterable / sequence and that each element is an instance of the correct AST class.
For simple types (e.g., strings, ints), check accordingly.
Wouldn’t that introduce a (somewhat big) runtime overhead?
How do you plan to turn it on/off? If it is off by default, most potential with helping to catch bugs is just gone, because it is unlikely people will remember to turn it on. If it is on by default, there is a runtime overhead by default.
So you check some all(isinstance(el, tp) for el in it)? That would again introduce more runtime overhead, as we would need to iterate over some iterable just for checking types.
IMO, just using type-hints and type-checkers should be enough. The ast module is (most likely) used in professional code, where type checkers are already very likely used.
Why would ast be different than any other library not providing runtime type-checking? Most libs rely on static type-checking only, and this seems to work
So you want to toggle something via an import statement? That would work, but it would both be confusing, and it would mean that we have to create an unnecessary clone of most ast functionality, just for that.
Sadly, it also means you can’t exactly tell, wether the type-checking part is turned on or off, just by reading a ast.<attr> statement. (Although it is good that the names for attributes would stay the same).
Thanks! I now notice that VSCode provides stubs for ast, with type annotations for every field, which might not be the case for other IDEs. Regarding mypy, I don’t understand typeshed’s stub file for ast though: typeshed/stdlib/_ast.pyi at main · python/typeshed · GitHub (only re-exporting stuff and not annotating any node class).
Python 3.13.5 (main, Aug 2 2025, 02:04:16) [GCC 14.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import ast
>>> node = ast.FunctionDef(what="is this")
<python-input-4>:1: DeprecationWarning: FunctionDef.__init__ got an unexpected keyword argument 'what'. Support for arbitrary keyword arguments is deprecated and will be removed in Python 3.15.
<python-input-4>:1: DeprecationWarning: FunctionDef.__init__ missing 1 required positional argument: 'name'. This will become an error in Python 3.15.
<python-input-4>:1: DeprecationWarning: FunctionDef.__init__ missing 1 required positional argument: 'args'. This will become an error in Python 3.15.
Type hints are not part of the standard library, but of typeshed. Still, a runtime verification would be nice as well, but this could be done with a function verify as well; such a function could actually be implemented to check the whole AST recursively. Although such a function could be implemented in a third-party library as well, it might make sense to have it in the standard library such that it can be kept more easily in sync if the AST changes.
I have checked this once more and since type information is available in the classes of the ast hierarchy, it is possible to (recursively) validate a given ast with code, like
import ast, types
def validate(node, name=None):
if not isinstance(node, ast.AST):
raise TypeError(f"{node} is not an ast node")
if name is None:
name = f"<{type(node).__name__}>"
sentinel = object()
for field in node._fields:
attribute = getattr(node, field, sentinel)
if attribute is sentinel:
raise ValueError(f"'{name}' has no member '{field}'")
expected_type = type(node)._field_types[field]
attribute_name = f"{name}.{field}"
if ( isinstance(expected_type, types.GenericAlias)
and expected_type.__origin__ is list
and len(expected_type.__args__) == 1
):
expected_type = expected_type.__args__[0]
for i, item in enumerate(attribute):
item_name = f"{attribute_name}[{i}]"
if not isinstance(item, expected_type):
raise ValueError(f"'{item_name}' should be "
f"'{expected_type.__name__}', but is "
f"'{type(item).__name__}'")
if isinstance(item, ast.AST):
validate(item, name=item_name)
else:
if not isinstance(attribute, expected_type):
raise ValueError(f"'{attribute_name}' should be "
f"'{expected_type.__name__}', but is "
f"'{type(attribute).__name__}'")
if isinstance(attribute, ast.AST):
validate(attribute, name=attribute_name)
The fact that __annotations__, and for AST nodes, _fields/_field_typesexist, means that third party packages could easily use that as a kind of API, where nothing would need to change, if those attributes exist for all ASTs, even if they change in some way.
Basically, it shouldn’t be too hard to put this into a thrid-party module, as AST validation doesn’t really belong into ast anyways (imo). Python modules always expect users to make errors, yes, but they also expect users to know what they are doing. If something breaks, e.g. some private attribute is accessed / changed, it’s the users fault, and his problem. So why validate the AST? If someone provides an invalid type, it’s their problem.
I really don’t like to repeat myself, but this still holds in my opinion, and there were no responses to that section of my post: