How to Fix Python Black Error When Auto Formatting Selected Code via External Tools

I ran into a frustrating snag while wiring Black into my editor’s External Tools so I could auto format only the selected code. Sometimes it worked; other times Black threw errors like Cannot parse: <line>:<col> or even a TypeError about unexpected keyword arguments. I’ll walk through the exact scripts I started with, why the errors happen, and the small “project” I built a safer wrapper script that made selection formatting reliable. I’ll finish with extra practice enhancements and a quick checklist.

The Original Setup I Use

I first hooked my editor’s External Tools to run this Python script on the current selection:

#!/usr/bin/env python3

# Adjust to your liking:
LINE_LEN = 79

from black import format_stdin_to_stdout, FileMode, WriteBack

mode = FileMode(line_length=LINE_LEN)

# fast can be ignored
format_stdin_to_stdout(fast=False, mode=mode, write_back=WriteBack.YES)

I also had a separate tool for full-file formatting:

#!/usr/bin/env bash
black --line-length 79 $GEDIT_CURRENT_DOCUMENT_NAME

At first glance both look fine but the “selection” tool kept failing in real projects.

The Error: Why Black Fails on “Selected Code”

When I pipe only the selected text to Black, I often get:

  • Cannot parse: <line>:<col>
  • or (on newer Black versions) TypeError: format_stdin_to_stdout() got an unexpected keyword argument 'write_back'

What’s Actually Going on (in Plain Terms)

  1. Partial selection ≠ valid module
    Black expects a complete chunk of Python (a full file or a syntactically self-contained block). If my selection slices through a def, class, if, try, or a multi-line expression (like a function call’s argument list), Black can’t parse it so it aborts with “Cannot parse…”.
  2. API drift across versions
    The snippet above calls format_stdin_to_stdout with arguments (write_back=WriteBack.YES, fast=False) that don’t match the function signature in newer Black releases. That triggers a TypeError. A better long-term bet is to use black.format_str with a Mode, which has been stable and predictable for me.

The Fix: a Tiny “Project” Wrapper That Formats Selections Safely

I created a small script, black_selection.py, that reads from stdin, uses Black’s stable format_str API, and prints clearer errors when my selection isn’t a complete block.

What this wrapper does for me:

  • Accepts familiar flags (line length, preview mode, target versions).
  • Formats the selection if it’s syntactically complete.
  • If parsing fails (very common with partial selections), it shows a friendly tip rather than a scary traceback.
  • Works great when my editor pipes the selection to stdin and replaces the selection with stdout.

black_selection.py

#!/usr/bin/env python3
"""
Format Python code from STDIN using Black, with clear errors for partial snippets.

Usage example (editor external tool):
  - Input: Current selection (falls back to whole document if your editor supports it)
  - Output: Replace selection
  - Command: python3 /path/to/black_selection.py --line-length 79 --safe
"""

import sys
import argparse
from textwrap import dedent

try:
    import black
    from black import Mode
    from black.parsing import InvalidInput
except Exception as exc:
    sys.stderr.write(
        "ERROR: Could not import 'black'. Install it first, e.g.:\n"
        "  pip install black\n"
    )
    sys.exit(2)

def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(add_help=False)
    p.add_argument("--line-length", type=int, default=79)
    p.add_argument("--skip-string-normalization", action="store_true")
    p.add_argument("--preview", action="store_true",
                   help="Opt in to Black preview style (if supported by your version).")
    p.add_argument("--safe", action="store_true",
                   help="If parsing fails (likely due to a partial selection), "
                        "show a friendly error instead of a stack trace.")
    p.add_argument("--target-version", action="append", default=[],
                   help="Repeatable. Example: --target-version py39")
    p.add_argument("-h", "--help", action="help", help="Show this help message and exit.")
    return p.parse_args()

def to_target_versions(values):
    # Map strings like "py39" to black.TargetVersion.py39 (when available)
    out = set()
    for v in values:
        try:
            out.add(black.TargetVersion[v.lower()])
        except Exception:
            # Ignore unknown entries; Black will still format with defaults
            pass
    return out

def main() -> int:
    args = parse_args()
    src = sys.stdin.read()

    if not src.strip():
        # Nothing to format; keep editor UX quiet
        return 0

    mode_kwargs = {
        "line_length": args.line_length,
        "string_normalization": not args.skip_string_normalization,
    }
    # 'preview' is available in newer Black; if not, Mode ignores it
    if "preview" in Mode.__init__.__code__.co_varnames:
        mode_kwargs["preview"] = args.preview

    tv = to_target_versions(args.target_version)
    if tv:
        mode_kwargs["target_versions"] = tv

    mode = Mode(**mode_kwargs)

    try:
        formatted = black.format_str(src, mode=mode)
    except InvalidInput as e:
        if args.safe:
            sys.stderr.write(
                "Black could not parse the selection. This usually happens when the "
                "selection is not a complete Python statement or block.\n\n"
                "Tips to fix:\n"
                "  • Expand the selection to include the entire block (e.g., the whole def/class/if).\n"
                "  • Or run full-file formatting instead of selection.\n\n"
                f"Details: {e}\n"
            )
            return 1
        else:
            raise
    except Exception as e:
        sys.stderr.write(f"Unexpected error: {e}\n")
        return 2

    sys.stdout.write(formatted)
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

How I Wired it Up in my Editor

  • Input: Current selection (default to document)
  • Output: Replace current selection
  • Command:
    python3 /absolute/path/to/black_selection.py --line-length 79 --safe

If my editor can’t pipe only the selection, I still use full file Black:

black --line-length 79 "$GEDIT_CURRENT_DOCUMENT_NAME"

What the Fix Looks Like

Before (ragged arguments, odd spacing):

def fetch(user_id, *, retry = 3, timeout= 5 , verbose=False):
    return (  user_id, retry, timeout, verbose  )

After (Black via my wrapper):

def fetch(user_id, *, retry=3, timeout=5, verbose=False):
    return (user_id, retry, timeout, verbose)

If I accidentally selected only the function signature or missed the closing ), the wrapper prints a short hint telling me to select the entire block.

More “Practice” Functionality I Added

These switches became my go to extras while experimenting:

  • Check mode (useful for pre-commit/CI; no writes, non-zero on diff): black --check --diff --line-length 79 "$GEDIT_CURRENT_DOCUMENT_NAME"
  • Skip string normalization (keep original quotes): python3 black_selection.py --skip-string-normalization
  • Target a Python version (affects certain formatting decisions): python3 black_selection.py --target-version py39
  • Preview style (try upcoming rules): python3 black_selection.py --preview

You can combine these with your editor bindings or run them from a task runner or git hook.

Full File External Tool (what I still use a lot)

#!/usr/bin/env bash
black --line-length 79 "$GEDIT_CURRENT_DOCUMENT_NAME"

This remains the simplest option when I’m not worried about partial selections.

Final Thought

I wanted the convenience of formatting only what I highlight without random crashes. The trick was to accept that selections are fragile by nature and let a thin wrapper handle errors gracefully. Since I switched to format_str with a friendly “safe” mode, formatting selections feels reliable, and I can still fall back to full-file Black anytime. If you’re seeing the same errors, try this wrapper approach you’ll keep the speed of selection formatting while avoiding the pitfalls that used to trip me up.

Related blog posts