python/cpython · error · TypeError

expected AST, got %r

Error message

expected AST, got %r

What it means

ast.dump requires a single AST node instance as its first argument; after its internal _format helper handles lists/None/bools/numbers/strings, the top-level check rejects anything that is not an ast.AST instance with TypeError naming the offending class. Lists of nodes, source strings, tokens, or None passed at top level all fail here.

Source

Thrown at Lib/ast.py:223

                    args.append(f'{t.attribute}{name}{t.reset}={value}')
            cls_name = f'{t.node}{cls.__name__}{t.reset}'
            if allsimple and len(args) <= 3:
                return f'{cls_name}({", ".join(args)})', not args
            return f'{cls_name}({prefix}{sep.join(args)})', False
        elif isinstance(node, list):
            if not node:
                return '[]', True
            return '[%s%s]' % (prefix, sep.join(_format(x, level)[0] for x in node)), False
        if isinstance(node, bool) or node is None or node is Ellipsis:
            return f'{t.keyword}{node!r}{t.reset}', True
        if isinstance(node, (int, float, complex)):
            return f'{t.number}{node!r}{t.reset}', True
        if isinstance(node, (str, bytes)):
            return f'{t.string}{node!r}{t.reset}', True
        return repr(node), True

    if not isinstance(node, AST):
        raise TypeError('expected AST, got %r' % node.__class__.__name__)
    if indent is not None and not isinstance(indent, str):
        indent = ' ' * indent
    return _format(node)[0]


def copy_location(new_node, old_node):
    """
    Copy source location (`lineno`, `col_offset`, `end_lineno`, and `end_col_offset`
    attributes) from *old_node* to *new_node* if possible, and return *new_node*.
    """
    for attr in 'lineno', 'col_offset', 'end_lineno', 'end_col_offset':
        if attr in old_node._attributes and attr in new_node._attributes:
            value = getattr(old_node, attr, None)
            # end_lineno and end_col_offset are optional attributes, and they
            # should be copied whether the value is None or not.
            if value is not None or (
                hasattr(old_node, attr) and attr.startswith("end_")
            ):

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Call ast.parse(source) first and pass its result (a Module node) or an individual statement/expression node.
  2. For lists of nodes, dump each element: '\n'.join(ast.dump(n) for n in tree.body).
  3. Type-check before dumping when the node's provenance is dynamic: isinstance(node, ast.AST).

Example fix

# before
ast.dump(tree.body)  # list of stmts -> TypeError: expected AST, got 'list'

# after
ast.dump(tree)                    # whole module
# or
'\n'.join(ast.dump(n) for n in tree.body)
Defensive patterns

Strategy: type-guard

Validate before calling

if not isinstance(obj, ast.AST):
    obj = ast.parse(obj)  # accept source strings defensively before dump

Type guard

import ast

def is_ast_node(obj) -> bool:
    return isinstance(obj, ast.AST)

def dump_any(obj):
    if isinstance(obj, ast.AST):
        return ast.dump(obj)
    if isinstance(obj, (list, tuple)):
        return '\n'.join(ast.dump(n) for n in obj if isinstance(n, ast.AST))
    raise TypeError(f'cannot dump {type(obj).__name__}')

Try / catch

try:
    text = ast.dump(node)
except TypeError as e:
    if 'expected AST' in str(e):
        text = '\n'.join(ast.dump(n) for n in node)  # was a list of nodes

Prevention

When it happens

Trigger: ast.dump(parse_result.body) where body is a list of statements (must pick one node, e.g. body[0]); ast.dump('x = 1') forgetting ast.parse first; passing the result of ast.walk(...) (a generator); mixing up argument order in helper functions that wrap dump.

Common situations: Debugging helpers that dump 'whatever the traversal returned'; refactors that changed a single-node variable into a list; logging visitor output where lists of nodes are the natural unit.

Related errors


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/e3a5d704c0a68286. Report an issue: GitHub.