Variables: val and var

Every Kotlin program needs somewhere to store data, and Kotlin gives you two keywords for that: val and var. The choice between them is not just a style preference — it changes what the compiler is willing to let you do, and it is one of the first ways Kotlin nudges you toward safer code than Java. If you come from Java, think of val as final made the default instead of an opt-in afterthought, and var as the familiar mutable local variable you already know.

Overview: How val and var Work

val declares a read-only reference. Once you assign it a value, that name can never be bound to a different value again for the rest of its scope. var declares a mutable reference — you can reassign it as many times as you like, as long as every new value has a type compatible with the variable’s type.

Both val and var require a value before they can be read. The compiler performs definite-assignment analysis: if there is any code path where a variable might be read before it has a value, your program fails to compile. This is stricter than Java, which only enforces this for local variables and lets object fields default to null or zero.

Kotlin also has type inference. If you initialize a variable at declaration, you can omit the type and Kotlin infers it from the value on the right-hand side. That inferred type is then fixed for the life of the variable — a var can hold new values, but never a value of a different, incompatible type. var does not mean “can hold anything”; it only means “can be reassigned.”

A subtlety that trips up almost every newcomer: val only makes the reference read-only, not the object it points to. A val holding a MutableList cannot be pointed at a different list later, but the list’s contents can still be added to, removed from, or changed in place. Real immutability of the underlying data depends on the type you choose (for example, using listOf() instead of mutableListOf()), not on val alone.

Under the hood, a val or var declared as a class property compiles to a private backing field plus an auto-generated getter (and, for var, an auto-generated setter). That is why idiomatic Kotlin code almost never writes manual getters and setters the way Java does — the compiler writes them for you from the val/var declaration. For local variables inside a function, there is no backing field or accessor at all; the compiler simply tracks, at compile time, whether the name has already been assigned. Reassigning a val is not a runtime check — it is rejected before your program ever compiles, at zero runtime cost. This compile-time guarantee is also what powers Kotlin’s smart-casting of nullable types: the compiler can safely treat a checked val as non-null after an if (x != null) check because it knows nothing else can reassign x out from under that check. A var often cannot be smart-cast the same way, because another statement (or another thread) could change it between the check and the use.

Syntax

val name: Type = value
var name: Type = value
Part Meaning
val / var Chooses a read-only reference (val) or a reassignable reference (var)
name The identifier you will use to refer to the value elsewhere in the code
: Type Optional explicit type annotation; omit it and Kotlin infers the type from value
= value The initializer, required at the point of declaration (or, for a var declared without one, before its first use)

Examples

Example 1: The basics

fun main() {
    val name = "Kotlin"
    var score = 10

    score += 5
    println("Language: $name, Score: $score")
}

Output:

Language: Kotlin, Score: 15

name is a val, so it is set once and never reassigned. score is a var, so score += 5 — shorthand for score = score + 5 — is legal and updates it to 15.

Example 2: Type inference vs. explicit types

fun main() {
    val age: Int = 30
    val city = "Bangalore"
    var temperature: Double = 21.5

    temperature = 23.0
    println("Age: $age, City: $city, Temperature: $temperature")
}

Output:

Age: 30, City: Bangalore, Temperature: 23.0

age gets an explicit Int annotation, while city‘s type (String) is inferred purely from its initializer — both are equally valid Kotlin. temperature is a var declared as Double; it can be reassigned to 23.0, but only because 23.0 is also a Double. Note the printed value is 23.0, not 23 — Kotlin’s Double.toString() always shows the decimal point.

Example 3: val reference vs. mutable contents

fun main() {
    val numbers = mutableListOf(1, 2, 3)
    numbers.add(4)
    numbers[0] = 100
    println(numbers)
}

Output:

[100, 2, 3, 4]

numbers is a val, so the name numbers can never point to a different list. But the list itself is a MutableList, so its contents can still change freely — add and index assignment both work. This is the single most important distinction to internalize about val.

Example 4: A realistic use of var as an accumulator

fun calculateTotal(prices: List<Double>): Double {
    var total = 0.0
    for (price in prices) {
        total += price
    }
    return total
}

fun main() {
    val cart = listOf(19.99, 5.49, 12.0)
    val total = calculateTotal(cart)
    println("Total: ${"%.2f".format(total)}")
}

Output:

Total: 37.48

Inside calculateTotal, total genuinely needs to change on every loop iteration, so var is the right tool. Everything else — the input list, the returned sum, and the final formatted string — never changes once assigned, so they are all val. This mix is typical idiomatic Kotlin: var only where mutation is the point, val everywhere else.

