Bash & Python Error Guide: 'IndentationError' and 'TabError' (Mixed Tabs/Spaces)
Fix Python IndentationError and TabError: mixed tabs and spaces, inconsistent indent levels, unexpected indent/dedent, and editor settings that hide the problem.
- #automation
- #troubleshooting
- #errors
- #python
Overview
Python uses indentation as syntax, so the tokenizer is strict about it. An IndentationError means the leading whitespace of a line does not match what the grammar expects — too much, too little, or none where a block was required. A TabError is a specific subspecies: the file mixes tabs and spaces in a way that makes indentation ambiguous, and Python (in Python 3) refuses to guess. Because the offending whitespace is invisible in most editors, these errors are disproportionately confusing relative to how trivial the fix is.
The common forms:
File "/srv/app/handler.py", line 14
return result
^
IndentationError: unexpected indent
File "/srv/app/handler.py", line 22
do_work()
^
TabError: inconsistent use of tabs and spaces in indentation
These occur at compile time, before the module runs — Python parses the whole file first. The error points at the first line whose indentation cannot be reconciled, which is usually the line after the real inconsistency was introduced.
Symptoms
- A traceback with
IndentationError: unexpected indent,expected an indented block, orunindent does not match any outer indentation level. - A
TabError: inconsistent use of tabs and spaces in indentation. - The code looks perfectly aligned in your editor but fails to compile.
- It fails after a copy-paste from a web page, a diff merge, or switching editors.
python3 handler.py
File "/srv/app/handler.py", line 14
return result
^
IndentationError: unindent does not match any outer indentation level
Common Root Causes
1. Mixed tabs and spaces in the same block (TabError)
Some lines are indented with tabs, others with spaces, so the visual alignment lies. Python 3 treats this as ambiguous and raises TabError.
cat -A handler.py | sed -n '20,23p'
def process():$
^Iload()$
transform()$
^Isave()$
^I is a tab; the transform() line uses four spaces. Convert the file to spaces only:
python3 -c "import sys; sys.exit()" # nothing yet
expand -t 4 handler.py > handler.fixed && mv handler.fixed handler.py
(Or use your editor’s “convert indentation to spaces”.)
2. Unexpected indent (a line indented with no reason)
A line has more leading whitespace than its context allows — often a stray space before a top-level statement.
cat -A app.py | sed -n '12,14p'
x = 1$
y = 2$
z = 3$
y = 2 has one leading space but is not opening or inside a block. Remove the stray indent.
3. Expected an indented block
A def, if, for, while, try, or class header is not followed by an indented body (empty block, or the body got dedented).
python3 svc.py
File "svc.py", line 8
def handler():
^
IndentationError: expected an indented block after function definition on line 8
Add a body (even pass):
# def handler():
# pass
4. Unindent does not match any outer level
A closing line is dedented to a column that does not line up with any enclosing block — e.g., the body used 4 spaces but a later line uses 3.
cat -A loop.py | sed -n '5,8p'
for i in range(3):$
step_one()$
step_two()$
step_three()$
step_two() has 3 spaces, not 4. Normalize every line in the block to the same width.
5. An editor inserting tabs while you believe you typed spaces (or vice versa)
Different editors/configs produce different whitespace. Pasting code with tabs into a spaces project (or merging two files) silently mixes them.
grep -Pn '\t' app.py | head
17: value = compute()
grep -P '\t' lists every line containing a literal tab. Decide on one convention (PEP 8 says 4 spaces) and convert.
6. Whitespace differences hidden by a merge or copy from rendered HTML
Copying from a blog, PDF, or rendered notebook can bring non-breaking spaces (\xC2\xA0) that look like indentation but are not ASCII spaces.
grep -Pn '\xC2\xA0' app.py | head
9: process()
A non-breaking space precedes process(). Replace it:
sed -i 's/\xC2\xA0/ /g' app.py
Diagnostic Workflow
Step 1: Reproduce and read the exact line/column
python3 -m py_compile app.py
py_compile reports the error without running the module; the caret column hints at where indentation diverges.
Step 2: Reveal tabs vs spaces on the affected lines
cat -A app.py | sed -n '<start>,<end>p'
^I marks tabs; runs of spaces are plain. Mixed ^I and spaces in one block is a TabError.
Step 3: Ask Python’s tokenizer to verify tab consistency
python3 -t -W error -m py_compile app.py
python3 -tt app.py # error (not just warn) on inconsistent tabs
-tt turns inconsistent tab/space use into an error so it cannot be ignored.
Step 4: Hunt for stray tabs and non-breaking spaces
grep -Pn '\t' app.py
grep -Pn '\xC2\xA0' app.py
Step 5: Normalize to spaces and recompile
expand -t 4 app.py > app.tmp && mv app.tmp app.py
python3 -m py_compile app.py && echo OK
Example Root Cause Analysis
A monitoring script that ran for months suddenly fails after a teammate edited one function:
File "/opt/checks/disk.py", line 31
alert(host, usage)
^
TabError: inconsistent use of tabs and spaces in indentation
The file looks aligned in the reviewer’s editor. Revealing the whitespace around the function:
cat -A /opt/checks/disk.py | sed -n '28,32p'
def check(host):$
usage = read_usage(host)$
if usage > 90:$
^Ialert(host, usage)$
log(usage)$
Most of the file uses 4 spaces, but the new alert(...) line was indented with a single tab — the teammate’s editor inserted a tab while everyone else’s used spaces. The tab and the surrounding spaces are visually identical at a 4-wide tab stop but are different bytes, so Python 3 rejects the block as ambiguous.
Fix: convert the whole file to spaces and add a guard in CI:
expand -t 4 /opt/checks/disk.py > /tmp/disk.py && mv /tmp/disk.py /opt/checks/disk.py
python3 -m py_compile /opt/checks/disk.py && echo "compiles clean"
After conversion the indentation is uniform and the check runs again.
Prevention Best Practices
- Standardize on 4 spaces per PEP 8 and configure your editor to insert spaces for the Tab key; add an
.editorconfig(indent_style = space,indent_size = 4) so every contributor’s editor agrees. - Run
python3 -ttor a formatter (black) / linter (flake8,ruff) in CI; these turn mixed tabs/spaces into hard failures before merge. - Use
cat -A(or your editor’s “show whitespace”) whenever an IndentationError defies a visual explanation — the bytes always tell the truth. - Be wary of pasting code from rendered HTML, PDFs, or chat; convert non-breaking spaces and tabs immediately after pasting.
- Keep a
.gitattributesand pre-commit hook that normalizes whitespace so a single stray tab cannot reach the main branch. - See more language-tooling tips in the Bash & Python automation guides.
Quick Command Reference
# Compile-check without running
python3 -m py_compile app.py
# Treat inconsistent tabs/spaces as an error
python3 -tt app.py
# Reveal tabs (^I) and line ends
cat -A app.py | sed -n '<a>,<b>p'
# Find tabs and non-breaking spaces
grep -Pn '\t' app.py
grep -Pn '\xC2\xA0' app.py
# Convert tabs to 4 spaces
expand -t 4 app.py > app.tmp && mv app.tmp app.py
# Auto-format to consistent indentation
black app.py
Conclusion
IndentationError and TabError are Python’s tokenizer refusing whitespace it cannot reconcile. The recurring causes:
- Mixed tabs and spaces in one block (the classic
TabError). - A stray indent on a line that should be flush (
unexpected indent). - A block header with no indented body (
expected an indented block). - A dedent that matches no enclosing level (
unindent does not match). - An editor inserting the opposite whitespace from the rest of the file.
- Non-breaking spaces copied from rendered content masquerading as indentation.
Because the offending bytes are invisible, reach for cat -A, grep -P '\t', and python3 -tt immediately — then expand the file to spaces and enforce a formatter so it never recurs.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.