CodeToolProCodeToolPro
GitHub
Formatters·8 min read

Python Formatter Guide: Beautify & Indent Python Code

CodeToolPro Team·

Python Formatter Guide: Beautify & Indent Python Code

A Python formatter takes ragged, inconsistently indented Python source and turns it into clean, readable code with consistent 4-space indentation. When you paste a snippet copied from a chat thread, a broken paste, or a file edited by three different people, a quick pass makes the structure obvious again. Try it instantly with our Python Formatter — it runs entirely in your browser, so your code never leaves the machine.

This guide explains what the tool actually does, how its indentation engine works, shows real tested output (including the cases where it intentionally does not behave like Black), and when reaching for the tool beats writing code.

What Is a Python Formatter?

In the strict sense, a Python formatter is a program that adjusts the layout of source code without changing its meaning. Python is whitespace-sensitive, so indentation is not cosmetic — it defines blocks. A good formatter therefore has one job: make sure every line sits at the correct indent level.

Our in-browser tool is a lightweight re-indenter, not a full style enforcer. It does not reflow long lines, normalize whitespace inside expressions, sort imports, or rewrite code the way Black does. What it does do well is rebuild indentation from scratch using simple, predictable block rules — which is exactly what you want when the indentation itself is the problem.

The golden rule: the tool ignores the indentation you pasted and recomputes it. If your original indentation was wrong, that is fine — the tool does not trust it. If your original indentation was correct but you have multiple top-level blocks, read the pitfalls section before you trust the output.

How the Re-Indent Engine Works

The engine is a single-pass line scanner. For every line it:

  1. Strips leading/trailing whitespace (your original indent is discarded).
  2. Pushes the trimmed line at the current indent level (4 spaces per level).
  3. If the trimmed line ends with : and looks like a block header, it increments the indent level for the following lines.
  4. If the trimmed line is a dedent keyword, it decrements the indent level before writing the line.

The triggers are small and explicit:

TriggerKeywords
Indent after (line ends with :)if, elif, else, for, while, def, class, with, try, except, finally, match, case — plus any :-ended line that contains no {, [, or =
Dedent beforeelif, else, except, finally, case

A few consequences fall straight out of these rules:

  • Lines that end with : but contain {, [, or = are treated as data, not blocks. So config = {"a": 1} or data = src[1:3] never trigger an indent.
  • The indent step happens after writing the header line, so the header stays at its own level and its body drops one level deeper.
  • Dedenting only happens on elif/else/except/finally/case. Other exits from a block — return, break, pass, raise, continue — do not dedent.

Code Examples

JavaScript (the tool's actual algorithm)

This is the real function used by the formatter, reproduced verbatim so you can see the exact rules:

function formatPython(code) {
  const INDENT = "    "; // 4 spaces
  const lines = code.split("\n");
  const result = [];
  let indent = 0;

  for (const line of lines) {
    const stripped = line.trim();
    if (stripped === "") { result.push(""); continue; }

    const lower = stripped.toLowerCase();
    const isDedent =
      lower.startsWith("elif ") || lower.startsWith("else") ||
      lower.startsWith("except") || lower.startsWith("finally") ||
      lower.startsWith("case") || lower === "case";

    if (isDedent) indent = Math.max(0, indent - 1);

    result.push(INDENT.repeat(indent) + stripped);

    if (stripped.endsWith(":")) {
      const noColon = stripped.slice(0, -1).trimStart().toLowerCase();
      const isBlock = noColon.startsWith("def ") || noColon.startsWith("class ")
        || noColon.startsWith("if ") || noColon.startsWith("for ")
        || noColon.startsWith("while ") || noColon === "else"
        || noColon.startsWith("case ");
      if (isBlock || (!stripped.includes("{") && !stripped.includes("[") && !stripped.includes("="))) {
        indent++;
      }
    }
  }
  return result.join("\n");
}

Python (a faithful port)

The same logic in Python produces byte-for-byte identical output:

BLOCK_STARTERS = ["if ", "elif ", "else", "for ", "while ", "def ", "class ", "with ",
                  "try", "except ", "finally", "match ", "case "]

def format_python(code):
    INDENT = "    "
    result, indent = [], 0
    for line in code.split("\n"):
        stripped = line.strip()
        if stripped == "":
            result.append("")
            continue
        lower = stripped.lower()
        if lower.startswith(("elif ", "else", "except", "finally", "case")) or lower == "case":
            indent = max(0, indent - 1)
        result.append(INDENT * indent + stripped)
        if stripped.endswith(":"):
            without = stripped[:-1].lstrip().lower()
            is_block = any(without.startswith(s) for s in BLOCK_STARTERS) or without == "else"
            if is_block or ("{" not in stripped and "[" not in stripped and "=" not in stripped):
                indent += 1
    return "\n".join(result)

Hands-on: Tested with the Tool

I ran the exact algorithm from the tool (in Node and in Python) against several inputs. The outputs below are the real results — not illustrations.

Test 1 — a single function with an if/else (the sweet spot).

Input:

def classify(n):
if n > 0:
return 'positive'
else:
return 'non-positive'

Output:

def classify(n):
    if n > 0:
        return 'positive'
    else:
        return 'non-positive'

This is correct, valid Python. The if and else headers indent their bodies, and the else correctly dedents back to the function level before re-indenting its own body.

Test 2 — data lines must not be mistaken for blocks.

Input:

config = {"a": 1, "b": 2}
def show():
data = src[1:3]
print(config)

Output:

config = {"a": 1, "b": 2}
def show():
    data = src[1:3]
    print(config)

Note config = {...} stays at column 0 and data = src[1:3] (a slice) is not indented as if it were a block — the {/[/= guard works as designed.

Test 3 — the documented quirk: no auto-dedent at block end.

Input:

def a():
return 1

def b():
return 2

Output:

def a():
    return 1

    def b():
        return 2

def b(): ends up over-indented. Because the engine only dedents on elif/else/except/finally/case, it has no signal that the first function's block has ended, so it keeps stacking indentation. The same happens with return, break, pass, raise, and continue — they are recognized but intentionally do not change the indent level.

Common Mistakes & Pitfalls

  • Treating it as Black or Ruff. It does not wrap long lines, normalize spacing around operators, sort imports, or apply any style rules beyond indentation. Use a real linter for that.
  • Assuming it dedents when a block ends. It only dedents on elif/else/except/finally/ case. A second top-level function or class after the first will be over-indented. For a quick cosmetic fix of a single branchy block it is ideal; for whole multi-function modules it is not safe on its own.
  • Pasting already-correct code and trusting the result blindly. Correct input that contains several top-level definitions may come back over-indented. Always re-read the output before copying it back.
  • Expecting syntax validation. The tool never parses the code. A SyntaxError in your input will still produce "formatted" output — indentation is rebuilt regardless of whether the program is valid.
  • Mixing tabs and spaces. The engine emits 4 spaces exclusively and strips whatever you pasted, so the output is uniformly spaced — which is the point.

Related Tools

When to Use This Tool Instead of Code

You could re-indent by hand, or shell out to black --fast / ruff format if they are installed. A browser formatter wins in the moments those options are not at hand: you are reading a snippet in a ticket, you copied code out of a PDF, or you are writing docs and need a clean block right now — no install, no dependency, no upload of potentially sensitive source to a server. For production code, keep Black or Ruff in your pre-commit hook; for ad-hoc cleanup of a single branchy block, the Python Formatter is faster.