Python is the one mainstream language where whitespace decides what your code does
A stray space in JavaScript costs nothing. In Python, indentation is syntax. Mix one tab with four spaces inside the same block and the interpreter raises TabError before a line runs. Paste a method one level too deep and the program still starts, quietly attached to the wrong parent. Formatting Python is two jobs wearing one name. Operator spacing is cosmetic. Indentation is structural, and a formatter with a loose grip on the difference will break your file.
This page keeps the two apart. Indentation is rebuilt from the block depth of your file rather than by swapping tab characters for spaces, so a module written with tabs, one written with three spaces, and one where two people disagreed all land on the same grid. Operator spacing runs over code only. Strings, docstrings and comments are lifted out of every line before a single space moves.
What changes, line by line
| You wrote | You get | Rule behind it |
|---|---|---|
| total=0 | total = 0 | E225, spaces around an assignment |
| fetch(url,retries = 3) | fetch(url, retries=3) | E251, a keyword argument stays tight |
| def run(delay: float=1.0) | def run(delay: float = 1.0) | An annotated default takes spaces |
| def area ( w,h ) : | def area(w, h): | E211 and E203, no padding before brackets or colons |
| if code==404 : | if code == 404: | E225 on comparison operators |
| hits[k]+=1 #tally | hits[k] += 1 # tally | E262, two spaces before an inline comment |
| d = { 'a' :1 } | d = {'a': 1} | E201 and E231 inside a dict literal |
| → one tab of indent | four spaces | W191, tabs are remapped by level |
| five blank lines | two | E303, blank line runs are capped |
| def next to the one above | two blank lines between | E302 around top level definitions |
The equals sign follows two rules, and mixing them up is the giveaway
Most formatters written in an afternoon apply one rule to = and produce output no Python team accepts. PEP 8 asks for spaces around assignment at statement level, no spaces around a keyword argument or a bare default, and spaces again once that default carries an annotation. All three appear in a single line of ordinary code.
def send(url, retries=3, backoff: float = 1.5):
retries=3 is a bare default sitting inside brackets, so it stays tight. backoff carries a type, and the annotation brings the spaces back. Move down into the function body, write timeout = 5, and the spaces are required a third time. This page tracks bracket depth character by character while reading each line, which is how one symbol gets three different answers in the same function.
Indentation is rebuilt from depth, not from a find and replace
Swapping every tab for four spaces works until somebody used two spaces on one line and a tab on the next. The formatter measures the leading whitespace of each statement, compares it against a stack of the levels seen so far, and emits your chosen unit multiplied by the depth. A file indented with three spaces comes out at four. A file with a mixed block comes out consistent, and the TabError goes with it.
Two things stay untouched on purpose. Continuation lines inside an open bracket keep their own alignment, because a hanging indent carries meaning a depth counter has no way to read. Closing a docstring on its own line keeps the whitespace in front of the quotes, since those spaces belong to the string rather than to the code.
Long lines get counted, never broken
Pick 79 for the PEP 8 letter, 88 to match Black, or 120 for teams who settled there years ago. Lines past the limit are reported by number under the editor with nothing rewritten. Breaking a long call means deciding where arguments belong and whether a trailing comma is coming, and a wrong guess there rewrites the diff on every future edit of the same block. Line numbers point you at the work. The judgment stays yours.
Arithmetic operators keep whatever spacing you gave them. pycodestyle ignores E226 by default because PEP 8 recommends tightening around the higher priority operator, so dist = x*x + y*y reads better than a version with identical spacing everywhere. Install Black when you want every operator normalized without argument.
Three passes, in this order
- Each line is split into code, strings and comments. Triple quoted blocks are tracked across line breaks, so a docstring holding
x=1or a SQL query with commas comes through untouched. Prefixed literals such asf"…",rb'…'and raw regex patterns are recognized as strings, not code. - Only the code fragments are rewritten. Runs of spaces collapse, comparison and augmented assignment operators get their spacing, brackets lose their inner padding, commas gain a following space, and the two equals rules are applied against live bracket depth.
- The vertical layout is rebuilt last. Statements are reindented by level, blank line runs are capped at two at the top level and one inside a block, PEP 8 gaps are inserted around definitions, trailing whitespace is stripped, and a comment sitting directly above a function keeps its place attached to that function.
Everything happens in your browser tab
No upload, no API call, no server log. Cut your connection after the page loads and the formatter keeps working, because the parser and the rewriter are both plain JavaScript running in front of you. Python pasted into web tools routinely carries connection strings, internal hostnames and API keys sitting in os.environ.get defaults, so a round trip to somebody else's server buys nothing worth the risk.
Where this formatter stops
- No parse step. The page reads text, not an abstract syntax tree. A file with a syntax error still comes back formatted rather than rejected, so run the result through Python before committing it.
- No wrapping. Long calls, long conditions and long string literals stay on one line and show up in the over limit counter instead.
- No import work.
import re, sysstays on one line and nothing gets sorted or grouped. isort exists for exactly that job. - No lint fixes.
x != Nonekeeps its operator, becauseis not Nonebehaves differently on any object with a custom__eq__. Unused imports, bareexceptand mutable default arguments all survive untouched. - Slice colons are left as written.
a[1:2]anda[start : end]are both correct under PEP 8, and guessing which one you meant helps nobody. - Manual alignment is lost. Columns of assignments padded into a grid collapse to single spaces. PEP 8 asks for this. Plenty of teams still dislike seeing it happen.
- Two statements joined on one line stay joined. Splitting them changes the line count your traceback reports.
- This is not Black. Black rewrites a file from its parse tree, normalizes quotes, adds magic trailing commas and wraps at 88 columns. For a repository with a CI check, install Black or Ruff and let this page handle the snippet somebody pasted into chat.
- Around 2 MB is the ceiling. Browser editors slow to a crawl past that, and a module that size wants a local tool anyway.
