Skip to content

grahamcracker1234/dedent

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dedent

What textwrap.dedent should have been.

Dedent and strip multiline strings, and keep interpolated values aligned—without manual whitespace wrangling. Supports t-strings (Python 3.14+) and f-strings (Python 3.10+).

For documentation on how to use dedent with f-strings in Python 3.10-3.13, see Legacy Support.

Table of Contents

Usage

from dedent import dedent

name = "Alice"
greeting = dedent(t"""
    Hello, {name}!
    Welcome to the party.
""")
print(greeting)
# Hello, Alice!
# Welcome to the party.

# Nested multiline strings align correctly with :align
items = dedent("""
    - apples
    - bananas
""")
shopping_list = dedent(t"""
    Groceries:
        {items:align}
    ---
""")
print(shopping_list)
# Groceries:
#     - apples
#     - bananas
# ---

Installation

# Using uv (Recommended)
uv add dedent

# Using pip
pip install dedent

Options

align

When an interpolation evaluates to a multiline string, only its first line is placed where the {...} appears. Subsequent lines keep whatever indentation they already had (often none), so they can appear "shifted left". Alignment fixes this by indenting subsequent lines to match the first.

Format Spec Directives

Requires Python 3.14+ (t-strings).

Use format spec directives inside t-strings for per-value control:

  • {value:align} - Align this multiline value to the current indentation
  • {value:noalign} - Disable alignment for this value 1
  • {value:align:06d} - Combine with other format specs 2
from dedent import dedent

items = dedent("""
    - one
    - two
""")

result = dedent(t"""
    Aligned:
        {items:align}
    Not aligned:
        {items}
""")

print(result)
# Aligned:
#     - one
#     - two
# Not aligned:
#     - one
# - two

align Argument

Requires Python 3.14+ (t-strings).

Pass align=True to enable alignment globally for all t-string interpolations. Format spec directives override this.

from dedent import dedent

items = dedent("""
    - one
    - two
""")

result = dedent(
    t"""
        List 1:
            {items}
        List 2:
            {items}
        ---
    """,
    align=True,
)

print(result)
# List 1:
#     - one
#     - two
# List 2:
#     - one
#     - two
# ---

strip

The strip parameter controls how leading and trailing whitespace is removed after dedenting. It accepts three modes:

"smart" (default)

Strips one leading and trailing newline-bounded blank segment. Handles the common case of triple-quoted strings that start and end with a newline.

from dedent import dedent

result = dedent("""
    hello!
""")

print(repr(result))
# 'hello!'

"all"

Strips all surrounding whitespace, equivalent to calling .strip() on the result. Use when the string may have extra blank lines you want removed.

from dedent import dedent

result = dedent(
    """


        hello!


    """,
    strip="all",
)
print(repr(result))
# 'hello!'

"none"

Leaves whitespace exactly as-is after dedenting. Use when you need to preserve exact whitespace, e.g. for diff output or tests.

from dedent import dedent

result = dedent(
    """
        hello!
    """,
    strip="none",
)

print(repr(result))
# '\nhello!\n'

Legacy Support (Python 3.10-3.13)

On Python 3.10-3.13, t-strings are not available. Use dedent() on plain strings and f-strings, and wrap interpolated values with align() to get multiline indentation alignment.

from dedent import align, dedent

# dedent works on regular strings, like textwrap.dedent
message = dedent("""
    Hello,
    World!
""")
print(message)
# Hello,
# World!

# Use align() inside f-strings for multiline value alignment
items = dedent("""
    - apples
    - bananas
""")
shopping_list = dedent(f"""
    Groceries:
        {align(items)}
    ---
""")
print(shopping_list)
# Groceries:
#     - apples
#     - bananas
# ---

Per-value control with align() mirrors the format spec directives available on 3.14+:

from dedent import align, dedent

items = dedent("""
    - one
    - two
""")

result = dedent(f"""
    Aligned:
        {align(items)}
    Not aligned:
        {items}
""")

print(result)
# Aligned:
#     - one
#     - two
# Not aligned:
#     - one
# - two

There is no equivalent of the align argument in Python 3.10-3.13. There is no way to automatically align multiline values when using f-strings.

Why textwrap.dedent Falls Short

If you're here, then you're probably already familiar with the shortcomings of textwrap.dedent. But regardless, let's spell it out for the sake of completeness. For example, say we want to create a nicely formatted shopping list that includes some groceries:

from textwrap import dedent

groceries = dedent("""
    - apples
    - bananas
    - cherries
""")

shopping_list = dedent(f"""
    Groceries:
        {groceries}
    ---
""")

print(shopping_list)
#
#     Groceries:
#
# - apples
# - bananas
# - cherries
#
#     ---

Wait, that's not what we wanted. We accidentally included leading and trailing newlines from the groceries string. Now, we could do that manually by removing, escaping, or stripping the newlines, but it's either easy to forget, difficult to read, or unnecessarily verbose.

# Removing the newlines
groceries = dedent("""    - apples
    - bananas
    - cherries""")

# Escaping the newlines
groceries = dedent("""\
    - apples
    - bananas
    - cherries\
""")

# Stripping the newlines
groceries = dedent("""
    - apples
    - bananas
    - cherries
""".strip("\n"))

# But the shopping list still comes out wrong:
#     Groceries:
#         - apples
# - bananas
# - cherries
#     ---

Uh oh, something is still wrong; the indentation is not correct at all. The interpolation happens too early. When we use an f-string with textwrap.dedent, the replacement occurs before dedenting can take place. Notice how only the first line of groceries is properly indented relative to the surrounding text? The subsequent lines lose their indentation because f-strings interpolate immediately, injecting the groceries string before dedent can process the overall structure.

Sure, we could manually adjust the indentation with a bit of string manipulation, but that's a pain to read, write, and maintain.

from textwrap import dedent

groceries = dedent("""
    - apples
    - bananas
    - cherries
""".strip("\n"))

manual_groceries = ("\n" + " " * 8).join(groceries.splitlines())

shopping_list = dedent(f"""
    Groceries:
        {manual_groceries}
    ---
""".strip("\n"))

dedent solves these problems and more:

from dedent import dedent

groceries = dedent("""
    - apples
    - bananas
    - cherries
""")

shopping_list = dedent(t"""
    Groceries:
        {groceries:align}
    ---
""")

print(shopping_list)
# Groceries:
#     - apples
#     - bananas
#     - cherries
# ---

Footnotes

  1. Only has an effect when using the align=True argument.

  2. This rarely makes sense, unless you are also using custom format specifications, but nonetheless works.