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)
- 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 adef
,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…”. - API drift across versions
The snippet above callsformat_stdin_to_stdout
with arguments (write_back=WriteBack.YES
,fast=False
) that don’t match the function signature in newer Black releases. That triggers aTypeError
. A better long-term bet is to useblack.format_str
with aMode
, 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.