RizTech Academy logo
RizTech Academy
Language BasicsLesson 1 of 520 min

val and var: immutability by default

The very first choice you make on every line that stores something is val or var. It looks trivial. It is one of the most consequential habits in the language, because it decides whether a value can change under you later — and "a value changed when I did not expect it to" is behind a large share of real bugs.

Two keywords

val city = "Pune"      // read-only: cannot be reassigned
var count = 0          // mutable: can be reassigned
  • val (from value) declares a read-only reference. Once set, you cannot point it at something else.
  • var (from variable) declares a mutable reference. You can reassign it as often as you like.

Try to reassign a val and the compiler stops you before the program ever runs:

val city = "Pune"
city = "Mumbai"        // error: val cannot be reassigned
error: val cannot be reassigned

That is not an inconvenience — it is the compiler catching, at compile time, a category of mistake that in other languages you only discover at runtime.

The rule: val by default

Reach for val every time. Use var only when you have a specific reason to reassign. This is idiomatic Kotlin, and it is the opposite of the habit many people bring from languages where everything is mutable by default.

Why it matters, concretely:

  • A val cannot surprise you. When you read val total = a + b, you know total is that, for the rest of its life. With var, you have to scan every line below to check whether something reassigned it. Immutability is readability: it shrinks how much you have to hold in your head.
  • It prevents a whole class of bug. A value you did not mean to change, changed by code far away, is a bug that is miserable to find. If it is a val, that bug is impossible.
  • It signals intent. val says "this does not change"; var says "this is expected to". A reader learns your intentions from the keyword alone.

The instinct to fight is "but I might need to change it later". You almost never do, and if you do, changing val to var is a one-word edit the compiler will demand anyway. Start with val; let the compiler tell you when you genuinely need var.

What val does and does not promise

Here is the subtlety that catches people, and it is worth getting straight now. val means the reference cannot be reassigned. It does not mean the object cannot change.

val list = mutableListOf(1, 2, 3)
list.add(4)                 // fine! the list changed
println(list)               // [1, 2, 3, 4]
list = mutableListOf(5)     // error: val cannot be reassigned

list is a val, so you cannot point it at a different list. But the list it points at is itself mutable, so add works. The val locks the box, not the contents.

This is why "immutable" has two layers in Kotlin, and the collections module returns to it: a read-only reference (val) is one thing; a genuinely immutable object (like a listOf(...), which has no add) is another. For now, hold the distinction: val stops reassignment, not mutation.

Type inference — and when to be explicit

Notice none of the examples wrote a type. Kotlin infers it from the value:

val city = "Pune"      // inferred String
val count = 0          // inferred Int
val price = 199.99     // inferred Double
val active = true      // inferred Boolean

You can write the type, and sometimes should:

val city: String = "Pune"
val total: Long = 0        // you want a Long, not the inferred Int

Let Kotlin infer the type when it is obvious from the value; state it when it is not, or when the inferred type is not the one you want. A function's return type and a public property are places where an explicit type aids the reader even when inference would work — but for an ordinary local val, inference keeps the code clean.

One thing inference cannot do: figure out the type with nothing to infer from. This is an error:

val name        // error: this variable must either have a type annotation or be initialized

Either initialise it, or annotate it (and initialise it later, which is a var situation or a class-property situation you will meet in the classes module).

const — a compile-time constant, briefly

You will see a third form:

const val MAX_RETRIES = 3

const val is a val whose value is known at compile time — a fixed number or string, not the result of a function call. It is used for true constants (top level or in a companion object, which the classes module covers). For now: it is a stricter val, and if const gives an error ("const val has no meaning"), you wanted a plain val.

The habit, stated once

Every time you type var, pause for one second and ask: does this genuinely need to change? Most of the time the honest answer is no, and it becomes a val. That one-second habit, applied thousands of times, is a large part of what makes code readable and correct. It is the smallest good habit in the language and one of the highest-value.

Check your work

val versus var. val is a read-only reference (cannot be reassigned); var is mutable.

When the error appears for reassigning a val. At compile time — before the program runs.

The default to reach for, and why. val — it cannot surprise you, prevents a class of bug, and signals intent; use var only with a specific reason.

What val does not promise. That the object cannot change — only the reference. A val mutableListOf can still be added to.

The two layers of immutability. A read-only reference (val) versus a genuinely immutable object (like listOf).

What type inference does. Deduces the type from the value, so you need not write it.

When to write the type explicitly. When it is not obvious, when the inferred type is not what you want, and for public/return types as a courtesy to the reader.

Why val name alone is an error. Nothing to infer from — it must be typed or initialised.

What const val adds. A value known at compile time — a true constant.

Practice

  1. Declare a val and try to reassign it. Read the exact compiler error.
  2. Change it to var and confirm the reassignment now works.
  3. Create a val pointing at a mutableListOf(1, 2, 3), add(4) to it, and confirm it changed — then try to reassign the val itself and watch that fail.
  4. Declare five vals of different types with no annotation and confirm inference gets each right (hover in the IDE, or print and reason).
  5. Declare val total: Long = 0 and a val n = 0, and check (in the IDE) that their types differ.
  6. Write val name with no value and read the error. Then fix it two ways: annotate, and initialise.
  7. Go through any code you have and count var versus val. For each var, decide honestly whether it needed to be one.

Official documentation

Next: the basic types, and the inference you just met, in full.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship