Variables & Constants

Swift separates what a value is from whether it can change. Every binding starts with either let (constant) or var (variable). The compiler uses that choice to enforce safety, so it is one of the first things to get right.

This lesson covers how to declare values, how Swift infers their type, when reassignment is legal, and how scope and shadowing decide which name you actually see.

Declaring Values

A declaration introduces a name, a type, and sometimes an initial value. The keyword in front of the name answers a single question: may this binding be reassigned after it is created?

Constants with let

let creates a constant: once initialised, the binding can never point to a different value. Prefer let whenever the value does not need to change — it documents intent and lets the optimiser work harder.

let pi = 3.14159            // type Double is inferred from the literal
let name = "Ada"            // type String
let isReady = true          // type Bool

// pi = 3.14                // ❌ compile error: cannot assign to a 'let' constant
print(pi, name, isReady)    // prints: 3.14159 Ada true

Variables with var

var creates a mutable binding. Use it only when the value must change during the lifetime of the program, such as a running total or a loop counter.

var score = 0               // mutable Int
score += 10                 // legal: var may be reassigned
score += 5
print(score)                // prints: 15

Note that "constant" refers to the binding, not to the contents of a value. A let array cannot be reassigned or resized, but its elements still obey their own rules — this matters once you study collections.

Type Inference & Annotation

Swift is statically typed, yet you rarely write the type. The compiler infers it from the initial value, which keeps code short without giving up type safety.

Explicit Type Annotation

Write the type after a colon when the value cannot be inferred, when you want a wider or narrower type than the literal suggests, or when readability demands it.

let ratio: Double = 1        // literal 1 would infer Int; annotation forces Double
let label: String = ""       // explicit type for an empty string
var population: Int64 = 0    // wider integer than the default Int

// let empty = []            // ❌ error: cannot infer the element type of []
let emptyNames: [String] = []   // annotation supplies the missing element type
print(ratio, label, population, emptyNames.count)

Multiple Bindings

Several names can be declared on one line, and a single annotation applies to every name on that line.

var x = 1, y = 2, z = 3      // three Int bindings in one declaration
var a: Double = 0, b: Double = 0   // one type for both names

x += 1
print(x, y, z, a, b)         // prints: 2 2 3 0.0 0.0

Mutability Rules

Mutability is decided at declaration time and checked by the compiler, not at runtime. This removes a whole class of bugs where a value changes behind your back.

Reassignment

A var may be assigned any number of times, but the new value must have the same type as the original declaration.

var temperature = 20         // inferred Int
temperature = 22             // fine: same type
// temperature = 22.5        // ❌ error: cannot assign Double to Int
print(temperature)           // prints: 22

Immutable by Choice

Choosing let is a design decision: it tells every reader and the compiler that the name is fixed. Compiler optimisations and thread-safety reasoning both benefit.

let finalPrice = 99.90       // will never change
// finalPrice = 89.90        // ❌ error: cannot assign to 'let' constant
print(finalPrice)            // prints: 99.9

Naming & Scope

The same identifier may exist in more than one scope. Understanding which declaration a name refers to prevents confusing bugs.

Naming Conventions

Values and functions use lowerCamelCase; types use UpperCamelCase. Names should describe purpose, not type: elapsedSeconds beats intVar.

let itemCount = 12           // purpose-first name, no type prefix
var totalPrice = 0.0         // reads like a sentence when used
print(itemCount, totalPrice)

Scope & Lifetime

A name is visible from its declaration to the end of the enclosing block. Local values are destroyed when the block exits; top-level and global values live for the whole program.

let global = "I live for the whole program"

func demo() {
    let local = "I exist only during this call"
    print(global, local)     // both are visible here
}
// print(local)              // ❌ error: 'local' is out of scope
demo()

Shadowing

An inner declaration may reuse an outer name, hiding the outer one until the inner block ends. This is occasionally useful (a converted value keeps the same name) but easy to overuse.

let score = 10

func adjusted() {
    let score = score * 2    // shadows the outer 'score' inside this function
    print(score)             // prints: 20
}
adjusted()
print(score)                 // prints: 10 — the outer value is untouched

Global & Computed Values

Not every stored value must be initialised up front. Swift offers lazy stored properties and computed properties so a value can be produced on demand.

Lazy Stored Properties

lazy postpones initialisation until the property is first read. It is ideal for expensive work that may never be needed, and it must be declared with var.

struct Report {
    lazy var summary: String = buildSummary()   // built only when first read

    func buildSummary() -> String {
        return "expensive report"               // pretend this is slow work
    }
}

var r = Report()             // summary is NOT built yet
print(r.summary)             // built now, then cached: expensive report

Computed Properties

A computed property stores no value of its own: it calculates the result each time it is read. Use it to expose a derived view of other stored data.

struct Rectangle {
    var width = 4.0
    var height = 3.0

    var area: Double {       // recomputed on every access
        return width * height
    }
}

let rect = Rectangle()
print(rect.area)             // prints: 12.0

Common Pitfalls

Unused Variable Warnings

Declaring a value you never use produces a compiler warning. If a value is intentionally discarded, replace the name with an underscore to silence the warning.

let _ = performSideEffect()  // underscore: result ignored on purpose

func performSideEffect() -> Int { return 0 }

Choosing let over var

Reaching for var by default is a habit worth breaking. Start with let; the compiler will tell you when you truly need mutability, and every let you keep is one fewer thing that can change unexpectedly.

let userId = 42              // never changes → let, not var
var sessionCount = 0          // genuinely changes → var is justified
sessionCount += 1
print(userId, sessionCount)  // prints: 42 1

With declarations, inference, mutability, and scope in hand, you can move on to the types those values can hold: continue with Data Types.