← All guides

Practical workflows

Python from a PDF: Check What the Indentation Changed

Getting rid of IndentationError is only the first check. The return statement still has to belong to the right block.

When Python copied from a PDF loses indentation, compare the block structure with the original before running it. Then check the restored function against inputs whose correct outputs you can determine independently. Code can be syntactically valid while doing the wrong work.

The example below should return the names of every ready item. Moving one return statement four spaces to the right makes it stop after the first row. Both versions are valid Python. The complete comparison script is safe to inspect in full: it uses only the original functions and literal sample lists, with no file access, network calls or external input. Do not use that description as permission to run other unfamiliar extracted code.

Copy the complete comparison script

"""Original indentation demonstration. No files, network or external input.
The wrong function is intentionally defective. Passing the paired checks
confirms this demonstration; it does NOT approve the wrong function.
"""

def ready_names(rows):
    result = []
    for row in rows:
        if row["ready"]:
            result.append(row["name"])
    return result


def ready_names_wrong(rows):
    result = []
    for row in rows:
        if row["ready"]:
            result.append(row["name"])
        return result


cases = [
    (
        "unready first, ready second",
        [{"name": "Draft", "ready": False}, {"name": "Report", "ready": True}],
        ["Report"],
        [],
    ),
    (
        "two ready rows",
        [{"name": "A", "ready": True}, {"name": "B", "ready": True}],
        ["A", "B"],
        ["A"],
    ),
    ("empty input", [], [], None),
]

for label, rows, expected, expected_wrong in cases:
    actual = ready_names(rows)
    wrong_actual = ready_names_wrong(rows)
    assert actual == expected, (label, "intended function", actual, expected)
    assert wrong_actual == expected_wrong, (label, "counterexample", wrong_actual)
    print(f"{label}: intended={actual!r}; wrong={wrong_actual!r}")

print("All 3 paired checks match the documented example.")

A complete example to adapt to your task.

Open the complete text to save as a file ↗

Start with the intended result

For this exercise, the input is a list of dictionaries. Every dictionary has a name string and a ready Boolean. The function must inspect every row, keep the names whose ready value is true, preserve their order and return an empty list when no row qualifies. Validation of missing keys or other input types is outside this small contract.

This is the original source function for the example. Keep it available for comparison. The for statement belongs inside the function; the if belongs inside the loop; appending a name belongs inside the if. The return belongs inside the function but outside the loop. It runs after all rows have been considered.

Original intended functionThe return line has four leading spaces. It lines up with the for statement.
def ready_names(rows):
    result = []
    for row in rows:
        if row["ready"]:
            result.append(row["name"])
    return result

Python’s language reference defines leading whitespace as part of how statements are grouped. Here those groups are the information to preserve. Matching the visible words while changing their groups does not reproduce the same function.

A neat-looking repair can return too soon

Suppose someone restores the missing indentation but places return inside the loop. The following complete function is intentionally wrong for the stated contract. Its different name lets the comparison script run both versions without overwriting either one.

On the first loop iteration, Python evaluates the if, possibly appends a name, and then reaches return. The function ends at that point. It never considers a second row. The Python return reference describes this exit from the current function call; return is not an instruction to continue to the next row.

Intentional wrong-scope reconstructionThe return line now has eight leading spaces. It lines up with the if statement inside the loop.
def ready_names_wrong(rows):
    result = []
    for row in rows:
        if row["ready"]:
            result.append(row["name"])
        return result

An editor may display this version cleanly, and a parser can accept it. Neither result shows that the restored layout matches the original author’s intent. If the source is unreadable, a formatter cannot supply the missing evidence about which block should own return.

Choose inputs that expose the lost work

A single ready row would give the same output in both versions, so it would miss this defect. Use at least one case that requires the loop to reach a later row. The first case below starts with an unready item and puts the only ready item second. The wrong version returns an empty list too early.

The second case has two ready rows. It exposes a partial result that might otherwise look plausible: A appears, but B disappears. With empty input, the loop body never executes. In the wrong version, no return is reached and the function yields None; the intended version returns the empty list required by the contract.

Known outputs for the original example
Input patternRequired resultWrong versionWhat it checks
Draft: false, Report: true['Report'][]A later qualifying row must be inspected
A: true, B: true['A', 'B']['A']All qualifying rows must be retained
No rows[]NoneEmpty input must still return a list

Run the comparison and read what passed

Save the complete copied script as indentation_check.py and run it with an installed Python 3 interpreter. For example, use python indentation_check.py in the folder where you saved it, or python3 indentation_check.py if that is how Python is installed on your system. The script needs no additional packages.

Each case checks the intended output and the documented defective output. Seeing the final success line means both observed results match this demonstration. It does not mean the wrong function satisfies the contract. The expected output below makes that distinction visible.

Use the actual source and requirements to design cases for your own restored function. These three checks establish the illustrated behavior for these inputs; they do not prove equivalence for every possible program, input or exception path.

Expected output from the complete scriptThe wrong outputs remain wrong. The paired checks confirm the counterexample.
unready first, ready second: intended=['Report']; wrong=[]
two ready rows: intended=['A', 'B']; wrong=['A']
empty input: intended=[]; wrong=None
All 3 paired checks match the documented example.

Use the source to restore blocks, then test behavior

For a real recovery, retain the original PDF and the first extraction. Work on a copy in your destination editor. Identify the function boundary, each loop and conditional, and any statement that exits a block or function. Compare indentation levels line by line with the source; do not just add spaces until the error disappears.

Check whether a visible wrap is one long statement or a new statement before joining it. Keep string contents unchanged while repairing leading whitespace. Converting indentation to spaces can remove an inconsistent tab convention, but it cannot tell you whether a line belongs to the function, loop or conditional.

After reviewing the code and understanding its effects, check syntax and then run a small set of independently known cases in an appropriate local environment. Include a case that reaches a later iteration and any relevant empty-input path. Inspect the edit diff so a whitespace repair has not quietly changed a name, operator or string.

Stop if the source does not show the indentation clearly, required lines are missing or the intended result is unknown. Ask for the original source file or a clearer excerpt. Keep the unresolved function out of the code you rely on; a passing guess is still a guess.

Keep extraction and code repair as separate steps

If you use CleanMD to prepare Markdown from a PDF or image on iPhone, review the extracted text against the permitted source, then copy it to your code editor for repair. Its current public description supports extraction, preview and copying. It does not establish automatic recovery of Python intent, an in-app code editor or Python execution.

Conversion sends the selected file to third-party parsing, so confirm that you are allowed to upload it before using that step. Manual transcription from a readable source remains an option for a small function. This article did not upload a PDF or test a conversion on a device.

The final handoff should include the checked function, the source reference and the known-input results. That is more useful to the next developer than a note saying only “indentation fixed.”

Sources and further reading

Python language reference: indentation and statement groupingPython language reference: the return statementReader-reported problem: PDF copy loses Python indentation