Inconsistent indentation — a mix of tabs and spaces, or the wrong tab width — is one of the most common causes of noisy diffs and broken layout. This tool converts cleanly in both directions while respecting how tabs actually behave.
The tab-versus-spaces debate (and why it matters practically)
The eternal tabs-versus-spaces argument exists because tabs render differently depending on the tool. A file indented with tabs looks fine in an editor set to width 4, but looks completely different in a terminal or web viewer where the default is width 8. Spaces are visually consistent everywhere, but they bake a specific visual width into the file.
In practice, what matters most is consistency within a project. Python will error on mixed indentation. Go formats source code with tabs by convention (via gofmt). Most JavaScript and TypeScript projects use two or four spaces (via Prettier or ESLint). When you receive a file from a different project or editor, you need to convert before integrating it — which is exactly what this tool does.
How tab expansion works
A tab character does not equal a fixed number of spaces; it advances the cursor to the next tab stop — the next column that is a multiple of the tab width. The expansion algorithm tracks the current column position as it moves left to right through each line:
next_stop = floor(column / width) * width + width
spaces = next_stop - column
Because the column position depends on all the characters before the tab on that line, a tab at column 0 always adds exactly width spaces, while a tab at column 5 in a width-4 setting adds only 3 spaces (to reach column 8). This is why naive tab-to-space replacement (replacing every tab with N spaces) is wrong — it only works if every tab is at a column that is a multiple of N.
How space-to-tab compression works
Compressing back to tabs is applied only to leading indentation — the spaces at the start of each line:
- The existing leading indentation is expanded to spaces first (normalizing any mixed input).
- Every complete group of
widthspaces becomes one tab. - Any remaining spaces (not enough to form a full tab stop) stay as spaces.
Spaces inside a line — used for alignment, for example to line up the values in a config file — are left untouched. Replacing those with tabs would corrupt the alignment.
Worked example
Given width 4, the line ····if x: (4 spaces of indent) compresses to \tif x:. The line ··if x: (2 spaces) stays as ··if x: — two spaces are not enough to form a full tab stop, so they stay as spaces.
Going the other direction, \t\tresult (two tabs) expands to ········result (eight spaces at width 4) or result (sixteen spaces at width 8), depending on the chosen width — which is why the tool asks you to specify.
Practical tips
- Set the width to match your editor or formatter. For Python the convention is 4 spaces, for JavaScript/TypeScript often 2, for Go the
gofmtdefault produces a single tab per indent level. - Convert before committing. Running the converter before adding a third-party file to your repo prevents a noisy diff where every line appears changed simply because of indentation format.
- Tabs to spaces before pasting into web tools. Browsers, chat apps, and form fields often render tabs as a very wide stop (8 columns is common), making pasted code look strange. Converting first keeps the formatting readable.