Swift Immutability First: Why You Should Prefer let Over var
In Swift, the default habit should be to make values immutable with let and only use var when mutation is truly needed. This rule makes code easier to reason about, reduces bugs, and helps you see which values are meant to change.
Quick answer: Use let whenever a value does not need to change after it is created. Use var only for values that must be reassigned later, such as counters, caches, or state that genuinely changes.
Difficulty: Beginner
You'll understand this better if you know: basic Swift variables, assignment, and how simple values like String and Int are stored.
1. What Is Immutability First?
Immutability first means you start by declaring values as constants with let. A constant can be assigned once, then it stays fixed for the rest of its scope. That makes the code easier to trust because the value cannot change unexpectedly later.
- let creates a value that cannot be reassigned.
- var creates a value that can be changed after initialization.
- Immutability is a default mindset, not a strict rule that forbids all mutation.
- The goal is to minimize change until change is actually required.
This idea is especially important in Swift because the language is designed to encourage safety, clarity, and predictable behavior.
2. Why Immutability First Matters
Using let by default helps you write code that is easier to debug and safer to refactor. When a value cannot change, you can rule out a whole category of bugs caused by accidental reassignment.
It also makes intent clearer. Another developer reading your code can immediately tell whether a value is meant to be stable or evolving. That clarity matters in small functions, large codebases, and especially in code that is shared across a team.
In practice, immutability first is useful when you want to:
- protect values from accidental changes
- make functions easier to understand
- reduce side effects
- spot logic mistakes earlier through compiler errors
- keep data flow more predictable
There are cases where var is the right tool, but it should usually be the exception rather than the starting point.
3. Basic Syntax or Core Idea
The core difference is simple: let defines a constant, and var defines a variable. The compiler enforces that difference.
Constant with let
Use let when the value should not change after it is assigned.
let appName = "Trail Tracker"
let maxRetries = 3Both values are fixed after creation. If you try to assign a new value later, Swift will stop you at compile time.
Variable with var
Use var when a value really needs to change.
var score = 0
score = score + 10Here, mutation is intentional because the score changes over time.
4. Step-by-Step Examples
Example 1: A fixed greeting
A greeting string does not need to change, so let is the right choice.
let greeting = "Hello, Swift!"
print(greeting)This example shows the simplest case: if the value is final, declare it as a constant.
Example 2: Building a derived value
Even if you calculate a value in steps, you can often still keep the result immutable once it is complete.
let basePrice = 50
let taxRate = 0.2
let finalPrice = basePrice + (basePrice * taxRate)
print(finalPrice)The computed result does not need to remain mutable just because it was derived from other values.
Example 3: A changing counter
When the value changes repeatedly, use var.
var attempts = 0
attempts = attempts + 1
attempts = attempts + 1This is a valid use of mutation because the counter’s purpose is to change.
Example 4: Reassigning only when needed
Sometimes a variable starts out mutable because you do not yet know the final value, but you can often narrow the mutable portion to a small part of the code.
let rawName = " Ada "
let cleanName = rawName.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
print(cleanName)Instead of mutating one value over and over, create a new constant for each stage of transformation. That keeps each step easy to inspect.
5. Practical Use Cases
Immutability first is especially useful in real Swift code such as:
- configuration values like API endpoints, theme names, and feature flags
- intermediate results in data transformation pipelines
- function parameters that should not be changed inside the function
- return values that represent a finished result
- IDs, dates, labels, and other values that should remain stable
- local helper values in parsing, formatting, and validation code
As a practical rule, ask whether the value represents state or fact. Facts usually belong to let. State that evolves over time may need var.
6. Common Mistakes
Mistake 1: Using var by default for everything
Many beginners declare every local value with var out of habit. That works, but it hides intent and makes accidental mutation easier.
Problem: The code allows values to change even when they should be fixed, which can lead to bugs that are harder to trace later.
var username = "maria"
var country = "DE"
var accountID = 7421Fix: Use let for values that do not need to change.
let username = "maria"
let country = "DE"
let accountID = 7421The corrected version makes the programmer’s intent explicit and prevents accidental reassignment.
Mistake 2: Trying to reassign a constant
If a value is declared with let, Swift will not let you change it later. This is good, but it can surprise beginners who expect a value to be editable.
Problem: Swift reports an error such as Cannot assign to value: 'name' is a 'let' constant because constants cannot be reassigned.
let name = "Lina"
name = "Mina"Fix: Use var only if the value must change.
var name = "Lina"
name = "Mina"The fixed version works because the variable is designed for reassignment.
Mistake 3: Rebuilding a value with unnecessary mutation
Sometimes developers create a mutable variable, change it several times, and only then use the final result. In many cases, the same logic can be expressed with constants and a single final expression.
Problem: Excessive mutation makes it harder to see which version of the value is important and can introduce mistakes if one step is forgotten.
var fullName = "Ada Lovelace"
fullName = fullName.trimmingCharacters(in: CharacterSet.whitespaces)
fullName = fullName.uppercased()Fix: Create new constants for each stage of transformation.
let rawFullName = "Ada Lovelace"
let trimmedFullName = rawFullName.trimmingCharacters(in: CharacterSet.whitespaces)
let displayName = trimmedFullName.uppercased()The corrected version keeps each transformation step clear and avoids accidental overwrite of the final value.
7. Best Practices
Practice 1: Default to let, then relax only when needed
Starting with let forces you to justify mutation. That usually leads to cleaner code because you only introduce var when the design actually requires it.
let pageTitle = "Settings"
let sectionCount = 4Use mutation as an intentional design choice, not a default habit.
Practice 2: Keep mutable scope as small as possible
When you do need var, limit it to the smallest scope that requires change. That reduces the chance that unrelated code will alter it.
func makeMessage(name: String) -> String {
var message = "Hello, " + name
message += "!"
return message
}This is better than making a wider-scope variable mutable for no reason.
Practice 3: Prefer transforming into new constants
Instead of changing one value repeatedly, create a new constant for each meaningful stage. This is especially useful for strings, arrays, and parsed data.
let input = " swift "
let trimmed = input.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let uppercased = trimmed.uppercased()This style makes the pipeline easier to read and debug.
8. Limitations and Edge Cases
- let prevents reassignment of the binding, but it does not always make every underlying piece of data deeply immutable.
- Some reference types can still have internal state that changes even when the reference itself is constant.
- You may need var for counters, accumulators, and stateful operations such as building a result over time.
- Changing a stored property inside a let instance of a value type is not allowed because the whole instance is constant.
- Mutable collections can still be used in limited ways if the collection itself is declared with var.
Note: A constant binding is not the same thing as a deep freeze of every object graph. In Swift, the exact behavior depends on whether you are using value types or reference types.
9. Practical Mini Project
Here is a small example that processes a purchase total. The code uses let for stable facts and var only for the one value that must change as discounts are applied.
let itemPrice = 120
let shippingFee = 8
let discountRate = 0.15
var subtotal = itemPrice + shippingFee
let discountAmount = Double(subtotal) * discountRate
subtotal = Int(Double(subtotal) - discountAmount)
print("Final total: \(subtotal)")This example shows a practical balance: constants for fixed values, a variable for the one changing intermediate total, and a final output that is easy to follow.
10. Key Points
- Prefer let when the value should stay the same.
- Use var only when reassignment is necessary.
- Immutable values make intent clearer and reduce accidental bugs.
- Swift will catch attempts to mutate a constant at compile time.
- You can still write flexible code without making everything mutable.
11. Practice Exercise
Try rewriting a small piece of Swift code so that every value uses let unless mutation is truly required.
- Create a greeting string from a first name and last name.
- Trim extra whitespace from the full name.
- Format the result for display.
- Only use var if you cannot express the result with new constants.
Expected output: a clean display name such as "Ada Lovelace" or "ADA LOVELACE", depending on your formatting choice.
Hint: Build the result in stages with separate constants instead of changing one value repeatedly.
let firstName = " Ada"
let lastName = "Lovelace "
let fullName = firstName + " " + lastName
let trimmedName = fullName.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let displayName = trimmedName.uppercased()
print(displayName)12. Final Summary
Swift’s immutability-first style is simple: prefer let and reach for var only when you need mutation. This habit makes your code safer, clearer, and easier to maintain because the compiler helps enforce your intent.
In everyday Swift code, most values are facts rather than state, so they belong in constants. When you do need changing data, keep the mutable scope small and make the reason for mutation obvious. That balance gives you the readability benefits of immutability without limiting real-world flexibility.
As a next step, practice converting a few functions in your own codebase from var-heavy style to let-first style and watch how much simpler the logic becomes.