PEP: 822
Title: Dedented Multiline String (d-string)
Author: Inada Naoki <songofacandy@gmail.com>
Discussions-To: https://discuss.python.org/t/105519
Status: Draft
Type: Standards Track
Created: 05-Jan-2026
Python-Version: 3.16
Post-History: `05-Jan-2026 <https://discuss.python.org/t/105519>`__,


Abstract
========

This PEP proposes to add a feature that automatically removes indentation from
multiline string literals.

Dedented multiline strings use a new prefix "d" (shorthand for "dedent") before
the opening quote of a multiline string literal.

Example (spaces are visualized as ``.``):

.. code-block:: python

   def hello_paragraph() -> str:
   ....return d"""
   ........<p>
   ..........Hello, World!
   ........</p>
   ...."""

Unlike ``textwrap.dedent()``, indentation before closing quotes is also
considered when determining the amount of indentation to be removed.
Therefore, the string returned in the example above consists of the following
three lines.

* ``"....<p>\n"``
* ``"......Hello, World!\n"``
* ``"....</p>\n"``


Motivation
==========

When writing multiline string literals within deeply indented Python code,
users are faced with the following choices:

* Write the contents of the string without indentation.
* Use multiple single-line string literals concatenated together instead of
  a multiline string literal.
* Use ``textwrap.dedent()`` to remove indentation.

All of these options have drawbacks in terms of code readability and
maintainability.

* Writing multiline strings without indentation in deeply indented code
  looks awkward and tends to be avoided.
  In practice, many places including Python's own test code choose other
  methods.
* Concatenated single-line string literals are more verbose and harder to
  maintain. Writing ``"\n"`` at the end of each line is tedious.
  In argument lists or collection literals, separating commas between
  multi-line string literals are easy to miss.
* ``textwrap.dedent()`` is implemented in Python so it requires some runtime
  overhead. Moreover, it cannot be used to dedent t-strings.

This PEP aims to provide a built-in syntax for dedented multiline strings that
is both easy to read and write, while also being efficient at runtime.


Rationale
=========

The main alternative to this idea is to implement ``textwrap.dedent()`` in C
and provide it as a ``str.dedent()`` method.
This idea reduces the runtime overhead of ``textwrap.dedent()``.
By making it a built-in method, it also allows for compile-time dedentation
when called directly on string literals.

However, this approach has several drawbacks:

* To support cases where users want to include some indentation in the string,
  the ``dedent()`` method would need to accept an argument specifying
  the amount of indentation to remove.
  This would be cumbersome and error-prone for users.
* When continuation lines (lines following a line that ends with a backslash)
  are used, they cannot be dedented.
* f-strings may interpolate expressions as multiline string without indent.
  In such case, f-string + ``str.dedent()`` cannot dedent the whole string.
* t-strings do not create ``str`` objects, so they cannot use the
  ``str.dedent()`` method.
  While adding a ``dedent()`` method to ``string.templatelib.Template`` is an
  option, it would lead to inconsistency since t-strings and f-strings are very
  similar but would have different behaviors regarding dedentation.

The ``str.dedent()`` method can still be useful for non-literal strings,
so this PEP does not reject that idea.
However, for dedenting multiline strings, especially t-strings, using dedicated
syntax is superior.


Specification
=============

Add a new string literal prefix "d" for dedented multiline strings.
This prefix can be combined with "f", "t", "r", and "b" prefixes.
As with existing string prefixes, uppercase and lowercase forms have the same
meaning, and any order is allowed.

