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 avarare checked only for type compatibility. - For class-level properties, the compiler additionally generates a backing field and accessor methods (
getX(), andsetX()only forvar), 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
valfor every new variable; only switch tovaronce you have a concrete reason the value must change. - Don’t confuse a read-only reference with an immutable object — a
valholding aMutableList,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 onvalto 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
varfor multiple unrelated purposes in the same function — prefer a fresh, well-namedvalfor each distinct piece of data. - In loops and accumulation, use
varfor the running value (a counter or total) but keep the loop’s input collection and final result asval.
Practice Exercises
- Declare a
valholding your name and avarholding your age. Print both in one string template, then write a line that increases the age by one and prints it again. - Create a
valreferencing amutableListOfof three integers. Without reassigning theval, 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 avarto track the running maximum inside a loop. Call it frommainwith a small list and print the result.
Summary
valcreates a read-only reference that can be assigned exactly once;varcreates 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.
valonly prevents rebinding the name — it does not make the underlying object immutable; mutable collections and objects can still change in place.- Reassigning a
valis 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/vargenerate getters (and, forvar, setters) automatically, replacing Java’s manual boilerplate. - Prefer
valby default and reach forvaronly when a value genuinely needs to change, such as an accumulator in a loop.