How It Works Step by Step

When the compiler processes a val or var declaration, it performs roughly these steps:

  • It evaluates the initializer expression (if present) and determines its type — either the explicit annotation you wrote, or the inferred type of the initializer.
  • It records the variable’s mutability (read-only or mutable) alongside its type in the compiler’s internal symbol table for that scope.
  • Every subsequent read of the variable is checked against definite-assignment rules — using it before any assignment is a compile error.
  • Every subsequent assignment to a val (other than its single initializer) is rejected at compile time with “Val cannot be reassigned.” Assignments to a var are checked only for type compatibility.
  • For class-level properties, the compiler additionally generates a backing field and accessor methods (getX(), and setX() only for var), which is what other JVM code — including Java code calling into your Kotlin class — actually invokes.

None of this is a runtime check. By the time your program runs, an attempted reassignment of a val simply does not exist in the compiled bytecode — the program that violates the rule never got built in the first place.

Common Mistakes

Mistake 1: Trying to reassign a val

val pi = 3.14
pi = 3.14159

This fails to compile with “Val cannot be reassigned”. If a value genuinely needs to change over time, it was never a val in the first place — declare it as var instead:

var pi = 3.14
pi = 3.14159
println(pi)

If the value truly should never change, the fix is to remove the reassignment, not the keyword — the compiler was correctly catching a bug.

Mistake 2: Assuming a val list can always be mutated

val fruits = listOf("apple", "banana")
fruits.add("cherry")

This fails with “unresolved reference: add” — not because of val, but because listOf() returns a read-only List, which has no add function at all. The fix is to use mutableListOf() when you need a collection whose contents can change:

fun main() {
    val fruits = mutableListOf("apple", "banana")
    fruits.add("cherry")
    println(fruits)
}

Output:

[apple, banana, cherry]

Mistake 3: Defaulting to var out of habit

var discountRate = 0.1
val price = 200.0
// ...much later in the same function, another edit slips in:
discountRate = 0.0
println(price - (price * discountRate))

This compiles fine and prints 200.0 — silently applying no discount at all, because nothing stopped a later line from overwriting discountRate. If discountRate had been declared val in the first place, that accidental second assignment would have failed to compile immediately, catching the bug the moment it was introduced instead of at runtime (or never):

val discountRate = 0.1
val price = 200.0
discountRate = 0.0 // Error: Val cannot be reassigned
println(price - (price * discountRate))

Declaring everything var “just in case” throws away this protection for no benefit — reach for var only when a value is actually meant to change.

Best Practices

  • Default to val for every new variable; only switch to var once you have a concrete reason the value must change.
  • Don’t confuse a read-only reference with an immutable object — a val holding a MutableList, MutableMap, or a mutable custom class can still change internally.
  • When you truly need an unchangeable collection, use the read-only factory functions (listOf, mapOf, setOf) rather than relying on val to protect the contents.
  • Let type inference do the work for obvious cases (val name = "Kotlin"); add an explicit type annotation when it improves readability or when the inferred type would be too broad or unclear.
  • Avoid reusing a single var for multiple unrelated purposes in the same function — prefer a fresh, well-named val for each distinct piece of data.
  • In loops and accumulation, use var for the running value (a counter or total) but keep the loop’s input collection and final result as val.

Practice Exercises

  • Declare a val holding your name and a var holding your age. Print both in one string template, then write a line that increases the age by one and prints it again.
  • Create a val referencing a mutableListOf of three integers. Without reassigning the val, write code that removes the first element and adds a new element at the end, then print the list. (Expected output shape: a list with two of the original numbers plus one new one.)
  • Write a function that takes a List<Int> and returns the largest value using a var to track the running maximum inside a loop. Call it from main with a small list and print the result.

Summary

  • val creates a read-only reference that can be assigned exactly once; var creates a reference that can be reassigned any number of times.
  • Both require a value before use, and Kotlin infers the type from the initializer when no explicit type is given.
  • val only prevents rebinding the name — it does not make the underlying object immutable; mutable collections and objects can still change in place.
  • Reassigning a val is a compile-time error, not a runtime check, so bugs from accidental mutation are caught before the program ever runs.
  • Class properties declared with val/var generate getters (and, for var, setters) automatically, replacing Java’s manual boilerplate.
  • Prefer val by default and reach for var only when a value genuinely needs to change, such as an accumulator in a loop.