Desk Clarity

A field guide to everyday digital problems.

Developer tools / Practical guide

Python ast.parse single mode: incomplete blocks and codeop

Fix ast.parse single-mode confusion: keep complete blocks, parse files in exec mode, and distinguish incomplete interactive input from invalid syntax.

By ON Lab3 min readSources checked November 10, 2026

ast.parse(line, mode="single") does not make an incomplete physical line into a complete Python statement. Keep an if header with its indented body. For a whole file, parse the complete source with mode="exec". For interactive input, distinguish incomplete input from invalid syntax with codeop.compile_command.

Match the input boundary to the task

A November 2026 Python Help discussion described a tool that split source into lines and failed at an if header. Its later workaround appended pass; a trailing comment broke that approach. This is a real reader problem, but the tool's transformation requirements were not fully specified. The examples here diagnose parsing boundaries; they do not claim to implement that reader's transformer.

Input taskStarting point
Analyze a complete file or several statementsKeep the source together; parse with exec mode.
Parse one complete interactive statementsingle mode; a statement may contain multiple lines.
Decide whether an interactive command needs more inputUse the completion API rather than catching every syntax error as incomplete.

For example, x = 1; y = 2 is accepted in single mode, while two independent assignment lines are rejected by that mode in our fixture. An isolated if True: header lacks its suite. Removing indentation from subsequent lines also discards the structure you need to analyze.

Keep complete source for AST analysis

Parsing an entire source string preserves the relationship between the condition and its body. AST nodes expose location information, and ast.get_source_segment can retrieve the original segment when the required locations exist. In this example it returns the complete conditional, including its header comment.

Do not treat AST generation as proof that every later compilation check will pass. Python documents that some checks happen during compilation. If your application must produce executable code, validate that separate step for the intended context.

Test incomplete, complete, and invalid input separately

import ast
import codeop

header = "if True: # keep this comment"
assert codeop.compile_command(header) is None

source = header + "\n    x = 3\n"
assert codeop.compile_command(source) is not None

tree = ast.parse(source, mode="exec")
node = tree.body[0]
assert isinstance(node, ast.If)
assert ast.get_source_segment(source, node) == source.rstrip("\n")

try:
    codeop.compile_command("if :")
except SyntaxError:
    print("Invalid syntax; asking for another line is not the fix")

The first completion call returns None. Adding an indented body and a final newline makes this example complete and produces a code object. The deliberately malformed condition raises SyntaxError. These are three distinct outcomes; a loop that always requests another line on an exception can hide a real error.

The completion call compiles input; this example never executes its returned object. The final newline is intentional for this interactive compound statement. Do not use successful completion as a promise that a user has finished typing every clause they might want to add.

Why adding pass is a poor repair

In our fixture, appending pass directly after a header's trailing comment leaves the body missing: the appended text is part of the comment. More fundamentally, a fabricated body is different source. Preserve complete syntactic units and choose a transformation strategy that retains their relationships. Reconstructing source one physical line at a time needs more design than stripping indentation and replacing newlines.

Choose the right boundary for a real tool

For a file analyzer, collect the complete file before parsing. For an embedded console, Python's documentation points to the code module for the loop. If using lower-level completion across commands, CommandCompiler retains future-statement state. The documentation also describes a rare trailing-input limitation; this API should not be treated as an exhaustive validation boundary.

Verification: the linked local fixture passed on Python 3.11.6 on November 10, 2026, including complete blocks, invalid input, multiple statements, source-segment retrieval and the comment workaround failure. Current official documentation was checked separately. No claim is made about testing every Python release or the reader's full tool. Never execute untrusted submitted code merely to inspect its syntax.

Sources & further reading