Skip to main content

format-dedent

Format multiline strings with proper indentation — A Python code formatter that formats only the literal string arguments of textwrap.dedent() calls.

✨ What does it do?

ci pypi version Python Versions PyPI - Downloads coverage GitHub Sponsors

format-dedent automatically formats multiline strings inside textwrap.dedent() calls to make them visually match their runtime output. This makes your code more readable while preserving the exact behavior.

Key features:

  • 🎯 Surgical precision — Only formats strings inside dedent() calls, leaves everything else untouched
  • 🔄 Two modes — Format existing dedent strings OR automatically add dedent() to strings that need it
  • 👀 Safe — Validates that formatting doesn't change runtime behavior
  • 🎨 Smart indentation — Aligns content with the visual structure of your code
  • 🧹 Clean — Removes trailing whitespace and normalizes spacing

📦 Installation

uv tool install format-dedent

🚀 Quick Start

Format strings (default mode)

Preview formatted output without modifying files:

uvx format-dedent yourfile.py

Write changes to files:

uvx format-dedent yourfile.py --write

Format multiple files or directories:

uvx format-dedent src/ tests/ --write

Add dedent() calls (--add-dedent mode)

Automatically wrap multiline strings with dedent() calls:

uvx format-dedent yourfile.py --add-dedent --write

This will:

  • Find multiline strings where dedent(str) == str (no leading indentation to remove)
  • Wrap them with dedent() calls
  • Add from textwrap import dedent import if needed

📖 Usage Options

python -m format_dedent [OPTIONS] [FILES/DIRECTORIES]

Options:
  -w, --write       Write changes to files (default: output to stdout)
  --add-dedent      Add dedent() calls to multiline strings
  -h, --help        Show help message

Behavior:

  • Default → Output formatted code to stdout (no file modification)
  • --write → Modify files directly and print confirmation

🔧 Pre-commit Hook

Use format-dedent as a pre-commit hook to automatically format dedent strings before each commit.

Add this to your .pre-commit-config.yaml:

repos:
  - repo: https://github.com/15r10nk/format-dedent
    rev: v0.1.0  # Use the latest version
    hooks:
      - id: format-dedent

💡 Examples

Example 1: Formatting SQL queries

Before formatting:

import textwrap


def get_sql_query():
    return textwrap.dedent("""
SELECT users.name, orders.total
FROM users
JOIN orders ON users.id = orders.user_id
WHERE orders.status = 'complete'
    """)

Inconsistent indentation inside the string makes it hard to read and understand the actual SQL query structure.

After formatting:

import textwrap


def get_sql_query():
    return textwrap.dedent("""
        SELECT users.name, orders.total
        FROM users
        JOIN orders ON users.id = orders.user_id
        WHERE orders.status = 'complete'
    """)

The key insight: The indentation you see in the source code now matches what dedent() returns. When this code runs, dedent() strips the common leading whitespace, and you get properly formatted SQL.

Example 2: Using --add-dedent mode

Before:

def get_message():
    message = """
Hello World!
This is a message.
"""
    return message

After running with --add-dedent:

from textwrap import dedent
def get_message():
    message = dedent("""
        Hello World!
        This is a message.
    """)
    return message

What changed:

  1. Detected that the string has no leading whitespace (left-aligned)
  2. Wrapped it with dedent() for consistency
  3. Added the import statement automatically
  4. Reformatted with proper indentation matching the code structure

Why use dedent here? Even though this string doesn't need dedenting now, using dedent() consistently makes it easier to modify the string later. You can add indentation for readability without affecting the runtime output.

Example 3: HTML template formatting

Before:

from textwrap import dedent


def render_html():
    return dedent("""
    <div class="container">
        <h1>Welcome</h1>
            <p>This is a paragraph.</p>
    </div>
    """)

After:

from textwrap import dedent


def render_html():
    return dedent("""
        <div class="container">
            <h1>Welcome</h1>
                <p>This is a paragraph.</p>
        </div>
    """)

Sponsors

I would like to thank my sponsors. Without them, I would not be able to invest so much time in my projects.

Silver sponsor 🥈

logfire


🧠 How It Works

  1. Parse — Uses Python's AST module to analyze source code
  2. Find — Locates all dedent() or textwrap.dedent() calls with string arguments
  3. Analyze — Determines the appropriate indentation level based on context
  4. Validate — Ensures dedent(original) == dedent(formatted) (behavior unchanged)
  5. Replace — Updates the source file with formatted strings

The key insight: Strings are formatted in the source to visually match their runtime output after dedent() processes them. This makes the code more readable without changing behavior.


🛡️ Safety & Compatibility

  • Non-destructive — Always validates that dedent(original) == dedent(formatted)
  • Preserves behavior — Formatted strings have identical runtime output
  • Quote style aware — Maintains your choice of """ vs '''
  • Escape handling — Correctly handles backslashes and escape sequences
  • Python 3.8+ — Works with modern Python versions

What gets formatted:

  • ✅ Literal strings inside textwrap.dedent() calls
  • ✅ Literal strings inside dedent() calls (when imported)

What doesn't get formatted:

  • ❌ Regular strings (not in dedent calls)
  • ❌ F-strings (can't be wrapped with dedent)
  • ❌ String concatenations
  • ❌ Docstrings at module level
  • ❌ All other code (completely untouched)

🧪 Development

Setup

# Clone the repository
git clone https://github.com/15r10nk/format-dedent.git
cd format-dedent

# Install with development dependencies
pip install -e ".[dev]"

# Install pre-commit hooks
pre-commit install

Running Tests

# Run all tests
pytest

# Run with verbose output
pytest -v

# Run specific test file
pytest tests/test_formatter.py

Tests use inline-snapshot for snapshot testing.


📝 License

MIT License - See LICENSE file for details


🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

format_dedent-0.2.0.tar.gz (118.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

format_dedent-0.2.0-py3-none-any.whl (13.8 kB view details)

Uploaded Python 3

File details

Details for the file format_dedent-0.2.0.tar.gz.

File metadata

  • Download URL: format_dedent-0.2.0.tar.gz
  • Upload date:
  • Size: 118.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for format_dedent-0.2.0.tar.gz
Algorithm Hash digest
SHA256 127a860eda6332b38ddc24e0c8926019dd09c672f0c4a3a18851aae1d98b51b4
MD5 3c9e8a6519d8dc32a60c903f01050f25
BLAKE2b-256 31ab7245d05848057aa9fe17d5b86e4c83f4385e69a21ac0d5b957bf74950832

See more details on using hashes here.

Provenance

The following attestation bundles were made for format_dedent-0.2.0.tar.gz:

Publisher: ci.yml on 15r10nk/format-dedent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file format_dedent-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: format_dedent-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 13.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for format_dedent-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 666e4f254f3ad52ddf41e5607a0c16fc1ab6656de6a4d93ba647ab8350e67b15
MD5 69521c929874389be4cbd5013367d0be
BLAKE2b-256 cb40f1429c664fa18cac1730bf38d2c9672f7dca51216b798bdce7127683d21f

See more details on using hashes here.

Provenance

The following attestation bundles were made for format_dedent-0.2.0-py3-none-any.whl:

Publisher: ci.yml on 15r10nk/format-dedent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page