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.
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
# ---# Using uv (Recommended)
uv add dedent
# Using pip
pip install dedentWhen 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.
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
# - twoRequires 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
# ---The strip parameter controls how leading and trailing whitespace is removed after dedenting. It accepts three modes:
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!'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!'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'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
# - twoThere is no equivalent of the
alignargument in Python 3.10-3.13. There is no way to automatically align multiline values when using f-strings.
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
-
Only has an effect when using the
align=Trueargument. ↩ -
This rarely makes sense, unless you are also using custom format specifications, but nonetheless works. ↩