Skip to content

compile fails on 3.14 on a valid expression when -OO is set #137308

Closed

Description

@Liam-DeVoe

Bug report

Bug description:

Given the following code snippet:

import ast

source = b'class A:\n    """\n    """\n'
compile(ast.parse(source), "a", "exec")

python3.13 -OO test.py passes, but python3.14 -OO test.py fails, with:

Traceback (most recent call last):
  File "/Users/tybug/Desktop/sandbox2.py", line 7, in <module>
    compile(ast.parse(source), "a", "exec")
ValueError: empty body on ClassDef

Python: 3.14.0rc1

CPython versions tested on:

3.14

Operating systems tested on:

macOS

Linked PRs

Activity

  1. StanFromIreland commented on Aug 1, 2025

    @StanFromIreland
    Member

    CC @sobolevn (due to -OO)

    The docstring is optimised away here, I’m not sure why it was allowed in 3.13.

  2. added
    stdlibStandard Library Python modules in the Lib/ directory
    interpreter-core(Objects, Python, Grammar, and Parser dirs)
    and removed
    stdlibStandard Library Python modules in the Lib/ directory
    on Aug 1, 2025
  3. added
    3.14bugs and security fixes
    3.15bugs and security fixes
    on Aug 1, 2025
  4. picnixz commented on Aug 1, 2025

    @picnixz
    Member

    I think we somehow changed the parser.

    import ast
    stmt = "class A:\n\t\"\"\"\n\t\"\"\"\n"
    print(ast.dump(ast.parse(stmt)))

    With Python 3.12:

    Module(body=[ClassDef(name='A', bases=[], keywords=[], body=[Expr(value=Constant(value='\n\t'))], decorator_list=[], type_params=[])], type_ignores=[])
    

    With main:

    Module(body=[ClassDef(name='A')])
    

    EDIT:

    Apparently, it's more than that. The docstring is not properly eliminated prior to Python 3.14:

    $ python3.13 -OO -c 'import ast; stmt = "class A:\n\t\"\"\"abc\"\"\"\n"; print(ast.dump(ast.parse(stmt, optimize=2)))'
    Module(body=[ClassDef(name='A', body=[Expr(value=Constant(value='abc'))])])
  5. brianschubert commented on Aug 1, 2025

    @brianschubert
    Contributor

    Bisects to ffc2f1d (#130087), cc @iritkatriel

  6. picnixz commented on Aug 1, 2025

    @picnixz
    Member

    I think it's because when we parse the AST we somehow also optimize it, namely we inherit from "-OO":

    $ # at 468a7aaeb4dfcd21697dfa46362b9f415b9daf1b (before ffc2f1d)
    $ python -OO -c 'import ast; stmt = "class A:\n\t\"\"\"abc\"\"\"\n"; print(ast.dump(ast.parse(stmt)))'
    Module(body=[ClassDef(name='A', body=[Expr(value=Constant(value='abc'))])])
    $ python -OO -c 'import ast; stmt = "class A:\n\t\"\"\"abc\"\"\"\n"; print(ast.dump(ast.parse(stmt, optimize=0)))'
    Module(body=[ClassDef(name='A', body=[Expr(value=Constant(value='abc'))])])
    $ python -OO -c 'import ast; stmt = "class A:\n\t\"\"\"abc\"\"\"\n"; print(ast.dump(ast.parse(stmt, optimize=1)))'
    Module(body=[ClassDef(name='A', body=[Expr(value=Constant(value='abc'))])])
    $ python -OO -c 'import ast; stmt = "class A:\n\t\"\"\"abc\"\"\"\n"; print(ast.dump(ast.parse(stmt, optimize=2)))'
    Module(body=[ClassDef(name='A')])
    
    $ # at 9a21df7c0a494e2819775eabd522ebec994d96c0 (roughly now)
    $ python -OO -c 'import ast; stmt = "class A:\n\t\"\"\"abc\"\"\"\n"; print(ast.dump(ast.parse(stmt)))'
    Module(body=[ClassDef(name='A')])
    $ python -OO -c 'import ast; stmt = "class A:\n\t\"\"\"abc\"\"\"\n"; print(ast.dump(ast.parse(stmt, optimize=0)))'
    Module(body=[ClassDef(name='A', body=[Expr(value=Constant(value='abc'))])])
    $ python -OO -c 'import ast; stmt = "class A:\n\t\"\"\"abc\"\"\"\n"; print(ast.dump(ast.parse(stmt, optimize=1)))'
    Module(body=[ClassDef(name='A', body=[Expr(value=Constant(value='abc'))])])
    $ python -OO -c 'import ast; stmt = "class A:\n\t\"\"\"abc\"\"\"\n"; print(ast.dump(ast.parse(stmt, optimize=2)))'
    Module(body=[ClassDef(name='A')])

    So the bisected commit actually had a side-effect where ast.parse() seems to inherit the value of optimize from the interpreter [it should per the docs but it didn't seem to be the case previously] (it's only inherited by default, not when we explicitly indicate optimize=...)

    EDIT:

    Let's quote the ast.parse docs again:

    Parse the source into an AST node. Equivalent to compile(source, filename, mode,
    flags=FLAGS_VALUE, optimize=optimize), where FLAGS_VALUE is ast.PyCF_ONLY_AST if
    optimize <= 0 and ast.PyCF_OPTIMIZED_AST otherwise.

    I think the issue is how we handle the optimize default value and when we actually handle it (and how we then determine the flags to pass). In Python 3.13 it seems that the flags and optimize values passed to compile after calling ast.parse were not the correct ones (I haven't checked) and incidently, they are now correct. Note that AST optimization (defined via ast.PyCF_*( has nothing to do with optimize=... which is about parser optimization as one could still have docstrings even if we had ast.PyCF_OPTIMIZED_AST. Docstrings disappear once we have optimize=2 or -OO.

  7. picnixz commented on Aug 1, 2025

    @picnixz
    Member

    More generally, eliminating the docstring makes the class def (or the function def) empty. A similar error occurs with a function. Maybe docstring elimination should always add a "pass" statement instead? (namely we replace docstrings by pass)

  8. added 2 commits that reference this issue on Aug 2, 2025
  9. added a commit that references this issue on Aug 2, 2025
  10. iritkatriel commented on Aug 4, 2025

    @iritkatriel
    Member

    More generally, eliminating the docstring makes the class def (or the function def) empty. A similar error occurs with a function. Maybe docstring elimination should always add a "pass" statement instead? (namely we replace docstrings by pass)

    Just catching up on this. This would work, but I think it would add a line event for the line of the docstring, and I'm not sure whether that's desirable. Maybe add it only when the body is empty?

  11. sobolevn commented on Aug 4, 2025

    @sobolevn
    Member

    @iritkatriel this is exactly what I did :)
    When .body contains only a docstring, we replace it with pass, otherwise, we just remove it.

  12. added a commit that references this issue on Aug 8, 2025
  13. StanFromIreland commented on Aug 10, 2025

    @StanFromIreland
    Member

    The documentation was not updated accordingly after b74f3be IMO. I think it should note the exception.

  14. picnixz commented on Aug 10, 2025

    @picnixz
    Member

    I don't think we should update anything here. Docstrings are properly eliminated and we just have pass instead.

  15. added a commit that references this issue on Aug 19, 2025
  16. added a commit that references this issue on Sep 9, 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 fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions