Building a Python script that reformats Python source with autopep8
Working through a legacy codebase often feels like untangling extension cords behind a desk. Spacing drifts, indentation slips, and line lengths wander wherever earlier contributors pleased. A small automation tool that rewrites those files back into a tidy shape gives a developer back the hours that would otherwise be lost to manual cleanup. That is the kind of utility that fits neatly on a personal homepage next to other open-source experiments.
autopep8 is a long-standing Python package that wraps pycodestyle and rewrites source so it matches the conventions documented in PEP 8. It is stable, dependency-light, and predictable, which makes it a sensible choice for a self-contained reformatting script. Unlike opinionated formatters such as Black or yapf, autopep8 keeps changes minimal and offers a granular set of fixes, which matters when working on someone else's library.
In Australian software teams, from the engineering floors of Atlassian in Sydney to the data groups around the Parkville biomedical precinct in Melbourne, internal style guides usually extend PEP 8 rather than replace it. A reformatting tool that respects those extensions is more useful than one that enforces a single house style, and it works naturally with localised spelling such as organisation and colour. The script described here takes autopep8 as a baseline and wraps it with sensible defaults for solo projects.
The end result is a command line program that accepts file paths, globs, or directories, applies a configurable set of autopep8 fixes, and leaves a backup when needed. It runs on any modern Linux machine, including the modest VPS instances commonly used by hobbyists in Brisbane or Perth who want to keep their tooling portable.
Setting up the project layout
A clean project folder pays off the first time a script grows beyond a single file. The skeleton used here keeps the reformatting logic separate from the command line wrapper, which makes both parts easier to test. Inside the project root, a src/reformat/ package holds the file discovery and fixing routines, while a thin cli.py module provides the user interface. A tests/ directory mirrors that layout so unit tests sit beside the code they cover.
Dependencies are pinned in a small requirements.txt. autopep8 itself sits alongside pycodestyle, which autopep8 depends on internally. For local development, a virtual environment under .venv/ is created with the built-in venv module, avoiding the need for system-wide installs. On a fresh Ubuntu 22.04 box, or on a laptop carried between Melbourne's inner suburbs and a co-working space in Cremorne, the same three commands produce a working environment.
The directory also includes a pyproject.toml that declares the project metadata and an entry point. Declaring an entry point turns the script into a proper console command, installable with pip install -e .. That detail matters for anyone distributing the tool to colleagues, since running it through python -m reformat is fine for personal use but cumbersome when sharing.
Discovering Python source files
Before any formatting happens, the script needs to know which files to touch. Python's pathlib module is the right starting point because it handles relative paths, home directory expansion, and platform differences in a tidy way. A small helper function accepts either a single file path or a directory path and returns a list of .py files to process.
When a directory is supplied, the helper uses Path.rglob('*.py') to walk the tree recursively. That approach catches source files inside nested packages, which is essential when pointing the tool at a project like DenyHosts or Kodos. Symlinks are skipped by default to avoid loops on systems where shared code lives in linked directories, a common pattern on Australian university research servers that mirror repositories from GitHub.
File encoding deserves a moment of attention. Python source files in Australia, like everywhere else, are almost always UTF-8, but assuming so without checking can corrupt a file with an exotic encoding. The discovery helper reads the first few bytes with the tokenize module's detect helper, which raises a clear error if the file is not what it expects. That keeps the formatter from silently mangling anything unusual.
Applying autopep8 fixes
With a list of files in hand, the next step is the actual rewriting. autopep8 exposes two entry points: fix_code, which accepts a string of source and returns the corrected version, and fix_file, which works on filenames and supports an in-place mode. For a multi-file tool, fix_file is usually easier because it preserves original line endings and handles the read and write cycle internally.
The script calls fix_file with a configured aggressive level. Level 1 applies the safest fixes, while level 2 or 3 rewrites more aggressively, including things like converting x == None into x is None. For most repositories, level 1 or 2 is plenty. Turning the level up to 3 risks behaviour changes in subtle places, so it is left out of the default and exposed through a command line flag for the curious.
A list of ignore codes can also be passed through. Some Australian teams prefer a project-specific line length, for example 100 characters instead of the PEP 8 default. That is handled by adding E501 to the ignore list and setting the max_line_length argument. The script keeps these settings in a small dictionary so they can be overridden by configuration files later, without needing to touch the core logic.
Building a command line interface
argparse remains the most reliable way to expose a Python tool to the terminal, even with newer libraries such as Typer available. The interface built here keeps things straightforward: a positional argument for the path, an optional --aggressive flag, a --write flag that toggles in-place edits, and a --backup flag that copies the original to a sibling file with a .bak suffix.
Help text is written in plain Australian English, with examples drawn from real usage. A developer in Adelaide running the tool across a freshly cloned repository can copy the example directly: reformat src/ --aggressive 2 --write. The --dry-run flag prints a diff instead of writing anything, which is invaluable for first runs on unfamiliar codebases.
Exit codes follow the usual Unix conventions so the script slots into CI pipelines. A successful run with no changes returns 0, a run that rewrites files returns 0 as well, and any unexpected error returns 1. Log messages are written to stderr, leaving stdout available for piping into other tools, including the wc count that some teams run after a reformat to confirm the codebase is still syntactically valid.
Backing up and verifying results
Safety features turn a useful script into one that developers trust. The --backup option relies on shutil.copy2, which preserves timestamps and permissions. Backups are written next to the original rather than into a separate folder, which keeps the cleanup simple for one-off runs and avoids surprise directories showing up in version control.
A quick verification step runs python -m py_compile on each rewritten file. That step catches the rare case where autopep8 produces output that no longer parses, which can happen when aggressive fixes interact with unusual code patterns. The verify step happens after writing the file but before declaring success, so a broken file is reported alongside the path that produced it.
The whole tool is covered by a handful of pytest cases that exercise the file discovery, the fix application, and the command line surface. Tests run in under a second on a typical laptop, which encourages running them frequently. For anyone in Australia hosting a small CI runner on a budget VPS in Sydney, that speed matters: the script can sit inside a pre-commit hook and still feel instant.
