python/cpython · error · ValueError

Unexpected node inside JoinedStr, {node!r}

Error message

Unexpected node inside JoinedStr, {node!r}

What it means

Raised by ast.unparse() when traversing a JoinedStr (f-string) node whose values sequence contains an element that is neither a plain str Constant, a FormattedValue, nor a template-string Interpolation node. Real f-strings produced by ast.parse never contain such children, so this indicates a malformed hand-built or mutated JoinedStr.

Source

Thrown at Lib/_ast_unparse.py:654

        if isinstance(node, JoinedStr):
            # for both the f-string itself, and format_spec
            for value in node.values:
                self._write_ftstring_inner(value, is_format_spec=is_format_spec)
        elif isinstance(node, Constant) and isinstance(node.value, str):
            value = node.value.replace("{", "{{").replace("}", "}}")

            if is_format_spec:
                value = value.replace("\\", "\\\\")
                value = value.replace("'", "\\'")
                value = value.replace('"', '\\"')
                value = value.replace("\n", "\\n")
            self.write(value)
        elif isinstance(node, FormattedValue):
            self.visit_FormattedValue(node)
        elif isinstance(node, Interpolation):
            self.visit_Interpolation(node)
        else:
            raise ValueError(f"Unexpected node inside JoinedStr, {node!r}")

    def _unparse_interpolation_value(self, inner):
        unparser = type(self)()
        unparser.set_precedence(_Precedence.TEST.next(), inner)
        return unparser.visit(inner)

    def _write_interpolation(self, node, use_str_attr=False):
        with self.delimit("{", "}"):
            if use_str_attr:
                expr = node.str
            else:
                expr = self._unparse_interpolation_value(node.value)
            if expr.startswith("{"):
                # Separate pair of opening brackets as "{ {"
                self.write(" ")
            self.write(expr)
            if node.conversion != -1:
                self.write(f"!{chr(node.conversion)}")

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Wrap every dynamic piece in ast.FormattedValue(value=expr, conversion=-1, format_spec=None) instead of putting the raw expression in values.
  2. Keep literal text as ast.Constant(value='<str>') with an actual str value; convert non-str constants (e.g. 1) to FormattedValue.
  3. If targeting 3.14+ template strings, use the Interpolation node shape the parser produces (copy it from ast.parse of a t-string) rather than inventing one.
  4. Prefer generating source text or using ast.parse(f'f{...!r}') to obtain a correct JoinedStr instead of building it manually.

Example fix

// before
node = ast.JoinedStr(values=[
    ast.Constant(value='n='),
    ast.Name(id='n', ctx=ast.Load()),  # invalid child
])
ast.unparse(node)  # ValueError: Unexpected node inside JoinedStr

// after
node = ast.JoinedStr(values=[
    ast.Constant(value='n='),
    ast.FormattedValue(value=ast.Name(id='n', ctx=ast.Load()), conversion=-1, format_spec=None),
])
ast.unparse(node)  # "f'n={n}'"
Defensive patterns

Strategy: validation

Validate before calling

import ast

def joinedstr_ok(node: ast.JoinedStr) -> bool:
    return all(
        (isinstance(v, ast.Constant) and isinstance(v.value, str))
        or isinstance(v, (ast.FormattedValue, getattr(ast, 'Interpolation', ())))
        for v in node.values
    )

assert joinedstr_ok(node), 'bad JoinedStr children'

Type guard

import ast
_Interp = getattr(ast, 'Interpolation', None)

def is_valid_joinedstr(n: ast.AST) -> bool:
    return isinstance(n, ast.JoinedStr) and all(
        (isinstance(v, ast.Constant) and isinstance(v.value, str))
        or isinstance(v, ast.FormattedValue)
        or (_Interp is not None and isinstance(v, _Interp))
        for v in n.values
    )

Try / catch

try:
    ast.unparse(node)
except ValueError as e:
    if 'Unexpected node inside JoinedStr' in str(e):
        node.values = [v for v in node.values if isinstance(v, ast.Constant) and isinstance(v.value, str)]
    else:
        raise

Prevention

When it happens

Trigger: Constructing ast.JoinedStr(values=[...]) with an invalid element (e.g. a non-string Constant like ast.Constant(value=1), an ast.Name, or an arbitrary object) and calling ast.unparse on it. Also triggered by transform passes that splice non-interpolation nodes into an f-string's values list.

Common situations: Code-generation libraries that assemble f-strings node-by-node; naive attempts to embed an expression into an f-string by appending it directly to values; version differences where code targets FormattedValue but runs on a build using template-string Interpolation nodes.

Related errors


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