Python tabnanny: Fix Ambiguous Indentation

Published on: August 4, 2026
Reading time: 6 minutes
Code editor representing tab and space correction with Python tabnanny

Python uses indentation to define blocks, so mixed tabs and spaces can produce errors that are difficult to see. A file may look aligned in one editor but form different levels in another because tab width is configurable. The Python tabnanny module scans files and directories for ambiguous indentation before it causes failures or inconsistent interpretation.

This guide explains command-line usage, recursive project checks, check(), process_tokens(), and a dependable whitespace policy. It complements our articles about Python tokenize, IndentationError, TabError, bytecode with dis, and Python coding practices.

Why tabs and spaces cause problems

A tab character does not represent a fixed number of visual spaces. Editors may display it with a width of two, four, or eight columns. Two lines that appear aligned may contain different whitespace sequences.

if active:
	process()
    finish()

Depending on effective columns, the interpreter may raise TabError, IndentationError, or interpret the blocks differently from the author’s intention.

What tabnanny detects

Tabnanny looks for indentation whose meaning depends on tab width. It is not a formatter and does not rewrite a file. Its purpose is to identify ambiguous lines so the original source can be corrected.

The official tabnanny documentation says the module is primarily intended to be called as a script, although IDEs may import it.

Check one file

Run the module with -m and provide a path.

python -m tabnanny program.py

When no problem is found, the command normally prints nothing. An ambiguity produces a diagnostic with the filename, line, and whitespace details.

Check a directory recursively

When the argument is a directory, tabnanny recursively visits its tree and checks .py files.

python -m tabnanny src

Directories that are symbolic links are not traversed as ordinary directories. Large projects should still choose a narrow root to avoid virtual environments, copied dependencies, and generated output.

Verbose output

The -v option increases progress messages.

python -m tabnanny -v src

Repeating the option can increment internal verbosity. It is useful during local diagnosis, while continuous integration usually benefits from shorter output.

Filename-only mode

The -q option prints only filenames that contain problems.

python -m tabnanny -q src

This output can feed another script, but it omits line details. Run the check again without -q when fixing the source.

Call check() from Python

tabnanny.check() accepts a file or directory.

import tabnanny

tabnanny.check("src")

Diagnostics are written to standard output through print(). The function was not designed as a modern structured API, so callers must redirect output or carefully use lower-level pieces.

Capture output

from contextlib import redirect_stdout
from io import StringIO
import tabnanny

output = StringIO()

with redirect_stdout(output):
    tabnanny.check("src")

report = output.getvalue()
print(report)

Process-wide stdout redirection is unsafe in a multithreaded server. Concurrent tools should execute tabnanny in a subprocess.

Process tokens directly

process_tokens() accepts tokens produced by tokenize.

import tabnanny
import tokenize

with open("program.py", "rb") as file:
    tokens = tokenize.tokenize(file.readline)
    tabnanny.process_tokens(tokens)

When ambiguity is detected, it raises NannyNag. The higher-level check() function catches this exception and prints a diagnostic.

Catch NannyNag

try:
    with open("program.py", "rb") as file:
        tokens = tokenize.tokenize(file.readline)
        tabnanny.process_tokens(tokens)
except tabnanny.NannyNag as error:
    print("Ambiguous indentation:", error)

The exception contains information used by the report, but this surface may change. The documentation warns that the programmatic API may not remain backward compatible.

An API that may change

Tabnanny is an older command-oriented utility. Tools that depend on internal details should pin Python versions, maintain compatibility tests, and offer a fallback.

For a product that requires stable structured data, consider invoking python -m tabnanny in a subprocess or implementing a rule over INDENT and DEDENT tokens.

tabnanny versus TabError

TabError is raised when the interpreter encounters inconsistent tabs and spaces during compilation. Tabnanny can inspect an entire source tree without importing or executing its modules.

It therefore acts as a preventive check in commits, builds, and editors.

tabnanny versus IndentationError

IndentationError covers broader issues such as a missing suite, unexpected indentation, or unmatched dedent.

if active:
print("missing indentation")

Tabnanny has the narrower purpose of whitespace ambiguity. Projects should also compile or parse source to detect other syntax errors.

Continuous integration

A simple CI job can scan project folders.

python -m tabnanny src tests

Confirm the exit-code behavior of the selected Python version. Since the utility is designed around printed diagnostics, a reliable pipeline may wrap it and fail when output is produced.

import subprocess
import sys

result = subprocess.run(
    [sys.executable, "-m", "tabnanny", "src"],
    capture_output=True,
    text=True,
)

if result.stdout.strip() or result.stderr.strip():
    print(result.stdout)
    print(result.stderr)
    raise SystemExit(1)

Test the wrapper with a deliberately ambiguous file.

Pre-commit checks

A Git hook can run the check on changed Python files before commit.

python -m tabnanny file1.py file2.py

The default interface accepts paths, so a wrapper may invoke it once per file. Exclude virtual environments and generated artifacts.

Fix ambiguous whitespace

