Skip to main content

How to Convert Tabs to Spaces (and Back) Without Breaking Your Code

A practical guide to converting tabs to spaces, choosing the right tab width, and fixing mixed indentation that breaks Python and YAML, all locally.

Published By Li Lei
#tabs to spaces #indentation #code formatting #python #yaml

How to Convert Tabs to Spaces (and Back) Without Breaking Your Code

Indentation is the one thing in a file you can't see and can't get wrong. A tab and four spaces look identical on screen, but to a parser, a diff tool, or a teammate's editor they are three completely different things. Most of the time it doesn't matter. Then one day a Python script throws TabError, a YAML deploy file silently parses into the wrong shape, and a pull request shows 200 changed lines when you only touched 3. All of it traces back to invisible whitespace.

This is a guide to converting between tabs and spaces correctly, choosing the right tab width, and untangling files where the two are mixed. You can do all of it locally with the Tabs to Spaces Converter, no upload required.

What a tab width actually controls

The first thing to understand is that a tab is not a fixed number of spaces. A tab is a jump to the next tab stop, and tab stops sit at regular column intervals defined by the tab width.

Each tab expands to the spaces needed to reach the next multiple of the chosen tab width, commonly 2 or 4. With a tab width of 4, the stops sit at columns 4, 8, 12, and so on. A tab at the very start of a line jumps to column 4, so it expands to 4 spaces. But a tab placed after two characters only needs to fill columns 2 and 3, so it expands to just 2 spaces. That is exactly what your editor does when it renders the file, and it is why blindly swapping every tab for the same fixed count of spaces can wreck alignment.

A good converter offers both behaviors. Align-to-tab-stop is the correct, column-aware expansion. Fixed N spaces is the blunt version: every tab becomes exactly tab-width spaces regardless of where it sits. For pure leading indentation the two agree, because a tab at column 0 fills a full width either way. They only diverge when a tab sits mid-line after some text, which is common in hand-aligned tables or trailing comment alignment.

Why mixed indentation breaks Python and YAML

Most languages treat indentation as cosmetic. Python and YAML treat it as syntax, which is why they are the two formats most likely to blow up on mixed whitespace.

Python uses indentation to define blocks. Since Python 3, the interpreter refuses to guess when a file mixes tabs and spaces in a way it can't interpret consistently, and it raises TabError: inconsistent use of tabs and spaces in indentation. A block indented with a tab on one line and four spaces on the next may look pixel-perfect on screen, but to the parser it is ambiguous, and ambiguity is a hard error rather than a warning.

YAML is sneakier because it usually doesn't error at all. The spec forbids tabs for indentation, so an editor that inserts a tab where you expected spaces can break the document structure, collapsing a nested key to the wrong level. Instead of a crash you get a config that loads "successfully" with your values in the wrong place, which is a far worse failure mode. The cure for both is the same: make the indentation uniform. Run the file through a converter at one tab width in the tabs-to-spaces direction, and the ambiguity disappears because every level is now plain spaces.

A worked example

Here is a tab-indented snippet. The arrows mark where a real tab character sits:

def total(items):
→result = 0
→for item in items:
→→result += item.price
→return result

Converted to spaces at a tab width of 4, the leading tabs expand to consistent 4-space and 8-space runs:

def total(items):
    result = 0
    for item in items:
        result += item.price
    return result

Every indent level is now a clean multiple of 4 spaces. The nested result += item.price line gets 8 spaces because it sits two levels deep. If this file had originally mixed one tab-indented line with one 4-space-indented line, Python would have rejected it; after the conversion it runs without complaint, and nothing in the visible layout changed.

Keeping a team on one rule

Consistency matters more than which side you pick. Most JS and web teams standardize on 2 spaces, Go uses tabs, and Python projects usually settle on 4 spaces per PEP 8. The actual choice is less important than everyone obeying it, because mixed indentation is the number-one source of noisy diffs.

When half a file uses tabs and half uses spaces, the code looks aligned in one editor and ragged in another, and git blames every line that was merely retabbed rather than genuinely edited. That buries real changes under a wall of whitespace churn and makes review slower and less careful. The fix is to convert before you commit: paste the changed file, set the tab width your project uses, convert in leading-indent-only mode, and copy the result back. The diff then contains only the lines you actually changed.

To make the rule stick across editors automatically, pair the converter with an EditorConfig file. EditorConfig tells every editor in the project to use the same indent_style and indent_size, so new files start out correct and you only need the converter for cleanup and inbound contributions.

When to touch only the indent

A converter worth using lets you scope the change. Leading-indent-only mode rewrites just the run of tabs and spaces at the start of each line and leaves everything after the first non-whitespace character alone. That is the safe default for source code: a tab inside a string literal, or spaces used to align a trailing comment, survive untouched.

Whole-line mode converts every tab or space group anywhere in the line. It is the right choice for plain tabular text or TSV-style data, where the tabs are the data, but it can disturb in-code alignment. The most common mistake I see is reaching for whole-line mode on source files and accidentally rewriting a tab buried inside a string. When in doubt for code, keep leading-indent-only on, and only widen the scope when you specifically want every tab in the line to move.

Why I run this locally

I deal with a lot of inbound code, snippets from a chat, a file from a contributor, a paste from a Stack Overflow answer, and the first thing I check is the whitespace. Early on I'd paste straight into an online formatter, until I thought about what that means for a proprietary file: it's now sitting in someone else's server log. So I keep this conversion strictly client-side. Everything runs as plain JavaScript in the browser tab, nothing is uploaded, and there is no record of what I pasted. The one caveat is the share link, which encodes your input in the URL, so for private code I copy the cleaned text instead of sharing the link. It is a small habit, but it means I never have to wonder where a sensitive snippet ended up.

The same logic applies to the reverse direction. Contributing to a Go project or editing a Makefile, where recipe lines only accept real tabs, you can convert spaces back to tabs at the matching width. Each full group of leading spaces collapses into one tab and any leftover spaces stay put, which is the difference between a build that runs and one that errors with missing separator.

Whichever way you're going, the goal is the same: indentation that means exactly one thing, so the file behaves the same in every editor, parser, and diff that touches it.


Made by Toolora · Updated 2026-06-13