Skip to content

unparse() of manually created or modified Interpolation is difficult #138774

Closed

Description

@15r10nk

Bug report

Bug description:

Hi, I generated some code and experienced a difficulty generating the ast of template-strings.

ast.unparse uses the str attribute of the Interpolation to unparse it and ignores the value attribute.

from ast import *

tree = TemplateStr(
    values=[
        Interpolation(value=Name(id="test1", ctx=Load()), str="test2", conversion=-1)
    ]
)

print(unparse(tree))

output (Python 3.14.0rc2+):

t'{test2}'

I know that it is important to preserve the original code because the formatting might be important, but this makes it difficult to generate correct AST.

from ast import *


code=Lambda(args=arguments(args=[arg(arg='a')]), body=Name(id='a', ctx=Load()))

tree = TemplateStr(
    values=[
        Interpolation(value=code, str=unparse(code), conversion=-1)
    ]
)

compile(unparse(tree),"<string>","exec")

output (Python 3.14.0rc2+):

Traceback (most recent call last):
  File "/home/frank/projects/pysource-playground/pysource-codegen/example.py", line 12, in <module>
    compile(unparse(tree),"<string>","exec")
    ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<string>", line 1
    t'{lambda a: a}'
       ^^^^^^^^^
SyntaxError: t-string: lambda expressions are not allowed without parentheses

The solution here can be to add "()" around all interpolations.

Another problem can be that you use interpolations inside interpolations, which means that you have to process the childs first.

This is my current workaround which fixes these issues for me:

def walk_childs_first(node):
    for e in ast.iter_child_nodes(node):
        yield from walk_childs_first(e)
        yield e

def fix_result(node):
    if sys.version_info >= (3, 14):
        for n in walk_childs_first(node):
            if isinstance(n, ast.Interpolation):
                f_str = ast.JoinedStr(
                    [ast.FormattedValue(value=n.value, conversion=-1, format_spec=None)]
                )
                f_str_repr = ast.unparse(f_str)
                if f_str_repr.startswith(("f'''", 'f"""')):
                    n.str = ast.unparse(f_str)[5:-4]  # strip f"""{...}"""
                else:
                    n.str = ast.unparse(f_str)[3:-2]  # strip f"{...}"

I'm using the unparsing code for f-strings here to generate the correct source code.

I would like to know if there is something which I missed, or if this problem was just overlooked untill now.

I experienced this problem during my work on pysoruce-codegen and pysource-minimize. This might be the reason why this examples could look like some edge cases, but I think it can also be problematic for other people who just want to change the AST and unparse the code, because unparse would use the old code from str and ignore the changed value.

I have no idea how this could be solved but I think it should at least be mentioned in the docs.
Being able to set Interpolation.str to None could help with the unparse use case (it should unparse Interpolation.value in this case), but I don't know if this could cause other problems.

CPython versions tested on:

3.14

Operating systems tested on:

No response

Linked PRs

Activity

  1. added
    stdlibStandard Library Python modules in the Lib/ directory
    3.14bugs and security fixes
    3.15bugs and security fixes
    on Sep 11, 2025
  2. iritkatriel commented on Sep 17, 2025

    @iritkatriel
    Member
  3. George-Ogden commented on Sep 26, 2025

    @George-Ogden
    Contributor

    @15r10nk - here's an alternative workaround:

    import ast
    
    from _ast_unparse import Unparser
    
    tree = ast.Expr(
        value=ast.TemplateStr(
            values=[
                ast.Interpolation(
                    value=ast.Lambda(
                        args=ast.arguments(args=[ast.arg(arg="x")]), body=ast.Name(id="x")
                    ),
                    str="", # put anything here
                    conversion=-1,
                )
            ]
        )
    )
    
    
    class CustomUnparser(Unparser):
        def visit_Interpolation(self, node):
            return self._write_interpolation(node, is_interpolation=False)
    
    
    print(CustomUnparser().visit(tree))

    This is modified from the _ast_unparse.Unparser source code

    Some more permanent alternatives:

    • use is_interpolation=False if str == ""
    • use is_interpolation=False if str is None
    • try with is_interpolation=True exceptdo useis_interpolation=False` but reraise the original error if both fail.

    I'd be happy to implement this in a PR and update docs, etc, if you think that one of these could work @iritkatriel.

  4. 15r10nk commented on Sep 27, 2025

    @15r10nk
    ContributorAuthor

    use is_interpolation=False if str is None

    I think this could work, but I don't know if there are other things to consider when making this change.
    The user would still need to know to set node.str=None when he is changing the value, but it would improve the situation.

  5. George-Ogden commented on Sep 27, 2025

    @George-Ogden
    Contributor

    I agree - I think that's the best way as it's least ambiguous. We can put this detail in the docs, but it would only apply when generating the AST, as the str field would be filled in when parsing code.

  6. added a commit that references this issue on Oct 23, 2025
  7. added a commit that references this issue on Oct 23, 2025
  8. added a commit that references this issue on Oct 23, 2025
  9. added a commit that references this issue on Dec 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.14bugs and security fixes3.15bugs and security fixesstdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions