diff --git a/peps/pep-0822.rst b/peps/pep-0822.rst index 8315260ae48..73f7c129ed1 100644 --- a/peps/pep-0822.rst +++ b/peps/pep-0822.rst @@ -9,6 +9,12 @@ Python-Version: 3.16 Post-History: `05-Jan-2026 `__, +.. toctree:: + :hidden: + + pep-0822/dstring-tutorial + + Abstract ======== @@ -16,7 +22,7 @@ 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. +the opening quote of a triple-quoted string literal. Example (spaces are visualized as ``.``): @@ -48,10 +54,9 @@ 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. +* Use :func:`textwrap.dedent` to remove indentation. -All of these options have drawbacks in terms of code readability and -maintainability. +These options have various drawbacks. * Writing multiline strings without indentation in deeply indented code looks awkward and tends to be avoided. @@ -59,10 +64,10 @@ maintainability. 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. + In argument lists or collection literals, commas separating multiline + string literals are easy to miss. +* :func:`textwrap.dedent` requires a runtime call and cannot be applied + directly to 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. @@ -71,9 +76,9 @@ 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()``. +The main alternative to this idea is to implement :func:`textwrap.dedent` in +C and provide it as a ``str.dedent()`` method. +This idea reduces the runtime overhead of :func:`textwrap.dedent`. By making it a built-in method, it also allows for compile-time dedentation when called directly on string literals. @@ -85,13 +90,17 @@ However, this approach has several drawbacks: 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. +* f-strings may interpolate expressions as multiline strings without + indentation. In such cases, f-strings + ``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. +* When you want to write a multiline string containing ``\``, + you may want to use a raw string, but in that case you cannot start with + ``dedent(r"""\`` and avoid having a blank line at the beginning. The ``str.dedent()`` method can still be useful for non-literal strings, so this PEP does not reject that idea. @@ -103,50 +112,82 @@ Specification ============= Add a new string literal prefix "d" for dedented multiline strings. -This prefix can be combined with "f", "t", "r", and "b" prefixes. +This prefix can be combined with existing compatible string prefixes. +The valid d-string prefix families are ``d``, ``dr``, ``db``, ``drb``, +``df``, ``drf``, ``dt``, and ``drt``. As with existing string prefixes, uppercase and lowercase forms have the same -meaning, and any order is allowed. +meaning, and any order is allowed within each valid family. + +The ``u`` prefix is not compatible with ``d``. +Existing incompatibilities also apply: ``b`` cannot be combined with ``f`` or +``t``, and ``f`` cannot be combined with ``t``. 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. +As elsewhere in Python source code, physical source lines are normalized before +this rule is applied, so ``\r\n`` and ``\r`` line endings are treated as +newlines. 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 dedentation process consists of blank-line normalization and indentation +removal. The rules are as follows: -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. +* A line consisting only of tabs, spaces, and a newline is a *blank line*. +* The longest prefix consisting only of spaces and tabs that is common to + all non-blank lines and to the line containing the closing triple quotes is + the *longest common indentation*. + Since the line containing the closing triple quotes is always included, + there is always at least one line participating in this calculation. +* The longest common indentation is removed from each non-blank line. +* Each blank line is normalized to a single newline character. + If the content part of the line containing the closing triple quotes is + blank at this stage, that line is also normalized to the empty string. 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. +These steps are applied to the physical lines of the source code, before +escape sequences are processed. +Therefore, escape sequences such as ``\x20`` and line-continuation +backslashes are not treated as whitespace, and you cannot use ``\t`` in +indentations. -* 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. +For ``df`` and ``dt`` strings, physical lines of the literal include lines +inside replacement fields (``{...}``), including lines inside nested string +literals and multiline format specs. +Therefore, such lines also affect the longest common indentation of the outer +literal. -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. +Although lines inside replacement fields participate in calculating the +longest common indentation, the outer d-string does not remove indentation +from the contents of those replacement fields. Dedentation is applied only to +the constant text parts of the outer ``df`` or ``dt`` string. In particular, +the following retain their source indentation: + +* The raw source text of ``df`` debug expressions (``{expr=}``). +* The captured expression text of ``dt`` interpolations. +* Multiline string literals in replacement expressions. +* Multiline format specs. + +This distinction lets replacement-field lines constrain how much indentation +is removed from the surrounding constant text without rewriting Python source +or format-spec text inside the field. + +If a replacement field expression itself contains another d-string literal, +the nested d-string is supported and is dedented independently according to +its own physical lines. Those same physical lines also participate in the +outer d-string's longest-common-indentation calculation. The outer d-string +does not otherwise dedent the nested literal's contents. Examples -------- +In the following examples, spaces are visualized as ``.`` and tabs as ``--->``. + .. code-block:: python # d-string must start with a newline. @@ -158,19 +199,15 @@ Examples """ # 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. + # Blank lines are normalized to empty lines and ignored when determining + # the common indentation, while the closing quotes line is always + # considered. If that line is blank after dedentation, it is normalized too. 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! @@ -182,15 +219,17 @@ Examples . ..World! - ...""" # Longest common indentation is '..'. - print(repr(s)) # 'Hello\n\n\nWorld!\n.' + ...""" # Last line is normalized to an empty line. + 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. + # Spaces before the closing quotes are not normalized when it is not a + # blank line. s = d""" ..Hello - ..World!""" - print(repr(s)) # 'Hello\nWorld!' + ..World!.""" + print(repr(s)) # 'Hello\nWorld!.' # Tabs are allowed as indentation. # But tabs and spaces are treated as different characters. @@ -204,7 +243,16 @@ Examples --->Hello ..World! ..""" # There is no common indentation. - print(repr(s)) # '\tHello\n..World!\n..' + print(repr(s)) # '\tHello\n..World!\n' + + # A blank line that does not match the common indentation is not an + # error; it is just normalized to an empty line. + s = d""" + ..Hello + ---> + ..World! + ..""" + print(repr(s)) # 'Hello\n\nWorld!\n' # Line continuation with backslash works as usual. # But you cannot put a backslash right after the opening quotes. @@ -219,8 +267,9 @@ Examples ..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""" + # d-string can be combined with compatible r-string, b-string, + # f-string, and t-string prefixes, in any order. + s = rd""" ..Hello\ ..World!\ ..""" @@ -244,85 +293,263 @@ Examples print(s.strings) # ('Hello,.', '!\n') print(s.values) # ('World',) + # In df/dt strings, lines inside replacement fields participate in + # the outer longest common indentation calculation. + s = df""" + ..Hello + ....{42} + ..""" + print(repr(s)) # 'Hello\n..42\n' + + s = df""" + ..Hello + .{42} + ..""" + print(repr(s)) # '.Hello\n42\n' + + s = df""" + ....Hello {1 + + ...2} + ....""" + print(repr(s)) # '.Hello 3\n' + + # The source text of a multiline debug expression is not dedented, + # although its physical lines affect the outer common indentation. + s = df""" + ....{1 + + ..1=} + ....""" + print(repr(s)) # '..1 +\n..1=2\n' + + # A nested d-string in a replacement field is dedented independently. + s = df""" + ..outer + ....{d""" + ....nested + ....line + ...."""} + ..""" + print(repr(s)) # 'outer\n..nested\nline\n\n' + + # Lines in a nested d-string also constrain the outer indentation. + # Here, the unindented "bar" and "baz" lines make the outer common + # indentation empty, while the nested d-string is evaluated independently. + s = df""" + ....foo {d""" + bar + baz + ........"""} spam + ....""" + print(repr(s)) # '....foo bar\nbaz\n spam\n' + 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 -======================================= +This section explains the differences between :func:`textwrap.dedent` and +d-strings for users who are already using ``textwrap.dedent()``. + +.. note:: + + An explanation of d-strings for beginners can be found in + :ref:`pep822-dstring-tutorial`. + +The d-string is syntax for removing indentation from multiline literals. +It can often replace ``textwrap.dedent("""...""")``, but its status as literal +syntax leads to several important differences. + +Shared behavior +--------------- + +Both remove the longest leading indentation shared by all non-blank lines. +They also normalize a blank line, consisting only of spaces, tabs, and a +newline, to an empty line. Blank lines do not participate in calculating the +common indentation. + +.. code-block:: python + + import textwrap + + from_dedent = textwrap.dedent("""\ + first + + second + """) + from_dstring = d""" + first + + second + """ + + assert from_dedent == from_dstring == "first\n\nsecond\n" + +The first newline +----------------- + +In an ordinary triple-quoted string, the newline immediately following the +opening quotes is part of the string. Therefore, calls to ``dedent()`` often +start with ``"""\`` to remove that newline. A d-string requires a newline +immediately after its opening quotes, but does not include it in the string. +There is no need to use a line-continuation backslash for this purpose. + +.. code-block:: python + + import textwrap + + from_dedent = textwrap.dedent("""\ + Hello + World! + """) + from_dstring = d""" + Hello + World! + """ + + assert from_dedent == from_dstring == "Hello\nWorld!\n" + +Since d-string does not need a backslash to prevent a newline at the beginning +of the string, it is easier to combine with raw strings. + +.. code-block:: python + + from_dedent = textwrap.dedent(r""" + s = "Hello\nWorld!" + """.lstrip()) + from_dstring = dr""" + s = "Hello\nWorld!" + """ + + assert from_dedent == from_dstring == 's = "Hello\\nWorld!"\n' + +The closing-quote line +---------------------- + +With ``dedent()``, a line containing only the closing quotes is a blank line +in the input string, so it is excluded from the common-indentation calculation. + +On the other hand, a d-string always uses the closing-quote line to choose how +much indentation remains in the result. + +.. code-block:: python + + import textwrap + + from_dedent = textwrap.dedent("""\ + item + child + """) + from_dstring = d""" + item + child + """ + + assert from_dedent == "item\n child\n" + assert from_dstring == " item\n child\n" + +In this example, the four spaces on the d-string closing-quote line are part +of the common indentation. Only four of the six spaces on the body lines are +removed, allowing the multiline string to retain some indentation. + +``f``-string interpolation +-------------------------- + +A ``df``-string removes indentation from the literal's physical lines before +it evaluates replacement expressions. Newlines and following whitespace in an +interpolated value therefore do not change the d-string's common indentation. + +.. code-block:: python + + value = "first line\nsecond line" + + message = textwrap.dedent(f"""\ + Result: + {value} + """) + + assert message == ( + " Result:\n" + " first line\n" + "second line\n" + ) + + message = df""" + Result: + {value} + """ + + assert message == ( + "Result:\n" + " first line\n" + "second line\n" + ) + + +``t``-strings and ``b``-strings +-------------------------------- + +``dedent()`` accepts a ``str``, so it cannot be applied directly to a t-string +template value. D-string prefixes can be combined, making ``dt``-strings +available for template strings as well. + +.. code-block:: python + + template = dt""" + Hello, {name}! + """ + +Likewise, a ``db``-string can dedent a multiline bytes literal. This is useful +when writing indented test expectations or other byte-oriented text alongside +the surrounding Python code. + +.. code-block:: python + + expected = db""" + HTTP/1.1 200 OK + Content-Type: text/plain + + ok + """.replace(b'\n', b'\r\n') + + assert expected == ( + b"HTTP/1.1 200 OK\r\n" + b"Content-Type: text/plain\r\n" + b"\r\n" + b"ok\r\n" + ) + +Line continuation +----------------- + +A d-string removes indentation from physical lines before it processes escape +sequences. A line continued with a trailing ``\\`` can therefore also be +indented naturally. + +.. code-block:: python + + import textwrap + + # ``dedent()`` requires the continued line to begin at the left margin. + from_dedent = textwrap.dedent("""\ + first part \ + second part + """) + + # A d-string can align both physical lines at the same indentation depth. + from_dstring = d""" + first part \ + second part + """ + + assert from_dedent == from_dstring == "first part second part\n" + + + +Other Languages with Similar Features +====================================== Java 15 introduced a feature called `text blocks `__. -Since Java had not used triple quotes before, they introduced triple quotes for -multiline string literals with automatic indent removal. +Since Java had not used triple quotes before, it introduced triple quotes for +multiline string literals with automatic indentation removal. C# 11 also introduced a similar feature called `raw string literals `__. @@ -331,9 +558,9 @@ C# 11 also introduced a similar feature called `Swift `__ also support triple-quoted string literals that automatically remove indentation. -PHP 7.3 introduced `Flexible Heredoc and Nowdoc Syntaxes `__ -Although it uses closing marker (e.g. ``<<`__. +Although it uses a closing marker (e.g. ``<<`__ that removes indent from lines in heredoc. @@ -375,10 +602,9 @@ However, d-string is more suitable for multiline string literals because: Triple-backtick --------------- -It is considered that -`using triple backticks `__ -for dedented multiline strings could be an alternative syntax. -This notation is familiar to us from Markdown. While there were past concerns +`Using triple backticks `__ +for dedented multiline strings was also considered as an alternative syntax. +This notation is familiar from Markdown. While there were past concerns about certain keyboard layouts, nowadays many people are accustomed to typing this notation. @@ -415,7 +641,7 @@ 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: +a trailing newline, as shown below: .. code-block:: python @@ -442,9 +668,9 @@ 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. +Without this idea, if you do not need to preserve 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()``. @@ -489,16 +715,16 @@ 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 +Also, allowing content after the opening quotes would not resolve the usability issue where existing ``textwrap.dedent("""...""")`` users must write ``"""\`` to avoid including the first newline. Julia avoids this issue by removing the first newline when starting with ``"""``, 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. +ease of use for users rewriting from ``textwrap.dedent()`` or for users who +wanted to use ``textwrap.dedent()`` but could not for some reason, over +ease of rewriting existing multiline string literals to d-strings. Additionally, `in past discussions `__, there was a proposal to allow writing hints or comments for syntax highlighting @@ -508,6 +734,35 @@ allowing anything right after the opening quote, it leaves room for possibly allowing comments there in the future. +Do not normalize blank lines +---------------------------- + +An earlier draft of this PEP specified the following behavior for blank +lines instead of normalizing them to empty lines: + +* If a blank line was a prefix of the longest common indentation, it was + normalized to an empty line. +* If it started with the longest common indentation and was longer than it, + the longest common indentation was stripped from it, just like from any + other line. +* Otherwise---for example, when the longest common indentation was 8 spaces + and the blank line was ``"\t\n"``---an ``IndentationError`` was raised. + +This specification intentionally changed the handling of blank lines compared +to ``textwrap.dedent()`` to limit the role of d-strings to dedentation. + +However, for ease of use, blank lines that do not contain the longest common +indentation need to be allowed, and errors for whitespace characters that +conflict with the longest common indentation also need to be considered. +It makes the overall specification more complex. + +To make this easier to explain, this PEP normalizes blank lines to empty +lines, just like ``textwrap.dedent()`` does. +It makes the dedent rules simpler, and the difference from +``textwrap.dedent()`` becomes smaller, making it easier to explain the d-string +specification to users. + + Copyright ========= diff --git a/peps/pep-0822/dstring-tutorial.rst b/peps/pep-0822/dstring-tutorial.rst new file mode 100644 index 00000000000..0e0ccc23a06 --- /dev/null +++ b/peps/pep-0822/dstring-tutorial.rst @@ -0,0 +1,284 @@ +.. _pep822-dstring-tutorial: + +A Gentle Introduction to d-strings +================================== + +This document introduces d-strings for readers who already know ordinary +triple-quoted strings, like this: + +.. code-block:: python + + message = """Hello + World! + """ + +A triple-quoted string is useful when you want to write text across several +lines. The difficulty appears when that text lives inside indented Python code. + +.. code-block:: python + + def make_message(): + return """Hello + World! + """ + +This produces the text you probably want, but the Python code looks awkward: +the string body is pushed all the way to the left even though it belongs inside +the function. + +d-strings are designed for this situation. + +The Basic Idea +-------------- + +A d-string is a triple-quoted string with a ``d`` prefix: + +.. code-block:: python + + def make_message(): + return d""" + Hello + World! + """ + +The ``d`` stands for "dedent". Python removes the shared indentation from the +lines in the string, so the result is: + +.. code-block:: python + + "Hello\nWorld!\n" + +This lets the text line up naturally with the surrounding Python code without +putting those Python indentation spaces into the final string. + +The Opening Line +---------------- + +With an ordinary ``"""`` string, you can put text immediately after the opening +quotes: + +.. code-block:: python + + s = """Hello + World! + """ + +A d-string is different. The opening quotes must be followed immediately by a +newline: + +.. code-block:: python + + s = d""" + Hello + World! + """ + +The newline right after ``d"""`` is only there to start the block. Unlike an +ordinary triple-quoted string, that first newline is not included in the final +string. The string content starts on the next line. + +No Trailing Newline +------------------- + +Aside from dedenting, d-strings follow the same trailing-newline behavior as +ordinary triple-quoted strings. + +If you want the final string to end without a trailing newline, put the +closing quotes immediately after the last content line: + +.. code-block:: python + + def label(): + return d""" + Ready""" + +Result: + +.. code-block:: python + + "Ready" + +If the closing quotes are on their own line, the final newline is part of the +string: + +.. code-block:: python + + def label_with_newline(): + return d""" + Ready + """ + +.. code-block:: python + + "Ready\n" + +How Indentation Is Removed +-------------------------- + +Think of a d-string as looking at the actual lines in your source file. +It finds the indentation that all non-blank lines have in common, +then removes that shared indentation. + +.. code-block:: python + + def page_title(): + return d""" +

+ Welcome +

+ """ + +.. code-block:: python + + "

\n Welcome\n

\n" + +The two extra spaces before ``Welcome`` remain because they are part of the +document, not just part of the Python indentation. + +Keeping Some Indentation +^^^^^^^^^^^^^^^^^^^^^^^^ + +The closing quote line is important. Its indentation is included when +Python decides how much indentation to remove. + +You can use this behavior to preserve indentation by placing the closing quotes +farther to the left than the body. + +.. code-block:: python + + def indented_list(): + return d""" + items: + - apples + - bananas + """ + +Here the body lines are indented more than the closing quotes. The indentation +shared with the closing quote line is removed, and the extra indentation is +preserved: + +.. code-block:: python + + " items:\n - apples\n - bananas\n" + +This is useful when the text format itself needs indentation, such as +HTML, YAML-like examples, or generated code. + +Blank Lines Become Empty Lines +------------------------------ + +A blank line is a line that contains only spaces, tabs, and a newline. In a +d-string, blank lines are normalized to empty lines. + +.. code-block:: python + + def paragraph(): + return d""" + First paragraph. + + Second paragraph. + """ + +Even if the blank-looking line contains spaces, the result treats it as a plain +empty line: + +.. code-block:: python + + "First paragraph.\n\nSecond paragraph.\n" + +Blank lines do not decide how much indentation gets removed. They are simply +turned into empty lines. + +Although closing quotes are considered when deciding how much indentation to +remove, the line with the closing quotes can also be normalized to an empty +string if it only has spaces before the quotes. + +Escapes Are Handled After Dedent +-------------------------------- + +d-strings remove indentation before escape sequences are processed. + +That means dedent works on the physical lines as they appear in your source +file. For example, ``\t`` in the indentation area is not treated as a tab for +dedent purposes; it is just a backslash followed by ``t`` until escapes are +processed later. + +.. code-block:: python + + s = d""" + \tName + \tAge + """ + +The four real spaces before each line are indentation and can be removed. The +``\t`` text remains in the string until escape processing turns it into tab +characters. + +This rule matters most when you use a backslash at the end of a line. + +Indenting Continued Lines +------------------------- + +In Python strings, a backslash at the end of a line can continue the string +onto the next line: + +.. code-block:: python + + def long_sentence(): + return """\ + This is a long sentence that continues \ + on the next physical line. + """ + +D-string removes indentation before Python processes the line-continuation. +So you can indent continued lines in d-strings. + +.. code-block:: python + + def long_sentence(): + return d""" + This is a long sentence that continues \ + on the next physical line. + """ + +There is one special rule to remember: you cannot put that continuation +backslash immediately after the opening quotes. A d-string must start with a +real newline after ``d"""``. + +Combining with f/t-strings +-------------------------- + +d-strings can be combined with f-strings and t-strings. In those combinations, +dedent still happens first, and then f/t-string processing happens as usual. + +.. code-block:: python + + def build_validation_message(user, missing_fields): + return df""" + Validation failed for {user}. + Missing required fields: {", ".join(missing_fields)}. + Please update the input and try again. + """ + +.. code-block:: python + + >>> build_validation_message("Alice", ["email", "role"]) + "Validation failed for Alice.\nMissing required fields: email, role.\nPlease update the input and try again.\n" + + +What To Remember +---------------- + +- A d-string is written with ``d"""`` or ``d'''``. +- The opening quotes must be followed by a newline, and that newline is not + included in the string. +- Blank lines are normalized to empty lines. +- The closing quote line, even if it is blank, helps decide how much + indentation is removed. +- Put the closing quotes right after the last content line when you do not want + a trailing newline. +- Moving the closing quotes lets you preserve some indentation in the final + text. +- Dedent works on the physical source lines before escape sequences are + processed. +- Continued lines ending in ``\`` can be indented naturally inside a d-string.