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)

Several details in the output matter in practice:

  • map and filter return a String only when the function keeps the element type (Char => Char); _.toInt returns an ArraySeq[Int] because a String cannot hold integers.

  • name(1) is name.apply(1) from StringOps, so indexing uses parentheses, not brackets.

  • lines resolves to the JDK method String.lines() (JDK 11+), which returns a java.util.stream.Stream, not a Scala collection. Scala’s own method is linesIterator; use it, and linesWithSeparators when you need to keep the \n.

  • split interprets 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 Char is one UTF-16 unit. The emoji above has length 2 but is one code point; use codePoints or codePointCount when text may contain supplementary characters.

  • == on strings is value equality (it calls equals), unlike Java where == is reference equality. Reference identity is eq.

  • Strings are immutable: building a string in a loop with + copies each time. Use StringBuilder (scala.collection.mutable.StringBuilder, or java.lang.StringBuilder) or mkString on 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 $x in a pattern binds a String; 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" against Pico-12.5-extra gives Pico and 12.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

== is equals, eq is identity

== is identity, equals is value

== is equals, === is identity

Multiline literal

"""...""" plus stripMargin, or ''' (experimental)

Text block """...""", indentation stripped by the compiler

"""...""" plus trimIndent() or trimMargin()

Interpolation

s"$x ${expr}", f, raw, user-defined

String.formatted and String.format

"$x ${expr}" built in

Escape a quote in an interpolation

$" (Scala 3); Scala 2 needed triple quotes

\"

\"

Format checking

f"..." checked at compile time

runtime only

runtime only

Interpolators in patterns

yes (case s"$a-$b")

no

no

\s space escape

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_1 is the identifier name_1, not name followed by _1: braces (${name}_1) delimit the name.

  • $t.name interpolates t and prints .name literally, as $grade.toString printed 3.toString above.

  • println(a, b) is not a two-argument print: the compiler rejects it because println is overloaded, so wrap the tuple (printlna, b) or build one string.

  • The default locale changes f and format. A report written on one machine and read on another may disagree.

  • Escapes in s are processed by the interpolator, not the parser: custom interpolators that call sc.parts receive the raw text with the backslashes intact, and must decide what to do with them (StringContext.processEscapes does that).

  • Do not build SQL or shell commands with s; an interpolator that takes the arguments separately (as Doobie’s sql does) 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.