This prefix is only for multiline string literals.
It can only be used with triple quotes (``"""`` or ``'''``).

The opening triple quotes must be followed by a newline character.
This newline is not included in the resulting string.
The content of the d-string starts from the next line.

Indentation is leading whitespace characters (spaces and tabs) of each line.

The amount of indentation to be removed is determined by the longest common
indentation of lines in the string.
Lines consisting entirely of whitespace characters are ignored when
determining the common indentation, except for the line containing the closing
triple quotes.

Spaces and tabs are treated as different characters.
For example, ``" hello"`` and ``"\thello"`` have no common indentation.

The dedentation process removes the determined indentation from every line in
the string.

* Lines that are longer than or equal in length to the determined indentation
  must start with the determined indentation.
  Otherwise, Python raises an ``IndentationError``.
  The determined indentation is removed from these lines.
* Lines that are shorter than the determined indentation (including
  empty lines) must be a prefix of the determined indentation.
  Otherwise, Python raises an ``IndentationError``.
  These lines become empty lines.

Unless combined with the "r" prefix, backslash escapes are processed after
the dedentation process.
So you cannot use ``\\t`` in indentations.
And you can use line continuation (backslash at the end of line) and remove
indentation from the continued line.


Examples
--------

.. code-block:: python

   # d-string must start with a newline.
   s = d""           # SyntaxError: d-string must be triple-quoted
   s = d""""""       # SyntaxError: d-string must start with a newline
   s = d"""Hello"""  # SyntaxError: d-string must start with a newline
   s = d"""Hello
   ..World!
   """  # SyntaxError: d-string must start with a newline

   # d-string removes the longest common indentation from each line.
   # Empty lines are ignored, but closing quotes line is always considered.
   s = d"""
   ..Hello
   ..World!
   .."""
   print(repr(s))  # 'Hello\nWorld!\n'

   s = d"""
   ..Hello
   ..World!
   ."""
   print(repr(s))  # '.Hello\n.World!\n'

   s = d"""
   ..Hello
   ..World!
   """
   print(repr(s))  # '..Hello\n..World!\n'

   s = d"""
   ..Hello
   .

   ..World!
   ..."""  # Longest common indentation is '..'.
   print(repr(s))  # 'Hello\n\n\nWorld!\n.'

   # Closing quotes can be on the same line as the last content line.
   # In this case, the string does not end with a newline.
   s = d"""
   ..Hello
   ..World!"""
   print(repr(s))  # 'Hello\nWorld!'

   # Tabs are allowed as indentation.
   # But tabs and spaces are treated as different characters.
   s = d"""
   --->..Hello
   --->..World!
   --->"""
   print(repr(s))  # '..Hello\n..World!\n'

   s = d"""
   --->Hello
   ..World!
   .."""  # There is no common indentation.
   print(repr(s))  # '\tHello\n..World!\n..'

   # Line continuation with backslash works as usual.
   # But you cannot put a backslash right after the opening quotes.
   s = d"""
   ..Hello.\
   ..World!\
   .."""
   print(repr(s))  # 'Hello.World!'

   s = d"""\
   ..Hello
   ..World
   .."""  # SyntaxError: d-string must start with a newline.

   # d-string can be combined with r-string, b-string, f-string, and t-string.
   s = dr"""
   ..Hello\
   ..World!\
   .."""
   print(repr(s))  # 'Hello\\\nWorld!\\\n'

   s = db"""
   ..Hello
   ..World!
   .."""
   print(repr(s))  # b'Hello\nWorld!\n'

   s = df"""
   ....Hello,.{"world".title()}!
   ...."""
   print(repr(s))  # 'Hello,.World!\n'

   s = dt"""
   ....Hello,.{"world".title()}!
   ...."""
   print(type(s))   # <class 'string.templatelib.Template'>
   print(s.strings) # ('Hello,.', '!\n')
   print(s.values)  # ('World',)


How to Teach This
=================

The main difference between ``textwrap.dedent("""...""")`` and d-string can be
explained as follows:

* ``textwrap.dedent()`` is a regular function, but d-string is part of the
  language syntax. d-string has no runtime overhead, and it can remove
  indentation even from t-strings.

* When using ``textwrap.dedent()``, you need to start with ``"""\`` to avoid
  including the first newline character, but with d-string, the string content
  starts from the line after ``d"""``, so no backslash is needed.

  .. code-block:: python

     import textwrap

     s1 = textwrap.dedent("""\
         Hello
         World!
         """)
     s2 = d"""
         Hello
         World!
         """
     assert s1 == s2

* ``textwrap.dedent()`` ignores all blank lines when determining the common
  indentation, but d-string also considers the indentation of the closing
  quotes.
  This allows d-string to preserve some indentation in the result when needed.

  .. code-block:: python

     import textwrap

     s1 = textwrap.dedent("""\
           Hello
           World!
         """)
     s2 = d"""
           Hello
           World!
         """
     assert s1 != s2
     assert s1 == 'Hello\nWorld!\n'
     assert s2 == '  Hello\n  World!\n'

* Since d-string removes indentation before processing escape sequences,
  when using line continuation (backslash at the end of a line), the next line
  can also be dedented.

  .. code-block:: python

     import textwrap

     s1 = textwrap.dedent("""\
         Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
     sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
         Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris \
     nisi ut aliquip ex ea commodo consequat.
         """)
     s2 = d"""
         Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
         sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
         Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris \
         nisi ut aliquip ex ea commodo consequat.
         """
     assert s1 == s2


Other Languages having Similar Features
=======================================

Java 15 introduced a feature called `text blocks <https://openjdk.org/jeps/378>`__.
Since Java had not used triple quotes before, they introduced triple quotes for
multiline string literals with automatic indent removal.

C# 11 also introduced a similar feature called
`raw string literals <https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/raw-string-literal>`__.

`Julia <https://docs.julialang.org/en/v1/manual/strings/#Triple-Quoted-String-Literals>`__ and
`Swift <https://docs.swift.org/swift-book/documentation/the-swift-programming-language/stringsandcharacters/#Multiline-String-Literals>`__
also support triple-quoted string literals that automatically remove indentation.

PHP 7.3 introduced `Flexible Heredoc and Nowdoc Syntaxes <https://wiki.php.net/rfc/flexible_heredoc_nowdoc_syntaxes>`__
Although it uses closing marker (e.g. ``<<<END ... END``) instead of
triple quote, it removes indent from text too.

Ruby also has `"squiggly" heredoc <https://ruby-doc.org/core-2.5.0/doc/syntax/literals_rdoc.html#label-Here+Documents>`__
that removes indent from lines in heredoc.

Perl has `"Indented Here-documents <https://perldoc.perl.org/perl5260delta#Indented-Here-documents>`__
since Perl 5.26 as well.

Java, Julia, and Ruby use the least-indented line to determine the amount of
indentation to be removed.
Swift, C#, PHP, and Perl use the indentation of the closing triple quotes or
closing marker.


Reference Implementation
========================

A CPython implementation of PEP 822 is available at
`methane/cpython#108 <https://github.com/methane/cpython/pull/108>`__.


Rejected Ideas
==============

``str.dedent()`` method
-----------------------

As mentioned in the Rationale section, this PEP doesn't reject the idea of a
``str.dedent()`` method.
A faster version of ``textwrap.dedent()`` implemented in C would be useful for
runtime dedentation.

However, d-string is more suitable for multiline string literals because:

* It works well with f/t-strings.
* It allows specifying the amount of indentation to be removed more easily.
* It can dedent continuation lines.


Triple-backtick
---------------

It is considered that
`using triple backticks <https://discuss.python.org/t/40679>`__
for dedented multiline strings could be an alternative syntax.
This notation is familiar to us from Markdown. While there were past concerns
about certain keyboard layouts,
nowadays many people are accustomed to typing this notation.

However, this notation conflicts when embedding Python code within Markdown or
vice versa.
Therefore, considering these drawbacks, increasing the variety of quote
characters is not seen as a superior idea compared to adding a prefix to
string literals.


``__future__`` import
---------------------

Instead of adding a prefix to string literals, the idea of using a
``__future__`` import to change the default behavior of multiline
string literals was also considered.
This could help simplify Python's grammar in the future.

But rewriting all existing complex codebases to the new notation may not be
straightforward.
Until all multiline strings in that source code are rewritten to
the new notation, automatic dedentation cannot be utilized.

Until all users can rewrite existing codebases to the new notation,
two types of Python syntax will coexist indefinitely.
Therefore, `many people preferred the new string prefix <https://discuss.python.org/t/90988/54>`__
over the ``__future__`` import.


Removing newline in the last line
---------------------------------

Another idea considered was to remove the newline character from the last line.
This idea is similar to Swift's multiline string literals.

With this idea, users can write multiline strings with indentation without
a trailing newline like below:

.. code-block:: python

   s = d"""
       Hello
       World!
     """  # "  Hello\n  World!" (no trailing newline)

   s = d"""
       Hello
       World!

     """  # "  Hello\n  World!\n" (has a trailing newline)

However, including a newline at the end of the last line of a multiline string
literal is a very common case, and requiring an empty line at the end would
look quite unnatural compared to Python's traditional multiline string literals.

When using ``textwrap.dedent("""...""")``, in many cases users had to write a
backslash right after the opening quote, which was frustrating.
Therefore, not including the newline after the opening quote in d-strings is
a clear improvement for users.
On the other hand, removing the newline at the end of the line before the
closing quote would likely cause confusion when rewriting code that uses
``textwrap.dedent("""...""")``.

Without this idea, if you don't need remaining indentation but want to avoid a
trailing newline, you can put the closing quotes on the same line as the last
content line.
And if you need to retain some indentation without a trailing newline,
you can use workarounds such as line continuation or ``str.rstrip()``.

.. code-block:: python

   s = d"""
       Hello
       World!"""
   assert s == "Hello\nWorld!"

   s = d"""
       Hello
       World!\
     """
   assert s == "  Hello\n  World!"

   s = dr"""
       Hello
       World!
     """.rstrip()
   assert s == "  Hello\n  World!"

While these workarounds are not ideal, the drawbacks are considered smaller
than the confusion that would result from automatically removing the trailing
newline.

Since ``textwrap.dedent()`` does not consider the indentation of the closing
quotes, these workarounds are not necessary when rewriting ``textwrap.dedent()``
to d-strings.


Allow content after opening quotes
----------------------------------

There was also a proposal to allow writing content immediately after
the opening quotes, just like existing ``"""``.

Since closing quotes can be on the same line as content, it looks symmetrical
to allow content right after opening quotes too.
And it would make it easier to rewrite existing ``"""`` to d-strings.

On the other hand, it increases the number of syntax rules that readers need to
remember in order to understand how much indentation will be removed.

Also, allowing content after opening quotes would not resolve the usability
issue where existing ``textwrap.dedent("""...""")`` users must write
``"""\<newline>`` to avoid including the first newline.
Julia avoids this issue by removing the first newline when starting with
``"""<newline>``, but this adds another syntax rule that users need to remember.

Since we rejected the idea of using a ``__future__`` import, we prioritized
ease of use for users rewriting from ``textwrap.dedent()`` or those who
wanted to use ``textwrap.dedent()`` but couldn't for some reason, over
the ease of rewriting existing multiline string literal users to d-strings.

Additionally, `in past discussions <https://discuss.python.org/t/90988>`__,
there was a proposal to allow writing hints or comments for syntax highlighting
immediately after the opening quote.
That proposal was rejected to move the d-string discussion forward, but by not
allowing anything right after the opening quote, it leaves room for possibly
allowing comments there in the future.


Copyright
=========

This document is placed in the public domain or under the
CC0-1.0-Universal license, whichever is more permissive.
