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 raisesTypeError.
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:
- Indentation is an exact prefix of spaces and tabs; a tab never equals spaces.
- The longest indentation prefix shared by every nonblank line is removed.
- 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.
- Every line must be compatible with the chosen prefix. Otherwise,
IndentationErroris raised. - 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\npair and excludes the terminal\rfrom 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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dedent-1.0.0.tar.gz.
File metadata
- Download URL: dedent-1.0.0.tar.gz
- Upload date:
- Size: 41.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25d9bc20abe93c72fb32ff56a69927a74aa544644a0159e0f2faa45f7732b0c1
|
|
| MD5 |
3e19b25fd68c9483ce21b477d00a5524
|
|
| BLAKE2b-256 |
e03eba81f4d1631e429d08261510e38200a748eb330b6ab46725c2fa60ddaae4
|
Provenance
The following attestation bundles were made for dedent-1.0.0.tar.gz:
Publisher:
release.yaml on grahamcracker1234/dedent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dedent-1.0.0.tar.gz -
Subject digest:
25d9bc20abe93c72fb32ff56a69927a74aa544644a0159e0f2faa45f7732b0c1 - Sigstore transparency entry: 2506487921
- Sigstore integration time:
-
Permalink:
grahamcracker1234/dedent@a913c7398fbf06e530a7cc30457cca009140d658 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/grahamcracker1234
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@a913c7398fbf06e530a7cc30457cca009140d658 -
Trigger Event:
push
-
Statement type:
File details
Details for the file dedent-1.0.0-py3-none-any.whl.
File metadata
- Download URL: dedent-1.0.0-py3-none-any.whl
- Upload date:
- Size: 10.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a684fc0ef30054bc0991d8de765881f65893457f45e3eb23ea8d19d30556ea8a
|
|
| MD5 |
34f2a0133f92fc2f3386bb98ca6839cc
|
|
| BLAKE2b-256 |
cebac30620d730755adb7875e5b2d048ad817c119c35a7079b09b6ad6acc3905
|
Provenance
The following attestation bundles were made for dedent-1.0.0-py3-none-any.whl:
Publisher:
release.yaml on grahamcracker1234/dedent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dedent-1.0.0-py3-none-any.whl -
Subject digest:
a684fc0ef30054bc0991d8de765881f65893457f45e3eb23ea8d19d30556ea8a - Sigstore transparency entry: 2506488394
- Sigstore integration time:
-
Permalink:
grahamcracker1234/dedent@a913c7398fbf06e530a7cc30457cca009140d658 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/grahamcracker1234
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@a913c7398fbf06e530a7cc30457cca009140d658 -
Trigger Event:
push
-
Statement type: