Scala vals, vars, and immutability
Learn how Scala's vals, vars, and immutability differ from Python's mutable defaults, and why preferring val leads to safer, more functional code.
Focus: use vals, vars, and immutability
As a Python developer, you're used to rebinding names freely — x = 1 then x = 2 is no big deal. But Scala asks you to think twice before making a name mutable, and that shift feels awkward at first. This lesson shows you why Scala's val and var distinction is a gift, not a constraint, and how embracing immutability makes your code easier to reason about, test, and run in parallel — all without sacrificing the flexibility you love in Python.
The problem this lesson solves
In Python, every variable is mutable by default. You write count = 0, then later count += 1, and nothing stops you. That flexibility is convenient, but it's also the source of subtle bugs: a function might accidentally modify a variable that another part of your program relies on, or a shared list gets changed by an unexpected caller.
When you move to Scala, you'll see two keywords — val and var — and you might wonder why Scala bothers with both. The problem this lesson solves is that knowing when to use val vs var is the first step to writing idiomatic Scala. If you treat Scala like Python and use var everywhere, you lose many of the benefits of functional programming and make your code harder to parallelize and debug.
Immutability — the practice of never changing a value after it's created — is a core principle that Scala encourages. This lesson explains what immutability means, how to apply it with val, and when (rarely) you genuinely need a var. By the end, you'll not only be able to write val and var correctly, but you'll understand the why behind the choice.
Core concept / mental model
Think of a Python variable as a label you can move from one box to another. You can put a number in the box, then later replace it with a string — the label just points to whatever you last assigned.
In Scala, a val is like a label glued to a specific box. Once you stick it on, you cannot move it to a different box. The box's contents (the object) might still be mutable on the inside, but the binding itself is fixed.
A var is like a label with a peelable sticker — you can move it to another box later. But doing so comes at a cost: you now have to track which box it points to, and other code might see it change unexpectedly.
Here's a quick mental model:
| Python | Scala | Can you rebind? | Effect on safety |
|---|---|---|---|
x = 1 then x = 2 |
val x = 1 then x = 2 ❌ |
No | Compile-time error, safer |
| (same) | var x = 1 then x = 2 ✅ |
Yes | More flexible, but more risk |
Immutability extends this idea to the object itself. A val prevents rebinding, but if the object is mutable (like a Scala Array or a Java ArrayList), you can still change its contents. True immutability means the object cannot change at all — which is what Scala's default collections (List, Vector, Map, Set) give you.
🧠 Pro tip: Whenever you're unsure, reach for
valfirst. You can always change it tovarlater if you hit a situation that truly requires mutation. The compiler will guide you.
How it works step by step
Step 1: Declaring a val
A val is a value binding — it's read-only after initialization. You write:
val name: String = "Alice"
val age = 30 // type is inferred
Once you do this, name and age cannot be reassigned. Trying to write name = "Bob" will cause a compile error.
Step 2: Declaring a var
A var is a variable binding — you can reassign it later:
var count = 0
count = 1 // allowed
But notice: the type of count is fixed at Int. You cannot later assign a string to count because Scala is statically typed. This is different from Python's dynamic typing.
Step 3: Understanding immutable collections
Scala's default List is immutable. When you "add" an element, you actually get a new list:
val list = List(1, 2, 3)
val newList = list :+ 4 // prepend or append?
println(list) // List(1, 2, 3) — unchanged
println(newList) // List(1, 2, 3, 4)
If you need a mutable collection (rare), you can explicitly ask for one, e.g., ArrayBuffer from scala.collection.mutable.
Step 4: Compiler-enforced safety
The biggest win is that the compiler catches bugs before runtime. If you try to reassign a val, you get a clear error: reassignment to val. This is much better than discovering a stale variable in production.
Hands-on walkthrough
Let's put this into practice with a small exercise. Suppose you're writing a simple counter that processes a list of numbers and sums them.
Example 1: val can't be reassigned
val message = "Hello, Scala"
// message = "Goodbye" // ❌ compile error: reassignment to val
println(message)
Output:
Hello, Scala
Example 2: Using var and immutable collections together
Here's a typical pattern — you use a var to hold a mutable reference to an immutable structure:
var total = 0
val numbers = List(1, 2, 3, 4)
for (n <- numbers) { // Scala's for loop over a collection
total += n
}
println(s"Total: $total")
Output:
Total: 10
Notice how numbers is a val (its binding is fixed) but total is a var because we need to update it. The list itself never changes — we just read from it.
Example 3: Replacing var with pure functions
To really embrace immutability, you can avoid the var altogether by using a functional approach:
val numbers = List(1, 2, 3, 4)
val total = numbers.sum // or numbers.foldLeft(0)(_ + _)
println(s"Total: $total")
Output:
Total: 10
This version has no mutable state at all. It's easier to test and reason about.
Example 4: A word of caution — val doesn't make the object immutable
val arr = Array(1, 2, 3)
arr(0) = 99 // allowed! Array is mutable
println(arr.mkString(", "))
Output:
99, 2, 3
🧠 Pro tip: If you want a truly immutable array-like structure, use
Vectorinstead ofArrayfor most functional code.
Compare options / when to choose what
Now that you've seen val, var, and immutability in action, let's compare them side by side.
| Scenario | What to use | Why |
|---|---|---|
| Configuration value, constant, or fixed input | val |
It should never change. |
| Loop counter, accumulating result | var (or functional style) |
In a loop you need a mutable binding, but consider foldLeft instead. |
| Shared state across threads | immutable val + immutable collection |
Avoids race conditions. |
| Mutable data that grows/shrinks | val + ArrayBuffer (explicit) |
You get mutability, but you're making a deliberate choice. |
| Passing data to functions | immutable val |
Prevents accidental modification. |
Scala also offers advanced variations you'll meet later:
lazy val: Computed once on first access, then cached. Useful for expensive computations.final val: A compile-time constant (likefinalin Java).- Case classes produce immutable instances by default — you'll love them when you learn pattern matching.
🧠 Pro tip: In Scala, less state means less complexity. If you find yourself writing many
vars, step back and ask if you can restructure the problem with immutable data and pure functions.
Troubleshooting & edge cases
1. "reassignment to val" compile error
You see this when you try to assign to a val after initialization. Fix: either change it to var (if mutation is truly needed) or restructure your logic to avoid the reassignment.
2. Confusing rebinding with mutation
A val prevents rebinding, but if the object itself is mutable, you can still change its contents. Example:
val list = List(1, 2)
list = list :+ 3 // ❌ error: cannot reassign val
But:
val buffer = scala.collection.mutable.ArrayBuffer(1, 2)
buffer += 3 // ✅ allowed (mutation)
3. Forgetting that the type is fixed
You can't assign an Int to a var that was initialized with a String. This is a type error, not a mutability error.
4. Overusing var in loops
Python's for loop often has a counter. In Scala, you can usually avoid it with higher-order functions:
# Python
total = 0
for x in range(5):
total += x
// Scala — functional
total = (0 until 5).sum
5. Performance myths
You might worry that immutable collections are slow. In practice, Scala's immutable collections are efficient and share structure between versions. Premature mutation is rarely the right answer.
What you learned & what's next
Let's recap what you've mastered:
valcreates an immutable binding — you can't reassign the name, but the object may still be mutable inside.varallows reassignment — useful for mutable state, but use it sparingly.- Immutability is a design principle — it leads to safer, more predictable, and more concurrent-friendly code.
- You can connect immutability to the next lesson in this track, where you'll likely explore collections and higher-order functions that thrive on immutable data.
Remember: prefer val over var unless you have a compelling reason. This simple habit will make your Scala code cleaner and more professional.
Next up, you'll likely learn about collections and functions — where immutability really shines. You'll see how List, Vector, and Map let you transform data without mutating it.
🧠 Pro tip: Before you start your next exercise, challenge yourself: can you write it with zero
vars? If you can, you're thinking functionally!
Practice recap
Try this mini exercise: write a Scala function that takes a list of integers and returns the sum of the squares using only val and immutable collections (no var). Start with a for loop if you'd like, then refactor it to use map and sum. Test both versions to see the difference. This will solidify your understanding of immutability and functional style.
Common mistakes
- Treating
valas if it makes the object immutable. Avalonly prevents rebinding the name; the object it points to (e.g., anArray) can still be mutated. - Using
varwhen a functional approach works. Many loops can be replaced withfoldLeftormap, reducing mutable state. - Confusing Scala's static typing with Python's dynamic typing. Once you declare
var x = 1, you cannot later assign a string tox. - Trying to reassign a
valand getting a compile error instead of fixing the logic. The solution is often to restructure, not to switch tovar. - Assuming immutable collections are always slow. Scala's immutable structures are efficient and share memory, so don't switch to mutable collections without profiling.
Variations
- Use
lazy valfor a value that should be computed only when first accessed and then cached. - Use
final valfor compile-time constants, which can be inlined by the compiler. - Consider using immutable case classes to model data that never changes, paired with copy methods for updates.
Real-world use cases
- A configuration object loaded once at startup — use
valto ensure no part of the app can accidentally change it. - A financial transaction processor that sums a list of amounts without mutating the original list — using immutable collections for thread safety.
- A reactive stream handler that holds a
varfor the latest cursor position while keeping the rest of the state immutable, enabling safe, partial mutation.
Key takeaways
valcreates an immutable binding; once set, it cannot be reassigned.varallows reassignment but should be used sparingly; prefer functional transformations likesumorfoldLeft.- Immutability prevents bugs and makes code easier to parallelize and test.
- The Scala compiler enforces immutability at compile time, catching errors early.
valdoesn't mean the object is immutable; choose immutable collections likeListorVectorfor true immutability.- For most code, reach for
valfirst and considervaronly when you truly need mutable state.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.