Variables & Types

By the end of this lesson you can explain — and prove with real Dart output — what a variable actually is in memory, why Dart's type system stops whole classes of bugs before your program ever runs, when to use var, final, const, late and dynamic, and how to check and convert types safely.

1. What is a variable?

Think of a cloakroom. You hand in your coat and get a numbered tag ("A17"). The tag is not the coat — it's a label that currently points at your coat, hanging on a hook somewhere in the room. If you later swap your coat for a jacket and get a new tag pointing at the hook with the jacket, the coat you handed in earlier is still there — until the cloakroom clears it out because nobody's tag points to it anymore.

A variable is exactly that tag: a name that currently refers to a value living somewhere in the computer's memory. The name and the value are two different things. This matters because three different actions get confused by beginners:

A later x = 8; on its own is a reassignment: no new tag is created, the same tag "x" is simply moved to point at a different value. What happens to the old value depends on what kind of value it is — a plain number just gets overwritten in place, but an object (like a List) keeps existing on the heap until nothing points to it any more, at which point Dart's garbage collector reclaims the memory. (Two words used here, defined: the stack is the small, tidy desk of named boxes that belongs to the function currently running; the heap is the big shared storage room where objects such as Lists live, and the stack box then holds just an arrow — a reference — to the object. The garbage collector is Dart's cleaner that throws away heap objects no arrow points to any more.)

The same thing as runnable Dart — declaration, initialization, assignment, x = x + 1 and the reference-copy of a List (exact output shown in the comments, verified):

Input size → what's feasible: a declaration or assignment is O(1) at any scale; copying a variable that holds a List copies one reference (O(1)), never the n elements — so oldA = a costs the same for n = 2 or n = 106 items.

Beginners often think x = x + 1; is "impossible" ("how can x equal x plus one?"). It isn't an equation — = in Dart means "evaluate the right side first, then store the result into the box named on the left." Read it right-to-left.
A variable is a named reference to a value. Reassignment moves the name to point somewhere new — it does not change what the name "is".

2. Types: what they are, and Dart's core types

