Skip to main content

dedent

Write multiline strings naturally. Keep every interpolation aligned.

Supports both f-strings and t-strings.

from dedent import dedent


def showcase():
    features = dedent("""
        ✦ clean multiline strings
        ✦ automatic column alignment
        ✦ zero dependencies""")
    install = dedent("""
        uv add dedent
        pip install dedent""")

    print(
        dedent(t"""
        ╭─ dedent ──────────────────────────
          Features: {features}
          Install:  {install}
        ╰───────────────────────────────────""")
    )


showcase()
# ╭─ dedent ──────────────────────────
#   Features: ✦ clean multiline strings
#             ✦ automatic column alignment
#             ✦ zero dependencies
#   Install:  uv add dedent
#             pip install dedent
# ╰───────────────────────────────────

This example uses t-strings and requires Python 3.14+.

Table of Contents

Installation

# Using uv (Recommended)
uv add dedent

# Using pip
pip install dedent

Usage

dedent() removes shared indentation and omits the newline after the opening triple quotes:

from dedent import dedent

message = dedent("""
    Hello,
    World!""")

print(message)
# Hello,
# World!

See Dedentation Rules for newline, closing-quote, and mixed-indentation behavior.

t-string alignment

Requires Python 3.14+.

When a t-string interpolation evaluates to a multiline string, dedent aligns each subsequent line to the column where the interpolation begins. Column alignment is enabled by default. Do not wrap interpolations with align(); that wrapper is only for f-strings and raises TypeError here.

Use the dedent-specific noalign format specifier to disable alignment for an individual value:

  • {value:noalign} - Disable column alignment for this value
  • {value:06d:noalign} - Combine with other format specs
from dedent import dedent

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

result = dedent(t"""
    Aligned by default:
        {items}
    Not aligned:
        {items:noalign}""")

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

f-string alignment

For f-strings on every supported Python version. Use t-string alignment on Python 3.14+ t-strings; align() inside a t-string interpolation raises TypeError.

Use align() with f-strings because Python 3.10-3.13 do not support t-strings:

from dedent import align, dedent

items = dedent("""
    - apples
    - bananas
    - cherries""")
shopping_list = dedent(f"""
    Groceries:
        {align(items)}""")
print(shopping_list)
# Groceries:
#     - apples
#     - bananas
#     - cherries

Python renders an f-string before calling dedent(). The align() wrapper marks each multiline value so dedent() can align its continuation lines.

Why textwrap.dedent Falls Short

textwrap.dedent preserves the newline after the opening triple quotes. To keep the source readable without printing a blank first line, you must escape that newline or remove it afterward. It also cannot preserve indentation when an f-string inserts a multiline value:

from textwrap import dedent

items = dedent("""\
    - apples
    - bananas
    - cherries""")
shopping_list = dedent(f"""\
    Groceries:
        {items}""")

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

The escape removes the first newline. It cannot fix the indentation: Python renders the f-string first, so the continuation lines begin at column zero and prevent textwrap.dedent from finding shared indentation.

dedent() receives a t-string's literal segments and interpolations separately:

from dedent import dedent

items = dedent("""
    - apples
    - bananas
    - cherries""")
shopping_list = dedent(t"""
    Groceries:
        {items}""")

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

It dedents the literal text, then renders and aligns each continuation line with the interpolation column. On Python 3.10-3.13, align() provides the same alignment for f-strings.

Dedentation Rules

dedent follows the indentation model proposed by PEP 822:

  1. Indentation is an exact prefix of spaces and tabs; a tab never equals spaces.
  2. The longest indentation prefix shared by every nonblank line is removed.
  3. Lines containing only spaces and tabs do not determine that prefix, except for the final line. A final space/tab-only line is treated like the line containing closing triple quotes.
  4. Every line must be compatible with the chosen prefix. Otherwise, IndentationError is raised.
  5. The closing-quotes indentation can preserve intentional indentation:
result = dedent("""
      Hello
      World!
    """)

assert result == "  Hello\n  World!\n"

Keep the closing quotes at the indentation you want removed. Their placement also controls the final newline:

with_newline = dedent("""
    Hello
    World!
    """)
without_newline = dedent("""
    Hello
    World!""")

assert with_newline == "Hello\nWorld!\n"
assert without_newline == "Hello\nWorld!"

A final \n creates an empty closing line at column zero. This line preserves content indentation:

assert dedent("\n    Hello\n") == "    Hello\n"
assert dedent("\n    Hello\n    ") == "Hello\n"

Why IndentationError?

Space/tab-only lines are ignored when finding the common prefix, but they must still be compatible with it when that prefix is removed:

dedent("\n  hello\n \t\n  world\n  ")
# IndentationError: inconsistent indentation in dedented string at line 3

Here the nonblank lines and closing line establish a two-space prefix. The middle line contains one space followed by a tab, so that exact prefix cannot be removed. Raising an error catches mixed or malformed indentation instead of silently changing or preserving ambiguous whitespace. A shorter space/tab-only line is valid when it matches the beginning of the prefix; it simply becomes empty. This mirrors PEP 822.

Divergences from PEP 822

PEP 822 defines new literal syntax, while dedent() processes runtime values. The differences are:

  • PEP 822 requires the opening quotes to be followed immediately by a newline. dedent() also accepts spaces or tabs before that first line ending and treats the whole line as the opener. To preserve an intentional leading blank line, put it after the opener line. Values without an opening line ending are also accepted.
  • Python has already processed escape sequences before dedent() receives a string. For t-strings, dedent() processes literal text before rendering interpolations.
  • Runtime strings can contain CRLF line endings. dedent() preserves each \r\n pair and excludes the terminal \r from indentation checks.

All other whitespace, including form feed and non-breaking space, remains content. Use the closing-quote placement when you do not want a final newline.

Release files for dedent 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dedent 1.0.0
File Size Uploaded
dedent-1.0.0.tar.gz 41.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dedent 1.0.0
File Interpreter ABI Platform
dedent-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 51.8 kB

Release files / dedent-1.0.0.tar.gz

Download URL dedent-1.0.0.tar.gz
Size 41.1 kB
Tags Source
SHA-256 checksum
How to use checksums
25d9bc20abe93c72fb32ff56a69927a74aa544644a0159e0f2faa45f7732b0c1
BLAKE2b-256 checksum
How to use checksums
e03eba81f4d1631e429d08261510e38200a748eb330b6ab46725c2fa60ddaae4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 18, 2026.

Transparency log

Release files / dedent-1.0.0-py3-none-any.whl

Download URL dedent-1.0.0-py3-none-any.whl
Size 10.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a684fc0ef30054bc0991d8de765881f65893457f45e3eb23ea8d19d30556ea8a
BLAKE2b-256 checksum
How to use checksums
cebac30620d730755adb7875e5b2d048ad817c119c35a7079b09b6ad6acc3905
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 18, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.8.0

2 release files

0.7.2

2 release files

0.7.0

1 release file

0.6.0

1 release file

0.5

1 release file

0.4

1 release file

0.3

1 release file

0.2

1 release file

0.1

2 release 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