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
- Wrap every dynamic piece in ast.FormattedValue(value=expr, conversion=-1, format_spec=None) instead of putting the raw expression in values.
- Keep literal text as ast.Constant(value='<str>') with an actual str value; convert non-str constants (e.g. 1) to FormattedValue.
- 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.
- 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
- Wrap dynamic expressions in FormattedValue; keep only str Constants as literals.
- Generate f-strings via ast.parse(repr of source) instead of assembling nodes by hand.
- Test on the exact Python version you ship (Interpolation nodes appear in 3.14+ template strings).
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
- Node can't use cause without an exception.
- Cannot stringify annotation containing string formatting
- Unsupported major version: {major}
- malformed node or string: {node!r}
- expected AST, got %r
AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14).
Data as JSON: /api/errors/6be1c991721b1ae4.
Report an issue: GitHub.