A type is like the label on a storage bin at a warehouse: "Bin only holds screws." If a courier tries to drop a bag of nails into the screws bin, the warehouse manager (the Dart compiler) refuses the delivery on the spot — before it ever reaches the shelf. This is static typing: Dart checks that every value matches its variable's declared type before your program runs, not while it's running. Combined with sound null safety (a variable that isn't declared nullable can never secretly hold null), this is called a sound type system — the compiler's promises about types are guarantees, not hints.

Two moments to keep apart: compile time is when Dart (or your editor's analyzer) reads and checks your code before anything runs; runtime is while the program is actually executing. A static type is the type the compiler knows from the code; the runtime type is the type of the actual object found in memory when the line executes.

Dart's everyday built-in types:

TypeHoldsExample literal
intwhole numbers (64-bit two's-complement — a binary format for representing negative numbers, detailed in the Numbers & Bits lesson — on native VM/AOT; behaves like a JS number on the web — this matters for very large integers)42
doubledecimal / floating-point numbers3.14
numthe supertype of int and double — use when either is finenum n = 5; n = 5.5;
Stringtext'hello'
booltrue / falsetrue
Listan ordered, indexable sequence of values (covered fully in a later lesson)[1, 2, 3]
Mapa set of key → value pairs{'a': 1}
Seta collection of unique values, no duplicates{1, 2, 3}

The same thing as runnable Dart — every core type from the table, the num parent type, and the type hierarchy checks (exact output shown in the comments, verified):

You can write the type explicitly, or let Dart figure it out with type inference using var:

The same thing as runnable Dart — var locks in the inferred type; an empty literal falls back to List<dynamic> (exact output shown in the comments, verified):

Input size → what's feasible: inference is resolved by the compiler before the program runs, so it costs nothing at runtime for any n.

var is not "no type" — Dart looks at the value on the right of = once, at the point of declaration, and locks the variable to that inferred type forever. var x = 1; makes x permanently an int — assigning a double to it later is a compile error, exactly as if you had written int x = 1;. An explicit annotation (int x = 1;) does the same thing, just spelled out — useful for public APIs and for readability when the right-hand side is complex.

Under the hood, var is purely a compile-time convenience. By the time your code is compiled to native or JS, there is no difference at all between a var-declared and an explicitly-typed variable — both carry the same fixed static type.

3. dynamic vs Object vs Object? — the danger zone

Imagine three delivery slots at a warehouse. Slot Object accepts any parcel, but before you're allowed to open it and use what's inside as (say) a "screwdriver", a manager makes you show ID proving it really is one (a cast). Slot Object? is the same, but the parcel might also just be an empty slip of paper (null) — you must check for that too. Slot dynamic has no manager at all: you can call any method on it you like, and nobody checks until the delivery actually happens — if you ask a bag of nails to "turn the screw", it fails right there on the loading dock, at runtime, in front of a customer.

dynamic switches off Dart's compile-time type checking for that variable entirely. The compiler will let you call absolutely any method on a dynamic value — typos included — and only discovers the mistake when that line actually executes.

The same thing as runnable Dart — dynamic crashes at runtime, Object is checked at compile time, Object? admits null (exact output shown in the comments, verified):

dynamic should be rare in application code. Prefer a specific type, Object (then cast with as after checking with is), or generics. Every dynamic you write is a promise to the compiler "don't check this for me" — and the compiler will hold you to it, even when you're wrong.
Object/Object? keep compile-time safety (you must prove the real type before using subtype-only members); dynamic throws that safety away and defers every mistake to a runtime NoSuchMethodError.

4. final vs const

final is like a nametag glued onto one specific minibus the moment you're handed it: that nametag can never be peeled off and stuck onto a different minibus — the name is locked to THAT vehicle forever. But nothing stops someone from getting inside and rearranging the seats (the minibus is a normal, mutable object; only which minibus the name points to is frozen). const is stricter: it's a minibus stamped out of a mould at the factory (compile time) with its seats welded in place — nobody can ever rearrange them. And if two people separately order "the const minibus with these exact seats", the factory hands them the very same physical minibus, not two identical-looking ones.

final: the variable's reference can be set exactly once, but that happens at runtime (it can depend on a function call, user input, etc.), and if it refers to a mutable object, the object's contents can still change.

const: the value must be knowable at compile time — no function calls, no runtime data — and the whole value is deeply immutable: a const list, its elements, and anything they point to are frozen. Dart also canonicalizes const values: two const literals with identical contents are stored as the exact same object in memory.

The same thing as runnable Dart — final vs const, what each allows, and identical on const vs non-const literals (exact output shown in the comments, verified):

For the complete mutable-vs-immutable story (unmodifiable views and copies, const constructors, copyWith, shallow vs deep copy) see D22 · Mutable vs Immutable.

"final list" does NOT mean "immutable list" — it only fixes the name, not the contents. If you need contents that can never change, use const (compile-time data) or wrap in List.unmodifiable(...) / UnmodifiableListView (runtime data that should stay frozen).
identical(a, b) checks whether a and b are the literal same object in memory (same address), not just equal contents. For const [1, 2] written twice, Dart's compiler notices the two literals are equal and hands out one shared canonical object — proven at the end of this section by real Dart output.
final = set once, at runtime, contents may still change. const = fixed at compile time, deeply frozen, and canonicalized so equal literals share one object.

5. late: doing work later, and only once

A restaurant doesn't grill your steak the moment you walk in — it waits until you actually order (lazy), and once it's grilled, it won't re-grill the same steak for you if you ask to see the plate again (only once, then cached). late tells Dart: "don't compute this until something actually reads it — and once you have, remember the answer."

late final x = expensiveWork(); means expensiveWork() does not run when the line executes — it runs the first time something reads x, and never again after that. This is also exactly how Dart handles top-level and static variables: they are lazily initialized on first use, not at program start, so an expensive top-level constant that's never used never costs you anything.

The same thing as runnable Dart — lazy initialization runs once on first read; reading a never-assigned late field throws (exact output shown in the comments, verified):

Input size → what's feasible: a late final costs one initializer call however many times you read it (1 read or 106 reads → 1 call), so make an expensive value late when it may never be needed.

If you declare late String name; with no initializer and something reads name before any code assigns it, Dart throws an error whose message reads LateInitializationError: Field 'name' has not been initialized. (the thrown object's class is LateError; for a late local variable the word is “Local” instead of “Field”, and if the compiler can see that nothing was assigned yet it refuses to compile the read at all). late is a promise to the compiler that you'll assign it before it's read; break that promise and you get a crash, not a silent null.
late defers work to first-read and caches the result. It trades a compile-time safety net for a runtime promise — use it when you're certain the value will be set before it's needed (e.g. via initState-style setup).

6. Scope: blocks, shadowing, top-level & static

Think of nested rooms in a house. A name you shout inside the kitchen (a block, marked by { }) is only heard inside that kitchen. If someone in the kitchen is also named "Sam", and there's a "Sam" in the living room, shouting "Sam!" in the kitchen only reaches the kitchen's Sam — the living-room Sam never even hears it. That's shadowing: an inner name with the same spelling as an outer one temporarily hides the outer one inside its own block, without changing it.

Scope is the region of code where a name is visible. A variable declared inside { } (a block — the body of an if, a loop, a function) only exists inside that block. A top-level variable (declared outside any class or function, directly in a file) is visible everywhere in that file (and importable elsewhere), and is lazily initialized the first time it's touched, exactly like a late variable — static variables (one shared copy per class, rather than per object — classes are covered in a later lesson) behave the same way.

The same thing as runnable Dart — block scope, shadowing, and lazily initialized top-level variables (exact output shown in the comments, verified):

Inner blocks can declare a same-named variable that shadows the outer one for the rest of that block only; the outer variable is untouched and reappears once the block ends.

7. Naming conventions & privacy

Dart's style is a dress code, not a rule enforced by the compiler (except privacy, which IS enforced) — but everyone recognises it instantly, like doctors wearing white coats and chefs wearing tall hats.

The same thing as runnable Dart — the naming conventions plus the compiler-enforced underscore privacy (exact output shown in the comments, verified):

Case conventions are style; the underscore prefix for privacy is a real, compiler-enforced language feature scoped to the file (library).

8. Checking and converting types

Before you use a mystery parcel as a screwdriver, you check the label (is), and only then do you actually use it as one (as). Converting a hand-written note ("42") into an actual number you can do maths with is a deliberate step (int.parse), not something that happens automatically — and going the other way, turning a number back into text for display, is toString().

The same thing as runnable Dart — is, as, int.parse/tryParse, toString and runtimeType (exact output shown in the comments, verified):

Input size → what's feasible: int.parse reads each character once: O(L) for an L-digit string, and an int holds at most 19 digits (−9223372036854775808 … 9223372036854775807) — longer text makes tryParse return null (use BigInt.parse beyond that).

Never call int.parse directly on untrusted input (user typing, network data) without a try/catch or using int.tryParse — a single bad string crashes the call unless handled.

Quiz

Interview questions

Cheat sheet

KeywordWhen fixedContents mutable?Canonicalized?
vartype inferred once at declarationdepends on the objectno
finalreference set once, at runtimeyes, if the object is mutableno
constvalue fixed at compile timeno — deeply frozenyes — equal literals share one object
latedeferred to first read (then cached)n/a (about timing, not mutability)no
dynamicnever checked at compile timen/ano