The safest correction is to convert indentation to spaces, usually four per level, without altering tabs inside strings or data.

Use the editor’s indentation conversion command, inspect the diff, and run tests. A global replacement of every \t may corrupt intentional string content.

Configure the editor

Enable:

  • spaces when pressing Tab;
  • a visual width of four spaces;
  • visible whitespace characters;
  • trailing-whitespace cleanup;
  • per-file indentation detection.

An .editorconfig file helps share policy among different IDEs.

Formatters and linters

Formatters such as Black generally normalize indentation for valid code. Linters can detect tabs and additional style issues. Tabnanny remains useful because it ships with Python and requires no dependency.

A reasonable order is: detect syntax errors, fix ambiguous whitespace, format, lint, and run tests.

Generated files and dependencies

Do not automatically edit third-party code inside a virtual environment. Exclude directories such as .venv, build, dist, and caches by selecting the correct scan root.

If generated source fails the check, correct the generator rather than only patching its output.

Encoding considerations

Tabnanny relies on tokenize, which respects a UTF-8 BOM and encoding cookies. An encoding problem may appear before indentation analysis.

Use UTF-8 for modern projects and keep fixtures for legacy encodings that must remain supported.

Temporarily incomplete source

While a developer types, a file may contain an unfinished string or open delimiter. Tokenization can raise TokenError. IDEs should debounce analysis or wait for a save.

Do not turn every intermediate state into a persistent warning.

An isolated checker

from pathlib import Path
import subprocess
import sys


def check(path: Path) -> list[str]:
    result = subprocess.run(
        [sys.executable, "-m", "tabnanny", str(path)],
        capture_output=True,
        text=True,
        timeout=30,
    )
    lines = result.stdout.splitlines() + result.stderr.splitlines()
    return [line for line in lines if line.strip()]

The timeout limits unexpected scans of very large trees.

Security

The utility reads source without executing it, which is safer than importing modules. Untrusted paths can still refer to enormous trees, special files, or locations outside an allowed project.

Resolve the permitted root, reject traversal outside it, limit file count and size, and run with minimal permissions.

Common mistakes

  • Expecting tabnanny to format the file.
  • Scanning an entire virtual environment.
  • Redirecting global stdout inside a multithreaded server.
  • Depending on internal details without version tests.
  • Replacing tabs inside string literals.
  • Calling every IndentationError a tab ambiguity.
  • Patching generated files instead of the generator.
  • Scanning an unbounded path.

Best practices

  • Standardize on four spaces.
  • Run tabnanny on changed files and in CI.
  • Combine it with parsing, formatting, linting, and tests.
  • Exclude dependencies and generated artifacts.
  • Use a subprocess for concurrent integrations.
  • Test compatibility for supported Python releases.
  • Fix generators that emit ambiguous whitespace.
  • Protect the scan root and limit resources.

Conclusion

The Python tabnanny module detects ambiguous indentation caused by mixing tabs and spaces. It can inspect one file or recursively scan a directory, making it a lightweight check for editors, hooks, and pipelines.

Its scope is specific and its programmatic API may change, but its purpose is clear: find whitespace problems before execution. With a four-space policy, visible whitespace, a formatter, and automated tests, tabnanny helps keep Python blocks predictable in every editor.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Source code and syntax representing lexical analysis with Python tokenize
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python tokenize: Analyze Source Code

    Learn Python tokenize to inspect tokens, comments, encodings, source positions, and rebuild Python code safely.

    Ler mais

    Tempo de leitura: 5 minutos
    04/08/2026
    Programming terminal representing interactive input compilation with Python codeop
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python codeop: Compile Interactive Input

    Learn Python codeop to detect complete interactive input, compile REPL commands, and preserve __future__ state securely.

    Ler mais

    Tempo de leitura: 6 minutos
    04/08/2026
    Developer analyzing code structure and symbol tables with Python symtable
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python symtable: Scopes and Symbols

    Learn Python symtable to analyze scopes, symbols, globals, nonlocals, closures, imports, annotations, and type parameters.

    Ler mais

    Tempo de leitura: 6 minutos
    03/08/2026
    Monitor with binary code representing bytecode analysis with Python dis
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python dis: Understand Bytecode

    Learn Python dis to inspect bytecode instructions, adaptive caches, source positions, tracebacks, and CPython implementation details.

    Ler mais

    Tempo de leitura: 6 minutos
    03/08/2026
    Error screen representing crash and deadlock diagnosis with Python faulthandler
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python faulthandler: Diagnose Crashes

    Learn Python faulthandler to diagnose crashes, deadlocks, and timeouts using thread dumps and native C stack information.

    Ler mais

    Tempo de leitura: 6 minutos
    03/08/2026
    Laptop with code representing Python traceback analysis and debugging
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python traceback: Errors and Call Stacks

    Learn Python traceback to capture, format, and log error call stacks safely without leaking sensitive data or retaining memory.

    Ler mais

    Tempo de leitura: 6 minutos
    03/08/2026