It's like textwrap.dedent
, but with it works better.
This only supports Python 3.14 (which is not yet released) because it relies on t-strings.
Have you ever used textwrap.dedent
with an f-string that has newline characters in a replacement field?
For example, given this code
string (which has newlines
in it):
code = r"""
def strip_each(lines):
new_lines = []
for line in lines:
new_lines.append(line.rstrip("\n"))
return new_lines
""".strip("\n")
Using textwrap.dedent
with an f-string that uses code
in a replacement field results in very strange indentation:
>>> print(dedent(f"""\
... Example function:
... {code}
...
... That function was NOT indented properly!"""))
Example function:
def strip_each(lines):
new_lines = []
for line in lines:
new_lines.append(line.rstrip("\n"))
return new_lines
The problem is that f-strings immediately interpolate their replacement fields.
That code
string is injected into the new string before dedent
has a chance to even look at the string.
By the time textwrap.dedent
does its dedenting, the weirdness has already happened.
Passing a t-string to the better_dedent.dedent
function allows the replacement fields to maintain their original indentation level.
Using the same code
string as before:
code = r"""
def strip_each(lines):
new_lines = []
for line in lines:
new_lines.append(line.rstrip("\n"))
return new_lines
""".strip("\n")
The better_dedent.dedent
function will dedent the t-string and then inject the replacement field, resulting in much more sensible indentation:
>>> print(dedent(t"""\
... Example function:
... {code}
...
... That function was indented properly!""")
...
Example function:
def strip_each(lines):
new_lines = []
for line in lines:
new_lines.append(line.rstrip("\n"))
return new_lines
That function was indented properly!
Using a t-string allows for dedenting the whole string before the replacement fields are inserted and then inserting the replacement fields.
Note that if an f-string is passed to better_dedent.dedent
, it will simply delegate to textwrap.dedent
.
This package also includes an undent
function, which will strip a leading newline (note the lack of \
after t"""
):
>>> print(undent(t"""
... Example function:
... {code}
... That function was indented properly!"""))
Example function:
def strip_each(lines):
new_lines = []
for line in lines:
new_lines.append(line.rstrip("\n"))
return new_lines
That function was indented properly!
By default, the undent
function will also strip a trailing newline:
>>> print(undent(t"""
... Example function:
... {code}
... That function was indented properly!
... """))
Example function:
def strip_each(lines):
new_lines = []
for line in lines:
new_lines.append(line.rstrip("\n"))
return new_lines
That function was indented properly!
>>> print("Note that there's no blank line above this prompt")
Note that there's no blank line above this prompt
Passing strip_trailing=False
to undent
will suppress trailing newline removal.
You install better-dedent
with pip
(you'll need to be on Python 3.14):
pip install better-dedent
Or if you have uv installed and you'd like to play with it right now (Python 3.14 will be auto-installed):
uvx --with better-dedent python
You can then import dedent
and undent
like this:
from better_dedent import dedent, undent
And try them out:
code = r"""
def strip_each(lines):
new_lines = []
for line in lines:
new_lines.append(line.rstrip("\n"))
return new_lines
""".strip("\n")
text = undent(t"""
Here is some example code:
{code}
That indentation worked out nicely!
""")
print(text)
This project uses hatch.
To run the tests:
hatch test
To see code coverage:
hatch test --cover
hatch run cov-html
open htmlcov/index.html
better-dedent
is distributed under the terms of the MIT license.