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

The conventions that follow from this are the reason Scala code looks different from Java code:

  • Prefer val. Most Scala code has no var at all; a var is a signal that state changes and is worth a second look.

  • A val is 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 as List and case classes.

  • The name starts with a lowercase letter for values and variables (trailName); constants in a companion object conventionally use an initial capital (Max), which also matters for pattern matching (see Pattern Matching).

  • A definition is a statement, but an assignment x = 5 is an expression of type Unit. There is no chained assignment (a = b = 0 does not do what it does in Java).

  • val, var and lazy val can appear at the top level of a file, in an object, 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

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)

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
  • 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 val runs its right-hand side on first access and caches the result; it is thread-safe. See Laziness.

  • final val with a literal right-hand side and no type annotation defines a constant value definition: the compiler gives it the singleton literal type. A non-final val gets 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

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

Byte

8-bit signed integer

-128 to 127

byte

Short

16-bit signed integer

-32,768 to 32,767

short

Int

32-bit signed integer

-2,147,483,648 to 2,147,483,647

int

Long

64-bit signed integer

-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807

long

Float

32-bit IEEE 754

about 7 significant decimal digits

float

Double

64-bit IEEE 754

about 15 to 16 significant decimal digits

double

Char

16-bit unsigned UTF-16 code unit

0 to 65,535

char

Boolean

true or false

boolean

Unit

the single value ()

"no useful result"

void

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

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

42, 1_000_000

Int; Long with an L suffix (use the capital, l reads as 1)

Hexadecimal

0x1F, 0XFF, 0xCAFE_BABE

Int, or Long with L; 0xFFFFFFFF is -1

Binary

0b1010, 0B1010_1010

Int, or Long with L

Floating point

1.5, .5, 6.02e23, 1e-3

Double; Float with f/F, Double explicit with d/D

Char

'x', '\n', '\u0041'

Char

String

"text", "tab\t", triple-quoted """raw text"""

String (see Strings & Interpolation)

Boolean

true, false

Boolean

null

null

Null

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. 0b alone is rejected, and only 0 and 1 may follow the b.

  • An Int literal that does not fit is a compile-time error, and the error for a literal assigned to a smaller type shows the literal as an Int:

//> 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 010 is 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 Char takes part in arithmetic as a number: 'a' + 1 is the Int 98, and ('a' + 1).toChar is 'b'.

  • null has its own type, Null, which is a subtype of every reference type but not of Int, Boolean and the other value types. See the next section.

  • A string literal needs no escape for a single quote. The s, f and raw prefixes 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

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

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

val / var

final local / plain local (var infers the type only)

val / var

Primitives

Int, Double, …​ are classes, compiled to primitives when possible

separate primitive types and boxed classes

same as Scala for Int, Double, …​

Implicit widening

Int to Long and Double allowed, narrowing never

widening allowed, narrowing needs a cast

no implicit widening at all (val l: Long = i is an error)

Octal literal

010 is decimal 10 (no warning in 3.10.0)

010 is 8

not supported

Binary / underscore literals

0b1010, 1_000

0b1010, 1_000

0b1010, 1_000

Increment

x += 1 (no ++)

x++

x++, x += 1

Late initialization

var x: T = uninitialized

field default values

lateinit var

Arbitrary precision

BigInt, BigDecimal with operators

BigInteger, BigDecimal with methods

BigInteger, BigDecimal with operators

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; use scala.compiletime.uninitialized.

  • Collections and if expressions of mixed numerics inferring Double through weak conformance. In Scala 3 only Int literals adapt; write .toDouble for variables.

  • Implicit conversions of Int or Long to Float/Double presented 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-nulls option are documented in the reference.