Skip to content

Instantly share code, notes, and snippets.

@stephenmathieson
Created January 28, 2026 23:59
Show Gist options
  • Select an option

  • Save stephenmathieson/9b0faa1d4018675cfccc5c908ea8d818 to your computer and use it in GitHub Desktop.

Select an option

Save stephenmathieson/9b0faa1d4018675cfccc5c908ea8d818 to your computer and use it in GitHub Desktop.
Automatically wrap long comments in python files (because `black` does not)
#!/usr/bin/env python3
"""wrap_comments.py.
A script to auto-wrap full-line comments in Python files to a specified line length (default 88).
- Only wraps lines starting with '#' (not inline comments or code).
- Preserves indentation and comment markers.
- Skips docstrings and code.
Usage:
python wrap_comments.py <file_or_directory> [--line-length N] [--dry-run]
"""
import argparse
import os
import textwrap
from pathlib import Path
from typing import List
def wrap_comment_line(line: str, line_length: int) -> List[str]:
stripped = line.lstrip()
indent = line[: len(line) - len(stripped)]
# Only wrap if this is a single-line Python comment (not markdown heading)
# e.g. '# comment', not '##' or '###' etc.
if not (stripped.startswith("#") and (len(stripped) == 1 or stripped[1] == " ")):
return [line.rstrip("\n")]
# Remove leading '#' and possible space
comment_body = stripped[1:]
if comment_body.startswith(" "):
comment_body = comment_body[1:]
# Wrap the comment
wrapped = textwrap.wrap(comment_body, width=line_length - len(indent) - 2)
if not wrapped:
return [indent + "#"]
return [indent + "# " + w for w in wrapped]
def process_file(path: Path, line_length: int, dry_run: bool = False) -> int:
changed = False
with path.open("r", encoding="utf-8") as f:
lines = f.readlines()
new_lines = []
for line in lines:
if line.lstrip().startswith("#") and not line.lstrip().startswith("#!"):
wrapped = wrap_comment_line(line, line_length)
if wrapped != [line.rstrip("\n")]:
changed = True
new_lines.extend(wrapped)
else:
new_lines.append(line.rstrip("\n"))
if changed and not dry_run:
with path.open("w", encoding="utf-8") as f:
for l in new_lines:
f.write(l + "\n")
return int(changed)
def process_path(target: Path, line_length: int, dry_run: bool = False) -> int:
changed_files = 0
if target.is_file() and target.suffix == ".py":
changed_files += process_file(target, line_length, dry_run)
elif target.is_dir():
for root, _, files in os.walk(target):
for fname in files:
# Only process .py files
if fname.endswith(".py"):
changed_files += process_file(
Path(root) / fname, line_length, dry_run
)
return changed_files
def main():
parser = argparse.ArgumentParser(
description="Wrap full-line comments in Python files."
)
parser.add_argument("target", help="File or directory to process")
parser.add_argument(
"--line-length", type=int, default=88, help="Max line length (default 88)"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would change, don't write files",
)
args = parser.parse_args()
target = Path(args.target)
changed = process_path(target, args.line_length, args.dry_run)
if args.dry_run:
print(f"[DRY RUN] {changed} file(s) would be changed.")
else:
print(f"{changed} file(s) updated.")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment