A survey of how major programming languages handle multiline string literals, focusing on delimiters, whitespace trimming behavior, and design patterns.
| Language | Syntax | Whitespace Trimming | Trimming Strategy |
|---|---|---|---|
| Python | """...""" |
No (preserve all) | Use textwrap.dedent() manually |
| Python (PEP 822) | d"""...""" |
Yes (automatic) | d prefix for dedented strings; uses the textwrap.dedent() algorithm (Draft PEP) |
| JavaScript | `...` |
No | Preserve all; use tagged template or manual dedent |
| TypeScript | `...` |
No | Same as JavaScript |
| Go | `...` |
No | Raw literal; no interpolation, no trimming (but \r is discarded) |
| Zig | \\... (per line) |
No | Each \\ starts or continues a line; source indentation before the marker is outside the string |
| Rust | "..." or r"..." |
No | Preserve line breaks (CRLF normalized to LF); \ continuations remove following whitespace; use indoc! macro (external crate) |
| Java 15+ | """...""" |
Yes (automatic) | Strips incidental whitespace using the least indentation of content and closing-delimiter lines; also strips trailing whitespace |
| C# 11+ | """...""" |
Yes (automatic) | Closing """ position determines left margin |
| C# < 11 | @"..." |
No | Preserve all; no built-in trimming |
| Kotlin | """...""" |
No (built-in) | .trimIndent() / .trimMargin() stdlib functions |
| Swift 4+ | """...""" |
Yes (automatic) | Closing """ indentation defines left margin; no implicit trailing newline |
| Ruby | <<~ID (squiggly) |
Yes (automatic) | Strips minimum common whitespace from all lines |
| Ruby | <<ID, <<-ID |
No | Preserve all leading whitespace |
| Scala | """...""" |
No (built-in) | .stripMargin method with | margin markers |
| Elixir | """...""" |
Yes (automatic) | Indentation of last """ used to strip leading whitespace from all lines |
| Perl 5.26+ | <<~"ID" |
Yes (automatic) | Removes the exact indentation prefix before the closing delimiter |
| Perl < 5.26 | <<"ID", <<'ID' |
No | Preserve all; closing delimiter must be flush left |
| PHP 7.3+ | <<<ID ... ID; |
Yes (automatic) | Closing marker indentation determines stripping; body must not be less indented than marker, and must use the same whitespace kind |
| PHP < 7.3 | <<<ID ... ID; |
No | Closing marker must be flush left |
| Bash | <<-EOF |
Partial (tabs only) | <<- strips leading tabs, not spaces |
| Dart | '''...''' or """...""" |
No | Preserve all; use .trim() manually |
| GHC 9.12.1+ | """...""" |
Yes (automatic) | MultilineStrings extension; strips the longest whitespace prefix common to all lines except the first |
Haskell without MultilineStrings |
String gaps or QuasiQuoters | No | Preserve all; use string gaps or packages such as neat-interpolation or raw-strings-qq |
Python uses triple-quoted strings ("""...""" or '''...'''). No automatic
whitespace trimming is performed -- the string contains exactly what you type,
including leading indentation.
text = """
line one
line two
"""
# Result: "\n line one\n line two\n"To strip common indentation, use textwrap.dedent() or inspect.cleandoc():
import textwrap
text = textwrap.dedent("""
line one
line two
""")PEP 822 (Draft) proposes a d"""...""" syntax for dedented multiline
strings that would strip indentation at the language level, using the same
least-indented-line algorithm as textwrap.dedent(). This places it in the
Java/Ruby camp rather than the Swift/C# camp, which derive the margin from the
closing delimiter's position.
JavaScript uses backticks (`) for template literals. No automatic
whitespace trimming is performed.
const text = `
line one
line two
`;
// Result: "\n line one\n line two\n"No built-in dedent mechanism exists. Libraries like common-tags provide
tagged template utilities, but there is no standard approach. The standard
String.raw tag is unrelated to indentation -- it only suppresses escape
sequence processing.
Go uses backticks for raw string literals. No interpolation, no escape processing, and no whitespace trimming.
s := `line one
line two`
// Result: "line one\nline two"Backticks cannot contain backtick characters. No interpolation is supported within raw literals.
One exception to the "verbatim" rule: the specification requires carriage
return characters (\r) inside raw string literals to be discarded from the
value, so raw literals in a CRLF-encoded source file still yield LF-only
line breaks.
Zig uses multiline string literals with a \\ marker at the beginning of
each content line. The marker starts or continues the literal; the physical
line ending is omitted, but a newline is inserted when the following line also
begins with \\. Multiline literals do not process escape sequences.
const text =
\\line one
\\line two
;
// Result: "line one\nline two"Indentation before each \\ is source formatting, not string content. Any
whitespace after the marker is preserved, so this syntax avoids incidental
indentation without applying a common-whitespace trimming algorithm.
Rust double-quoted and raw string literals can span physical lines. Line breaks normally represent themselves, though a CRLF pair in the source is normalized to a single LF before the literal is processed. A backslash continuation escape strips the backslash, newline, and all immediately following whitespace (including further newlines). This is not intentional trimming, but a fundamental aspect of Rust's string literal syntax.
let s = "line one\n\
line two";
// Result: "line one\nline two" (the indentation is omitted)This behavior can be surprising for indentation stripping. For explicit indentation stripping, use the indoc! macro from the indoc crate:
use indoc::indoc;
let s = indoc! {"
line one
line two
"};Java introduced text blocks with """ delimiters. The compiler automatically
strips "incidental" whitespace using its content and closing-delimiter lines.
String s = """
line one
line two
""";
// Result: "line one\nline two\n"The algorithm finds the minimum indentation across all non-blank content lines
and the closing-delimiter line, then strips that amount from each line.
Trailing whitespace is also stripped. The closing """ indentation therefore
limits, but does not solely determine, the amount removed.
The trailing newline is not mandatory. It appears above only because the closing
delimiter sits on its own line. Placing the closing """ at the end of the last
content line omits it:
String s = """
line one
line two""";
// Result: "line one\nline two"C# 11 introduced raw string literals using three or more double-quote characters. The closing delimiter's position determines the left margin.
var s = """
line one
line two
""";
// Result: "line one\nline two"Prior to C# 11, verbatim strings (@"...") preserved all whitespace
without trimming.
Kotlin uses triple-quoted strings with no built-in trimming. Two stdlib methods provide trimming:
// trimIndent: removes common leading whitespace
val s = """
line one
line two
""".trimIndent()
// trimMargin: removes up to a margin character (default |)
val s = """
|line one
|line two
""".trimMargin()trimIndent finds the minimum indent across non-blank lines and strips it; it
also drops the first and last lines if they are blank, which is why the examples
above do not begin or end with a newline. Because both are ordinary runtime
functions operating on the interpolated result, an interpolated multi-line value
can defeat the indent calculation. trimMargin strips everything before its
margin character on lines that contain that margin prefix.
Swift uses """ delimiters with automatic indentation stripping based on
the closing delimiter's position.
let s = """
line one
line two
"""
// Result: "line one\nline two"The whitespace before the closing """ defines how much leading whitespace
to strip from all content lines. Both delimiters must be on their own lines, and
a single newline is always stripped after the opening delimiter and before the
closing one -- so unlike Java or Elixir, the value has no trailing newline unless
you add a blank line. A backslash at end of line suppresses the newline
character.
Ruby provides multiple heredoc variants:
# Standard heredoc (no trimming)
s = <<ID
line one
line two
ID
# Squiggly heredoc (auto-trim, Ruby 2.3+)
s = <<~ID
line one
line two
ID
# With explicit trim
s = <<~ID.strip
line one
line two
IDThe <<~ (squiggly) form strips the minimum common leading whitespace from
all lines. Blank lines and lines with only whitespace are ignored when
calculating the minimum indent.
Scala uses triple-quoted strings with no automatic trimming. The
.stripMargin method is the standard approach:
val s = """line one
|line two
|""".stripMargin
// Result: "line one\nline two\n"The | character (or custom delimiter) marks the margin on each line.
Everything before the margin character is stripped.
Elixir uses triple-quoted strings with automatic indentation stripping
based on the closing """ position.
s = """
line one
line two
"""
# Result: "line one\nline two\n"The indentation of the closing """ determines how much leading whitespace
to strip from each line, as in Swift, C#, and PHP. (Java differs: it uses the
least-indented line, of which the closing delimiter's line is only one
candidate.) Elixir heredocs always end with a newline.
Perl uses heredoc syntax with the ~ modifier for indentation stripping
(introduced in Perl 5.26):
# With indentation stripping (5.26+)
my $s = <<~"ID";
line one
line two
ID
# Without indentation stripping (closing delimiter must be flush left)
my $s = <<"ID";
line one
line two
IDThe <<~ form removes the exact whitespace prefix before the closing
delimiter. Every non-empty body line must begin with that prefix, or Perl fails
at compile time. Without ~, body lines may still be indented -- that
whitespace simply becomes part of the string -- but the closing delimiter itself
must start at column 0.
PHP uses heredoc/nowdoc syntax. PHP 7.3 introduced flexible indentation:
// PHP 7.3+ (with indentation stripping)
$s = <<<ID
line one
line two
ID;
// PHP < 7.3 (no indentation stripping, must be flush left)
$s = <<<ID
line one
line two
ID;Since PHP 7.3, the closing marker's indentation determines how much
leading whitespace to strip. The body lines must not be less indented
than the closing marker, or PHP raises a ParseError. Tabs and spaces are both
accepted but must not be intermixed between the closing marker and the body's
indentation prefix; doing so is also a ParseError.
Bash uses heredoc syntax with the - modifier for tab stripping:
# No stripping (content indentation is preserved)
cat <<EOF
line one
line two
EOF
# Tab stripping only (not spaces)
cat <<-EOF
line one
line two
EOFThe <<- form strips leading tabs only (not spaces). This is more
limited than other languages.
Dart uses triple-quoted strings ('''...''' or """...""") with no
automatic whitespace trimming.
var s = """
line one
line two
""";
// Result: " line one\n line two\n "No built-in dedent. Use .trim() for leading/trailing whitespace removal.
GHC gained multiline strings in version 9.12.1 via the
MultilineStrings extension, with automatic indentation stripping:
{-# LANGUAGE MultilineStrings #-}
s = """
line one
line two
"""
-- Result: "line one\nline two\n"The stripped indentation is the longest whitespace prefix shared by all lines
except the first, ignoring whitespace-only lines -- a least-indented-line rule,
not a closing-delimiter rule. The newline immediately after the opening delimiter
is dropped, but the final newline is kept; use a string gap (\) at the end of
the last content line to omit it. Use the \& escape to protect indentation that
should survive stripping.
Without MultilineStrings, ordinary string literals can use string gaps, and
packages can provide QuasiQuoters such as raw-strings-qq or
neat-interpolation.
Languages use four main strategies for handling whitespace in multiline strings:
Languages: Python, JavaScript, TypeScript, Go, Dart, C# < 11,
Scala, Ruby (<</<<-), Perl (<<), PHP < 7.3, Kotlin (built-in)
The string contains exactly what you type. Developer must handle indentation manually via library functions or string methods.
Pros: Simple semantics; what you see is what you get. Cons: Code indentation pollutes string content; common to need external dedent utilities.
Languages: Swift, C# 11+, Elixir, PHP 7.3+, Perl (<<~)
The closing delimiter alone defines the left margin. In C#, PHP, and Perl a content line indented less than the delimiter is an error; Swift warns.
// C# example
var s = """
line one
line two
""";
// ^-- closing """ at column 4
// Result: "line one\nline two"Pros: Elegant; code indentation and string content are independent. Cons: Confusing at first; closing delimiter position determines behavior; easy to miscount indentation.
Languages: Java 15+, Ruby (<<~), Kotlin (trimIndent), GHC
MultilineStrings, Python PEP 822 (Draft)
The algorithm scans all non-blank lines, finds the one with minimum leading whitespace, and strips that amount from all lines. Java is a hybrid: the closing-delimiter line participates in the calculation as one more candidate, so moving that delimiter left adds indentation back to every line.
# Ruby example
s = <<~ID
line one
line two
ID
# Minimum indent is 4 spaces (from "line two")
# Result: " line one\nline two\n"Pros: More flexible; doesn't depend on closing delimiter position. Cons: Blank lines can complicate the calculation; slightly more complex to reason about.
Languages: Scala (stripMargin), Kotlin (trimMargin), GHC
MultilineStrings (\&), Zig (\\)
Developer places a marker character (typically |) on each line to
indicate where the margin is. Everything before the marker is stripped. Zig
uses \\ as a required line prefix; indentation before the prefix is outside
the literal, and consecutive prefixed lines insert a newline.
val s = """|line one
|line two
|""".stripMarginPros: Explicit control over each line; unambiguous. Cons: Requires marker characters on every line; more verbose.
-
Newer languages automate trimming. Java (15), Swift (4), C# (11), Elixir, and GHC's
MultilineStringsextension (9.12.1) make indentation stripping the default behavior. Python's PEP 822 (Draft) is following this trend. -
Two algorithms dominate, roughly evenly. Swift, C#, Elixir, PHP, and Perl derive the margin from the closing delimiter; Java, Ruby, Kotlin, GHC, and the PEP 822 draft use the least-indented line. Both decouple code indentation from string content without requiring explicit markers.
-
Heredoc syntax (
<<MARKER) remains popular in scripting languages (Ruby, PHP, Bash, Perl). The indentation modifier is not standardized across them: Ruby and Perl spell it<<~, Bash uses<<-for tabs only, and PHP infers it from the closing marker with no sigil at all. -
Most languages still provide escape hatches for cases where automatic trimming is unwanted -- Java allows the closing
"""at column 0, Ruby has<</<<-variants, and Perl has ordinary<<EOF,<<'EOF',<<"EOF", and<<\EOFvariants. -
Trailing whitespace handling varies. Java strips trailing whitespace on each line; most others preserve it. Whether the value ends in a newline also differs: Elixir always adds one, Java and C# depend on where the closing delimiter sits, and Swift never adds one. These are subtle differences that can cause bugs when porting between languages.
- Python PEP 822: Dedented Multiline String (d-string)
- Java JEP 378: Text Blocks
- C# 11 Raw String Literals
- Kotlin stdlib:
trimIndent/trimMargin - Swift SE-0168: Multi-Line String Literals
- Ruby Squiggly Heredocs (Ruby 2.3)
- PHP RFC: Flexible Heredoc and Nowdoc Syntaxes
- GHC MultilineStrings extension (GHC 9.12.1)
- Go String Literals specification
- Zig Language Reference: Multiline String Literals
- Rust Reference: Literal expressions (string continuation escapes)
- Elixir Syntax Reference: Heredocs
- Perl 5.26 indented heredocs (
<<~) - GNU Bash Reference Manual: Here Documents