Values, Variables & Literals
|
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, released 2026-10-12; JDK 17 or newer). Every example was compiled and run
with Scala CLI and Scala 3.10.0 on JDK 26, and the output and compiler messages shown are the real ones from that run
(file paths are shortened to the file name). Everything here behaves the same on the 3.9 LTS line. The only
version-sensitive item is the deprecation of var x: T = _ (3.4), which warns on 3.4 and later, so also on 3.9 LTS, but
is silent on the 3.3 LTS line.
It covers the two ways to introduce a name (val and var), the built-in value types and their literals, arbitrary
precision numbers, the rules of numeric conversion, and the places where Scala deliberately differs from Java, Kotlin,
Python and from Scala 2.
val and var
A definition starts with val (an immutable binding, evaluated once) or var (a mutable variable). Types are inferred
from the right-hand side and can be written after a colon when you want to fix or document them.
//> using scala 3.10.0
@main def trailheadVals(): Unit =
val trailName = "Cabane Alpine" // inferred String, cannot be reassigned
val lengthKm: Double = 12.5 // explicit type annotation
var restStops = 0 // inferred Int, can be reassigned
restStops += 1
restStops = restStops + 1
println(s"$trailName: $lengthKm km, $restStops rest stops")
assert(restStops == 2)
Cabane Alpine: 12.5 km, 2 rest stops
Assigning to a val is a compile-time error, not a run-time one:
//> using scala 3.10.0
@main def reassign(): Unit =
val ascentM = 840
ascentM = 900
-- [E052] Type Error: f02.scala:5:10
5 | ascentM = 900
| ^^^^^^^^^^^^^
| Reassignment to val ascentM
|
| longer explanation available when compiling with `-explain`
1 error found
Compilation failed
Source: Tour of Scala: Variables.
The conventions that follow from this are the reason Scala code looks different from Java code:
-
Prefer
val. Most Scala code has novarat all; avaris a signal that state changes and is worth a second look. -
A
valis immutable as a binding, not as an object.val xs = scala.collection.mutable.ArrayBuffer(1)can still be appended to. Immutability of the data comes from choosing immutable types such asListand case classes. -
The name starts with a lowercase letter for values and variables (
trailName); constants in a companionobjectconventionally use an initial capital (Max), which also matters for pattern matching (see Pattern Matching). -
A definition is a statement, but an assignment
x = 5is an expression of typeUnit. There is no chained assignment (a = b = 0does not do what it does in Java). -
val,varandlazy valcan appear at the top level of a file, in anobject, a class or a method body. In a class parameter list they also create a member (see Classes & Constructors).
Compound assignment and a missing operator
var supports compound assignment (+=, -=, *=, /=, %= and so on). There is no ` or `--`: they are
ordinary method names, and `level does not parse.
//> using scala 3.10.0
@main def assignment(): Unit =
var level = 40
level += 5
level *= 2
println(level)
level++
-- [E018] Syntax Error: f18.scala:8:9
8 | level++
| ^
| expression expected but unindent found
|
| longer explanation available when compiling with `-explain`
1 error found
Compilation failed
Source: Tour of Scala: Variables.
Write level += 1 instead. A related trap arrived with named tuples (3.7): an assignment wrapped in parentheses is no
longer an assignment.
//> using scala 3.10.0
@main def parenthesised(): Unit =
var level = 40
val r = (level = 3)
println(r)
-- [E203] Syntax Migration Warning: f19.scala:5:10
5 | val r = (level = 3)
| ^^^^^^^^^^^
|Deprecated syntax: since 3.7 this is interpreted as a named tuple with one element,
|not as an assignment.
|
|To assign a value, use curly braces: `{level = 3}`.
|This can be rewritten automatically under -rewrite -source 3.6-migration.
1 warning found
(3)
Source: Tour of Scala: Variables.
level is not modified. See Tuples & Named Tuples.
Definitions that bind several names, lazily, or as constants
//> using scala 3.10.0
@main def patterns(): Unit =
val (name, beds) = ("north", 12) // pattern definition: two vals at once
val a, b = 7 // both are 7
lazy val expensive =
println("computing")
42
println(s"$name $beds ${a + b}")
println("before")
println(expensive)
println(expensive)
north 12 14
before
computing
42
42
Source: Tour of Scala: Values
-
A pattern on the left of
val(here a tuple pattern) binds several names at once; the pattern rules are on Extractors & Pattern Bindings. -
lazy valruns its right-hand side on first access and caches the result; it is thread-safe. See Laziness. -
final valwith a literal right-hand side and no type annotation defines a constant value definition: the compiler gives it the singleton literal type. A non-finalvalgets the widened type.
//> using scala 3.10.0
final val Max = 100
val NotFinal = 100
@main def constants(): Unit =
val a: 100 = Max
println(a)
val b: 100 = NotFinal
-- [E007] Type Mismatch Error: f17.scala:9:15
9 | val b: 100 = NotFinal
| ^^^^^^^^
| Found: (NotFinal : Int)
| Required: (100 : Int)
|
| longer explanation available when compiling with `-explain`
1 error found
Compilation failed
Source: Tour of Scala: Values
Constant types matter for inline code and match types (see Inline and
Match Types); for ordinary code you rarely write final val.
Type annotations and inference
Local and private definitions are usually left to type inference. Annotate the type when it is a public API member,
when the inferred type would be too narrow (for example val xs: Seq[Int] = List(1, 2)), or when you want a literal
converted (val b: Byte = 12). The full inference rules are on Type Inference.
The value types
Scala has no primitive types in its syntax. Int, Double and the rest are classes (subtypes of AnyVal), and the
compiler represents them as the JVM primitives int, double, and so on whenever it can, boxing them only when a
generic or Any-typed context needs an object. That gives primitive speed without a separate set of operators: 1 + 2
is the method call 1.+(2) on Int, compiled to an iadd instruction.
| Type | Values | Range or precision | Java type |
|---|---|---|---|
|
8-bit signed integer |
-128 to 127 |
|
|
16-bit signed integer |
-32,768 to 32,767 |
|
|
32-bit signed integer |
-2,147,483,648 to 2,147,483,647 |
|
|
64-bit signed integer |
-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
|
|
32-bit IEEE 754 |
about 7 significant decimal digits |
|
|
64-bit IEEE 754 |
about 15 to 16 significant decimal digits |
|
|
16-bit unsigned UTF-16 code unit |
0 to 65,535 |
|
|
|
|
|
|
the single value |
"no useful result" |
|
Integer types are two’s-complement and wrap around on overflow without any exception. Integer division truncates toward
zero and % keeps the sign of the dividend; floating-point division by zero gives an infinity, and NaN is never equal
to itself.
//> using scala 3.10.0
@main def valueTypes(): Unit =
val b: Byte = 127
val s: Short = 32767
val i: Int = 2147483647
val l: Long = 9223372036854775807L
val f: Float = 3.4028235f
val d: Double = 1.7976931348623157e308
val c: Char = 'A'
val flag: Boolean = true
val nothingMuch: Unit = ()
println(List(b, s, i, l, f, d, c, flag, nothingMuch).mkString(", "))
println(Int.MaxValue + 1) // wraps around silently
println(Int.MaxValue.toLong + 1) // widen first to avoid it
println(10 / 4) // integer division
println(10 / 4.0)
println(-7 / 2)
println(-7 % 2)
println(7.0 / 0) // no exception for floating point
println(Double.NaN == Double.NaN)
try Math.addExact(Int.MaxValue, 1)
catch case e: ArithmeticException => println(e.getMessage)
127, 32767, 2147483647, 9223372036854775807, 3.4028234, 1.7976931348623157E308, A, true, ()
-2147483648
2147483648
2
2.5
-3
-1
Infinity
false
integer overflow
Source: Scala 3 Book: Scala’s value types.
The 3.4028235f literal prints as 3.4028234: a Float is shown with the shortest digits that identify it, which is
not always the digits you typed. Math.addExact is the Java method that throws on overflow; there is no checked
arithmetic operator in the language. Char is an unsigned numeric type, and Unit is a real value (()) that
methods without a result return. The rest of the hierarchy (Any, AnyVal, AnyRef, Nothing, Null) is on
Type Hierarchy & Matchable.
Every numeric type has conversion methods (toInt, toLong, toDouble, toChar, …) and rich operations through
the standard library (max, min, abs, until, to, isWhole, round).
BigInt and BigDecimal
For integers beyond Long, and for exact decimal arithmetic, the standard library provides BigInt and BigDecimal
(wrappers around java.math.BigInteger and java.math.BigDecimal). They are ordinary classes, so you build them from a
string or a number and use the normal operators.
//> using scala 3.10.0
@main def bigNumbers(): Unit =
val big = BigInt("123456789012345678901234567890")
println(big * 2)
println(BigInt(2).pow(100))
println(0.1 + 0.2) // Double
println(BigDecimal("0.1") + BigDecimal("0.2")) // exact
println(BigDecimal(0.1) + BigDecimal(0.2)) // built from a Double: 0.1 is already rounded
println(BigDecimal("1.10") == BigDecimal("1.1"))
println(BigDecimal("10.005").setScale(2, BigDecimal.RoundingMode.HALF_UP))
246913578024691357802469135780
1267650600228229401496703205376
0.30000000000000004
0.3
0.3
true
10.01
Source: Scala 3 Book: BigInt and BigDecimal, with the API pages BigInt and BigDecimal.
Two details are worth knowing. BigDecimal(0.1) goes through Double.toString, so (unlike Java’s new BigDecimal(0.1))
it does not expose the binary expansion of the double, but it still starts from an inexact value: pass a String when
the digits matter. And Scala’s BigDecimal compares by numeric value (1.10 == 1.1 is true), where Java’s
BigDecimal.equals also compares the scale. There is no literal syntax for either type.
Literals
//> using scala 3.10.0
@main def literals(): Unit =
val dec = 1_000_000 // underscores are ignored
val hex = 0xCAFE_BABE // Int: wraps to a negative number
val bin = 0b1010_1010 // binary literal
val mask = 0xFFFFFFFFL // L makes it a Long
val lng = 1_000_000_000_000L
val dbl = 6.02e23
val flt = 2.5f
val ch = 'x'
val uni = '\u0041'
val tab = "tab\tseparated"
val tf = true
val nothing: String | Null = null
println(List(dec, hex, bin, mask, lng).mkString(" "))
println(List(dbl, flt).mkString(" "))
println(s"$ch $uni $tab $tf $nothing")
println(s"${hex.getClass.getSimpleName} ${mask.getClass.getSimpleName} ${dbl.getClass.getSimpleName} ${flt.getClass.getSimpleName}")
println(List(.5, 1e-3, 0x1F, 0XFF, 100L, 1.5F, 2D))
println(0xFFFFFFFF == -1)
1000000 -889275714 170 4294967295 1000000000000
6.02E23 2.5
x A tab separated true null
int long double float
List(0.5, 0.001, 31, 255, 100, 1.5, 2.0)
true
| Kind | Forms | Type |
|---|---|---|
Decimal integer |
|
|
Hexadecimal |
|
|
Binary |
|
|
Floating point |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Points that regularly surprise newcomers:
-
Underscores are allowed anywhere between digits and are ignored (
1_000_000,0b1010_1010). They are a readability aid only. -
Binary literals (
0b...) exist; per the reference page they were also backported to Scala 2.13.13.0balone is rejected, and only0and1may follow theb. -
An
Intliteral that does not fit is a compile-time error, and the error for a literal assigned to a smaller type shows the literal as anInt:
//> using scala 3.10.0
@main def literalErrors(): Unit =
val tooBigForInt = 3000000000
val tooBigForByte: Byte = 200
-- Error: f06.scala:4:21
4 | val tooBigForInt = 3000000000
| ^^^^^^^^^^
| number too large
-- [E007] Type Mismatch Error: f06.scala:5:28
5 | val tooBigForByte: Byte = 200
| ^^^
| Found: (200 : Int)
| Required: Byte
|
| longer explanation available when compiling with `-explain`
2 errors found
Compilation failed
Write 3000000000L for the first. The rule for the second is that an Int literal is accepted where a Byte, Short
or Char is expected only if its value fits, so val b: Byte = 127 and val c: Char = 97 compile, while a
non-literal Int never converts (see Numeric widening and the dropped weak conformance).
-
Leading zeros do not mean octal. In Java
010is 8. Scala 3.10.0 reads it as the decimal number:
//> using scala 3.10.0
@main def leadingZeros(): Unit =
val a = 010
val b = 08
println((a, b))
(10,8)
Compilation succeeds without a warning, so a literal pasted from Java or C code keeps compiling and silently changes
value. If you really want the octal value, use the hexadecimal or binary form, or call Integer.parseInt("10", 8).
-
Char literals are single UTF-16 code units. A
Chartakes part in arithmetic as a number:'a' + 1is theInt98, and('a' + 1).toCharis'b'. -
nullhas its own type,Null, which is a subtype of every reference type but not ofInt,Booleanand the other value types. See the next section. -
A string literal needs no escape for a single quote. The
s,fandrawprefixes are interpolators, not literal forms; they have their own page.
null, Unit and Nothing
null exists for Java interoperability, and a reference-typed value can be null:
//> using scala 3.10.0
@main def nulls(): Unit =
val s: String = null // allowed: Null is a subtype of every reference type
println(s)
println(s == null)
null
true
Source: Scala 3 Reference: Explicit Nulls.
Idiomatic Scala never uses null for "no value": use Option (see Option & Null Safety).
For code that must be careful, the compiler flag -Yexplicit-nulls removes Null from the subtypes of String and the
other reference types, so a possibly-null value has to be typed String | Null (the union type notation, see
Union & Intersection Types):
//> using scala 3.10.0
//> using option -Yexplicit-nulls
@main def explicitNulls(): Unit =
val s: String = null
-- [E007] Type Mismatch Error: f15.scala:5:18
5 | val s: String = null
| ^^^^
|Found: Null
|Required: String
|Note that implicit conversions were not tried because the result of an implicit conversion
|must be more specific than String
|
| longer explanation available when compiling with `-explain`
1 error found
Compilation failed
Source: Scala 3 Reference: Explicit Nulls.
Unit is the result type of methods called only for their effect, and () is its only value. Nothing is the type
of expressions that never return a value (a thrown exception, ???, an infinite loop) and is a subtype of every type.
Both appear in the type hierarchy page.
Uninitialized fields: uninitialized instead of the wildcard
A class field sometimes has to be declared first and assigned later, for example by a framework or by a lateinit-style
initializer. Scala 2 spelled the default value var x: T = _. Scala 3 replaces it with
scala.compiletime.uninitialized:
//> using scala 3.10.0
import scala.compiletime.uninitialized
class Sensor:
var reading: Double = uninitialized // the default of the type: 0.0
var label: String = uninitialized // null
var count: Int = uninitialized // 0
var active: Boolean = uninitialized // false
@main def uninit(): Unit =
val s = Sensor()
println(s"${s.reading} ${s.label} ${s.count} ${s.active}")
assert(s.reading == 0.0 && s.label == null && s.count == 0 && !s.active)
0.0 null 0 false
The old form still compiles for cross-building but is deprecated since 3.4:
//> using scala 3.10.0
class Sensor:
var reading: Double = _
@main def oldStyle(): Unit = println(Sensor().reading)
-- Warning: f09.scala:4:24
4 | var reading: Double = _
| ^
|`= _` has been deprecated; use `= uninitialized` instead.
|`uninitialized` can be imported with `scala.compiletime.uninitialized`.
|This construct can be rewritten automatically under -rewrite -source 3.4-migration.
1 warning found
0.0
The
field takes the JVM default of its type (0, 0.0, false, null), which is why a var of a reference type declared
this way can hold null until it is set. This is the closest Scala equivalent of Kotlin’s lateinit, without a runtime
check on access; for optional state prefer Option.
Numeric widening and the dropped weak conformance
Scala converts between numeric types automatically in one direction only: from a smaller to a wider type. The
conversions are Byte → Short → Int → Long → Float → Double, and Char → Int.
//> using scala 3.10.0
//> using option -deprecation
@main def widening(): Unit =
val i: Int = 123456789
val l: Long = i // Int to Long: exact
val d: Double = i // Int to Double: exact
val c: Int = 'a' // Char to Int
val s: Short = (3: Byte) // Byte to Short
println(List(l, d, c, s))
val f: Float = i // Int to Float: loses precision
println(f)
val big: Long = 123456789012345678L
val lossy: Double = big // Long to Double: loses precision
println(lossy.toLong)
-- Deprecation Warning: f10.scala:12:17
12 | val f: Float = i // Int to Float: loses precision
| ^
|method int2float in object Int is deprecated since 2.13.1: Implicit conversion from Int to Float is dangerous because it loses precision. Write `.toFloat` instead.
-- Deprecation Warning: f10.scala:15:22
15 | val lossy: Double = big // Long to Double: loses precision
| ^
|method long2double in object Long is deprecated since 2.13.1: Implicit conversion from Long to Double is dangerous because it loses precision. Write `.toDouble` instead.
2 warnings found
List(123456789, 1.23456789E8, 97, 3)
1.2345679E8
123456789012345680
The lossy conversions (Int or Long to Float, Long to Double) are deprecated since the 2.13 standard library;
call .toFloat / .toDouble to make the loss visible. -deprecation is needed to see the messages. The List(l, d, c, s)
line prints 123456789 and 1.23456789E8 side by side because the elements keep their own types instead of being
widened to a common Double (see the next sections).
The opposite direction is never automatic. A Double does not become an Int:
//> using scala 3.10.0
@main def narrowing(): Unit =
val d: Double = 3.99
val i: Int = d
-- [E007] Type Mismatch Error: f12.scala:5:15
5 | val i: Int = d
| ^
| Found: (d : Double)
| Required: Int
|
| longer explanation available when compiling with `-explain`
1 error found
Compilation failed
Use the explicit conversions. They truncate or wrap, exactly as a Java cast does:
//> using scala 3.10.0
@main def conversions(): Unit =
val d = 3.99
println(d.toInt) // truncates toward zero
println((-3.99).toInt)
println(d.round) // nearest Long
println(300.toByte) // keeps the low 8 bits
println(('a' + 1).toChar) // Char arithmetic gives an Int
println('a' + 1)
val code: Char = 97 // an Int literal that fits is accepted as a Char
println(code)
3
-3
4
44
b
98
a
Why weak conformance was dropped
Scala 2 had a weak conformance relation: when it computed the type of List(1, 2.5) or if c then 1 else 2.5, it
treated the numeric types (including Char) as if they were ordered by widening, and picked Double. That was
convenient for literals but surprising for variables: List(n, c, d) with an Int, a Char and a Double also
became a List[Double], silently widening a character into a number.
Scala 3 drops the general relation and keeps one rule: an Int literal is adapted to another numeric type when
needed. Everything else uses the least upper bound of the element types:
//> using scala 3.10.0
@main def weakConformance(): Unit =
val n: Int = 3
val c: Char = 'X'
val d: Double = math.sqrt(3.0)
val literalsOnly: List[Double] = List(1.0, math.sqrt(3.0), 0, -3.3) // Int literal 0 is adapted
val mixed = List(n, d)
val ofThree = List(n, c, d)
println(literalsOnly)
println(mixed)
println(ofThree)
val asDoubles: List[Double] = mixed
-- [E007] Type Mismatch Error: f11.scala:15:32
15 | val asDoubles: List[Double] = mixed
| ^^^^^
| Found: (mixed : List[Int | Double])
| Required: List[Double]
|
| longer explanation available when compiling with `-explain`
1 error found
Compilation failed
The first line, with a literal 0 among Double`s, is fine. `mixed is a List[Int | Double]: Scala 3 infers a union
type, where Scala 2 inferred Double (the reference text speaks of AnyVal for the three-way case; the 3.10.0 compiler
reports the union). To get a List[Double] convert explicitly: List(n.toDouble, d).
How it compares
| Topic | Scala 3 | Java | Kotlin |
|---|---|---|---|
Immutable / mutable |
|
|
|
Primitives |
|
separate primitive types and boxed classes |
same as Scala for |
Implicit widening |
|
widening allowed, narrowing needs a cast |
no implicit widening at all ( |
Octal literal |
|
|
not supported |
Binary / underscore literals |
|
|
|
Increment |
|
|
|
Late initialization |
|
field default values |
|
Arbitrary precision |
|
|
|
Python 3 has one arbitrary-precision int and no declaration keyword; in Scala BigInt is the counterpart of int
and a plain Int overflows. / on two Scala Int values is integer division (like Python’s //). See also
Java: Primitive Types and Variables,
Kotlin: Basic Types and Variables,
Python: Variables and Dynamic Typing and
Groovy: Variables, Typing, def, var & val.
Where the books are outdated
Books and tutorials written for Scala 2 teach several forms that Scala 3 changed:
-
var x: T = _as the way to leave a field uninitialized. It still compiles but warns since 3.4; usescala.compiletime.uninitialized. -
Collections and
ifexpressions of mixed numerics inferringDoublethrough weak conformance. In Scala 3 onlyIntliterals adapt; write.toDoublefor variables. -
Implicit conversions of
IntorLongtoFloat/Doublepresented as harmless. They are deprecated since the 2.13 library because they lose precision. -
Octal literals with a leading zero, as in Java or C (
017). In 3.10.0 a leading zero is just a decimal digit. -
The Scala 3 Book and the Tour show only the basics of
val/var; binary literals,uninitialized, constant definitions and the-Yexplicit-nullsoption are documented in the reference.
References
-
Scala 3 Book: Two types of variables and Scala’s value types, BigInt and BigDecimal
-
Type Hierarchy & Matchable, Strings & Interpolation and Type Inference — the neighbouring pages