Strings & Interpolation
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10 (3.10.0, 2026-10-12) and every example was run with Scala 3.10.0 through Scala CLI
on JDK 26 on 2026-10-15. A Scala String is simply a java.lang.String: there is no wrapper class, so everything the JDK
offers is available and strings interoperate with Java without any conversion. What Scala adds is a layer of
collection-style methods (via StringOps), triple-quoted multiline literals, and string interpolation, a compiler
feature that rewrites s"Hello $name" into a call on a StringContext. Anything newer than the 3.3 LTS is labelled with
its release: the $" escape is Scala 3, the \s escape is new in 3.10 (not available on 3.9 LTS), and dedented string
literals are experimental in 3.10. Numbers, characters and the other literal forms are covered in
Values, Variables & Literals; this page is about text.
The Java and Kotlin counterparts are on Java: Strings & Text and
Kotlin: Strings & Text.
String and StringOps
String is final and immutable, exactly as in Java. The methods of java.lang.String (length, charAt, indexOf,
substring, toUpperCase, split, replace, matches…) work unchanged. When a method is not found on String, the
compiler looks for an implicit conversion and finds Predef.augmentString, which wraps the string in
scala.collection.StringOps. StringOps is a value class, so the extra methods usually cost no allocation. It supplies
the whole sequence API (take, drop, filter, map, reverse, foldLeft, exists, zip, sorted…) treating the
string as a sequence of Char, plus string-specific helpers (capitalize, stripPrefix, stripSuffix, stripMargin,
toIntOption, toDoubleOption, *, linesIterator, format…). When a method needs a real collection, the string is
wrapped in a WrappedString instead (Predef.wrapString).
//> using scala 3.10.0
@main def stringOps(): Unit =
val name = "Pico del Aguila"
// java.lang.String members
assert(name.length == 15)
assert(name.charAt(0) == 'P' && name(1) == 'i') // apply comes from StringOps
assert(name.indexOf("Aguila") == 9)
assert(name.substring(5, 8) == "del")
assert(name.toUpperCase == "PICO DEL AGUILA")
// StringOps (collection-style) members
println(name.take(4)) // first four characters
println(name.drop(5).reverse) // drop, then reverse
println(name.filter(_.isUpper)) // keep upper-case characters
println(name.map(_.toUpper)) // String => String
println(name.map(_.toInt).take(3)) // Char => Int gives a sequence of Int, not a String
println("ab" * 3) // repetition
println("trail".capitalize)
println("--Pico--".stripPrefix("--").stripSuffix("--"))
println("12".toInt + "3.5".toDouble)
println("abc".toIntOption) // no exception
println("a\nb\nc".linesIterator.toList) // not .lines, see below
println("a\nb".lines.getClass.getName) // java.lang.String.lines (JDK 11+)
println("a|b".split("|").toList) // split takes a regex
println("a|b".split("\\|").toList)
println(("😀".length, "😀".codePointCount(0, 2)))
Pico
aliugA led
PA
PICO DEL AGUILA
ArraySeq(80, 105, 99)
ababab
Trail
Pico
15.5
None
List(a, b, c)
java.util.stream.ReferencePipeline$Head
List(a, |, b)
List(a, b)
(2,1)
Source: Scala Collections Guide: Strings
Several details in the output matter in practice:
-
mapandfilterreturn aStringonly when the function keeps the element type (Char => Char);_.toIntreturns anArraySeq[Int]because aStringcannot hold integers. -
name(1)isname.apply(1)fromStringOps, so indexing uses parentheses, not brackets. -
linesresolves to the JDK methodString.lines()(JDK 11+), which returns ajava.util.stream.Stream, not a Scala collection. Scala’s own method islinesIterator; use it, andlinesWithSeparatorswhen you need to keep the\n. -
splitinterprets its argument as a regular expression, so"a|b".split("|")splits between every character, as in Java. Escape the metacharacter ("\\|") or use a triple-quoted string ("""\|"""). -
A
Charis one UTF-16 unit. The emoji above haslength2 but is one code point; usecodePointsorcodePointCountwhen text may contain supplementary characters. -
==on strings is value equality (it callsequals), unlike Java where==is reference equality. Reference identity iseq. -
Strings are immutable: building a string in a loop with
+copies each time. UseStringBuilder(scala.collection.mutable.StringBuilder, orjava.lang.StringBuilder) ormkStringon a collection.
Concatenation with + converts the right-hand operand with toString, so "Hello" + 1 + 2 is Hello12, but
1 + 2 + "Hello" adds the numbers first. Adding a number on the left of a string with Int + String is deprecated since 2.13.0,
and the compiler says so when -deprecation is on:
[warn] ./a.scala:35:11
[warn] method + in class Int is deprecated since 2.13.0: Adding a number and a String is deprecated. Use the string interpolation `s"$num$str"`
[warn] println(1 + 2 + "Hello")
[warn] ^^^^^^^
Multiline strings and stripMargin
A literal delimited by three double quotes is a multiline string. It may span lines, may contain " characters without
escaping, and does no escape processing: \n stays as two characters. Indentation inside the literal is kept, so the
usual idiom is to start each continuation line with | and call stripMargin, which removes the whitespace and the first
| of each line. stripMargin(c) uses another margin character.
//> using scala 3.10.0
import java.util.Locale
@main def multiline(): Unit =
val name = "Pico"
val card = """Trail: Pico
|Length: 12.5 km
| (indented line keeps its two spaces)""".stripMargin
println(card)
val quoted = s"""He said "go to $name" and \n stayed"""
println(quoted)
println("""a\nb""") // no escape processing in triple quotes
val custom = """
#first
#second""".stripMargin('#')
println(custom.linesIterator.toList)
Trail: Pico
Length: 12.5 km
(indented line keeps its two spaces)
He said "go to Pico" and
stayed
a\nb
List(, first, second)
Note two things. First, a s"""...""" literal does process escapes, because the s interpolator does (next section), so
\n became a real newline in the second example while the plain """a\nb""" kept the backslash. Second, a literal that
starts with a newline produces an empty first element, as the List(, first, second) shows. The margin only applies to
lines that begin (after whitespace) with the margin character; any other line is left untouched.
The s interpolator
Prefixing a string literal with an identifier, id"...", makes the compiler call StringContext("...parts...").id(args*).
The standard library defines three such methods on StringContext: s, f and raw. The s interpolator replaces
$name with the toString of the value and ${expression} with the value of any expression, including blocks,
conditionals and method calls. $name takes a plain identifier only; $t.name interpolates t and leaves .name as
text, so use braces for member access.
//> using scala 3.10.0
case class Trail(name: String, km: Double, ascent: Int)
@main def interpolators(): Unit =
val t = Trail("Pico del Aguila", 12.5, 840)
val grade = 3
// s: embed names and blocks
println(s"Trail ${t.name} is ${t.km} km long, grade $grade")
println(s"Climb in 10 years: ${t.ascent + 100} m")
println(s"${if t.km > 10 then "long" else "short"} trail")
println(s"$t")
println(s"$grade.toString vs ${grade}th")
// escapes: $$ is a dollar, $" is a double quote (Scala 3)
println(s"Entry fee: $$5, sign says: $"Keep right$"")
println(s"Tab:\tdone")
// f: printf-style, checked at compile time
println(f"${t.km}%.2f km, ${t.ascent}%05d m, ${t.name}%-20s|")
println(f"$grade%d and 100%%")
// raw: no escape processing
println(raw"C:\trails\new $grade")
println("C:\trails\new")
Trail Pico del Aguila is 12.5 km long, grade 3
Climb in 10 years: 940 m
long trail
Trail(Pico del Aguila,12.5,840)
3.toString vs 3th
Entry fee: $5, sign says: "Keep right"
Tab: done
12,50 km, 00840 m, Pico del Aguila |
3 and 100%
C:\trails\new 3
C: rails
ew
Inside a single-quoted interpolated string the escape for a quote is the meta character $ itself. In Scala 2 there was
no way to write a " inside s"..." (a backslash cannot work, because each interpolator decides how to treat
backslashes, so the parser cannot know whether the " ends the literal); you had to switch to triple quotes. Scala 3
reads a dollar followed by a quote as an escaped quote, and a doubled dollar sign as an escaped dollar, in every
interpolator, raw included (checked on 3.10.0: a raw literal containing a doubled dollar and 5 prints $5).
Like any other literal, an interpolated string can use triple quotes, so s"""Quote: "${t.name}" ok""" works with no escapes.
Because s applies the standard escape processing (\n, \t, \", \\, \u0041), the s interpolator is the right default.
${...} can contain any expression but must be closed on the same literal; an unbalanced brace fails to compile
with unclosed string literal and '}' expected, but eof found.
The f interpolator
f"..." is a type-checked printf. Each $value may be followed immediately by a java.util.Formatter format
specifier (%d, %5.2f, %-20s, %05d, %x, %,d…). Without a specifier the format defaults to %s. A % that
is not attached to an interpolated value must be doubled (%%) and %n is a newline. Unlike String.format, the
compiler checks that the argument type fits the conversion, so mistakes are compile errors rather than runtime
`IllegalFormatException`s:
//> using scala 3.10.0
@main def bad(): Unit =
val n = 3
val s = "x"
println(f"$s%d")
println(f"$n%q")
println(f"$s%s %s")
[error] ./p6.scala:5:14
[error] Found: (s : String), Required: Int, Long, Byte, Short, BigInt
[error] println(f"$s%d")
[error] ^
[error] ./p6.scala:6:16
[error] illegal conversion character 'q'
[error] println(f"$n%q")
[error] ^
[error] ./p6.scala:7:18
[error] conversions must follow a splice; use %% for literal %, %n for newline
[error] println(f"$s%s %s")
[error] ^^
The most common surprise is locale. f (like String.format) formats numbers with the JVM’s default locale. The
earlier output 12,50 km came from a machine whose default locale is es_ES, which uses a decimal comma. Programs whose
output is parsed by other programs, or compared in tests, should pin the locale:
// continuing the multiline example: f uses the default locale, so the result depends on the machine
println(Locale.getDefault)
println(f"${12.5}%.2f")
println("%.2f".formatLocal(Locale.ROOT, 12.5)) // explicit locale
Locale.setDefault(Locale.ROOT) // or change the default for the whole JVM
println(f"${12.5}%.2f")
es_ES
12,50
12.50
12.50
"...".format(args*) and formatLocal(locale, args*) are the StringOps equivalents when the format string is not a
literal. Use the interpolator when the format is a literal (you get checking), and format when it comes from data.
The raw interpolator
raw"..." performs the same substitutions as s but leaves backslash sequences exactly as written. It suits Windows
paths and regular expressions, which otherwise need doubled backslashes. In the example above raw"C:\trails\new $grade"
prints the backslashes, while the ordinary literal "C:\trails\new" turned \t into a tab and \n into a newline.
A triple-quoted literal is also unescaped, so """C:\trails\new""" and raw"""...""" are equivalent when you need no
substitution. To put a literal " in a single-quoted raw literal use $".
The \s escape (Scala 3.10)
Scala 3.10 adds the Java-style \s escape: it means one space (U+0020). String literals accept it, including in
interpolators; raw keeps the two characters. Its purpose is to make trailing spaces visible and safe, for
example in fixed-width records, where an editor that trims whitespace would silently shorten a line.
It is a 3.10 feature and is not available on the 3.9 LTS or earlier lines.
//> using scala 3.10.0
@main def esc(): Unit =
println("[" + "ab\s\s" + "]")
println(s"a\sb")
println(raw"a\sb")
[ab ]
a b
a\sb
Other escapes are unchanged: \b \t \n \f \r \" \' \\, octal escapes are not supported ("\0" fails with octal escape literals are unsupported: use \u0000 instead), and
Unicode escapes \uXXXX are processed by the compiler. A backslash followed by any other character, including a \ at the end of a
line (Java’s text-block line continuation does not exist in Scala), is a compile error (invalid escape character).
Custom interpolators
Any identifier can be an interpolator. id"a${x}b${y}c" is compiled to StringContext("a", "b", "c").id(x, y), and StringContext.parts
holds the literal fragments. In Scala 3 the idiomatic way to add one is an extension method on StringContext
(Scala 2 used an implicit class). The method can return any type, so interpolators work as typed constructors; a
val d = km"12.5" can build an opaque Km, a regular expression or a SQL statement.
Interpolators can also appear in patterns: id"a$x-$y" in a pattern is compiled to a call of StringContext(...).id.unapplySeq(scrutinee).
The standard s interpolator already works that way, which is useful for parsing simple text without a regular expression:
//> using scala 3.10.0
opaque type Km = Double
object Km:
def apply(d: Double): Km = d
extension (k: Km) def value: Double = k
extension (sc: StringContext)
def km(args: Any*): Km =
Km(sc.s(args*).trim.toDouble)
def shout(args: Any*): String =
sc.parts.zipAll(args, "", "").map((p, a) => p.toUpperCase + a).mkString
def hut: HutPattern = HutPattern(sc)
class HutPattern(sc: StringContext):
def unapplySeq(s: String): Option[Seq[String]] =
val Seq(prefix, suffix) = sc.parts: @unchecked
Option.when(s.startsWith(prefix) && s.endsWith(suffix))(Seq(s.stripPrefix(prefix).stripSuffix(suffix)))
@main def custom(): Unit =
val half = 6.25
println(km"${half * 2}".value)
println(shout"trail ${half} to peak ${1}")
"Hut:Refugio" match
case hut"Hut:$name" => println(s"hut named $name")
case _ => println("not a hut")
"T3" match
case s"T$g" => println(s"grade text $g")
"Pico-12.5" match
case s"$name-$len" => println(s"$name / $len")
"Pico-12.5-extra" match
case s"$a-$b" => println(s"$a | $b")
"no match" match
case s"$a-$b" => println("matched")
case other => println(s"no dash in: $other")
val s"$h:$m" = "09:30".runtimeChecked
println(h.toInt * 60 + m.toInt)
12.5
TRAIL 6.25 TO PEAK 1
hut named Refugio
grade text 3
Pico / 12.5
Pico | 12.5-extra
no dash in: no match
570
Points to remember about patterns:
-
Every
$xin a pattern binds aString; there is no type or format syntax, so convert afterwards (h.toInt). -
The first variable takes the shortest text before the first separator, so
s"$a-$b"againstPico-12.5-extragivesPicoand12.5-extra. -
A pattern definition such as
val s"$h:$m" = "09:30"is refutable (the match can fail at runtime), and Scala 3.10 warns about it even without options. Real compiler message:
[warn] ./k.scala:5:7
[warn] pattern binding uses refutable extractor `_root_.scala.StringContext.apply(["",":","" : String]*).s`
[warn]
[warn] If this usage is intentional, this can be communicated by adding `.runtimeChecked` after the expression,
[warn] which may result in a MatchError at runtime.
[warn] This patch can be rewritten automatically under -rewrite -source 3.8-migration.
[warn] val s"$h:$m" = "09:30"
[warn] ^^^^^^^^^^^^^^^^^^
Adding .runtimeChecked (as in the example) tells the compiler that a MatchError is acceptable; otherwise write a
match with a fallback case. A custom pattern interpolator needs a class with unapplySeq/unapply, because the pattern
hut"Hut:$name" desugars to StringContext("Hut:", "").hut.unapplySeq(...), which is why hut returns an object
rather than a value.
When the literal can be validated at compile time, an inline or macro-based implementation turns mistakes into compile errors; see
Inline. Third-party libraries such as Scalameta (quasiquotes), Doobie (sql) and
sttp (uri) are built on this mechanism.
Dedented string literals (experimental in 3.10)
Dedented string literals implement SIP-72 as an experimental feature of Scala 3.10. They add a ''' multiline literal
that strips indentation in the language, so you no longer need stripMargin. The opening ''' must be followed by a
newline and the closing one must be preceded by a newline and whitespace. The compiler removes the first newline, the last
newline with the whitespace before the closing delimiter, and the common indentation up to the column of the closing
delimiter. Lines may not be indented less than the closing delimiter. s'''...''' and f'''...''' work the same way.
Because the feature is experimental, it has to be switched on, either with the import language.experimental.dedentedStringLiterals
or with the compiler option -language:experimental.dedentedStringLiterals; it is not part of the 3.9 LTS and
may change or be removed. Do not use it in library code.
//> using scala 3.10.0
//> using options -language:experimental.dedentedStringLiterals
@main def dedented(): Unit =
val name = "Pico"
val plain = '''
Trail: Pico
Length: 12.5
'''
println(plain)
assert(plain == "Trail: Pico\n Length: 12.5")
val interpolated = s'''
Trail: $name
'''
println(interpolated)
Trail: Pico
Length: 12.5
Trail: Pico
Without the flag ''' is not a string at all; the compiler reads it as empty character literals and reports errors such
as empty character literal (use '\'' for single quote) and unclosed character literal.
Scala 2, Java and Kotlin compared
| Feature | Scala 3 | Java | Kotlin |
|---|---|---|---|
Identity vs value equality |
|
|
|
Multiline literal |
|
Text block |
|
Interpolation |
|
|
|
Escape a quote in an interpolation |
|
|
|
Format checking |
|
runtime only |
runtime only |
Interpolators in patterns |
yes ( |
no |
no |
|
3.10 |
Java 15 |
no |
Scala 2.13 differs from Scala 3 only in details: $" is not accepted (use triple quotes), the .runtimeChecked guidance
for refutable pattern bindings does not exist, and custom interpolators are written with implicit class wrappers. The cross-building implications are in
Scala 2 & Scala 3 Compatibility.
Pitfalls
-
$name_1is the identifiername_1, notnamefollowed by_1: braces (${name}_1) delimit the name. -
$t.nameinterpolatestand prints.nameliterally, as$grade.toStringprinted3.toStringabove. -
println(a, b)is not a two-argument print: the compiler rejects it becauseprintlnis overloaded, so wrap the tuple (printlna, b) or build one string. -
The default locale changes
fandformat. A report written on one machine and read on another may disagree. -
Escapes in
sare processed by the interpolator, not the parser: custom interpolators that callsc.partsreceive the raw text with the backslashes intact, and must decide what to do with them (StringContext.processEscapesdoes that). -
Do not build SQL or shell commands with
s; an interpolator that takes the arguments separately (as Doobie’ssqldoes) avoids injection.
Where the books are outdated
Older Scala books describe string interpolation as a Scala 2.10 feature and show implicit class wrappers for custom
interpolators; in Scala 3 prefer an extension on StringContext as above. They also predate $", \s and dedented strings, the
.runtimeChecked guidance for refutable pattern bindings, and the 2.13 collection overhaul (StringOps is a
value class built on the new collections, and the JDK 11 String.lines now shadows the old Scala lines).
Check any listing from a book against the official pages below, which track the current release.