skip to content

A Terraform variable can be typed `list(string)`, `set(string)` or `map(string)`. How do the three differ, and what does the module receive if a `set(string)` variable is given the value `["b", "a", "a"]`?

level: middleimportance: must knowfreq 62%

answer

  1. ordered, unique, keyed
  2. brackets make a tuple first
  3. duplicates vanish silently
  4. no indexing into a set
  5. order matters means list

basics

~20 s

A list is ordered and allows duplicates, a set is unordered and de-duplicated, a map is keyed by strings. Given ["b", "a", "a"], a set(string) variable yields two elements — "a" and "b" — with the caller's ordering and the duplicate discarded.

solid answer

~50 s

All three are collection types, so every element must share one type; they differ in how elements are addressed. `list(string)` is ordered and indexed by position, so `var.x[0]` is meaningful and duplicates survive. `set(string)` has no ordering and no duplicates — you cannot index into it, only iterate or convert it. `map(string)` addresses values by arbitrary string keys. The literal `["b", "a", "a"]` is really a *tuple*, which Terraform converts to the declared constraint: as `list(string)` it stays three elements in the given order, but as `set(string)` the repeated `"a"` collapses and the module sees exactly two elements with no caller-defined order. That conversion is silent — no warning that a duplicate vanished. So the choice of collection type is an interface decision: accept a set when order is meaningless and repeats are a caller mistake, a list when position matters, and a map when each entry needs a stable name.

code

hcl · 17 lines
hcl
variable "as_list" {
  type    = list(string)
  default = ["b", "a", "a"]
}

variable "as_set" {
  type    = set(string)
  default = ["b", "a", "a"]
}

output "list_len" {
  value = length(var.as_list) # 3
}

output "set_len" {
  value = length(var.as_set) # 2
}

go deeper

for a junior

Know the one-line difference: list is ordered with duplicates, set is unordered and unique, map is keyed by strings. Say which one tags naturally are.

for a middle

Explain that a bracketed literal is a tuple which Terraform converts to the declared constraint, and that the conversion to a set silently drops duplicates and ordering.

for a senior

Show the operational consequence of the choice: keyed collections keep an entry's identity stable across edits, while positional ones make the identity depend on where someone inserted a line.

for a principal

Frame the collection type as a contract you cannot change quietly — moving a published module's input from list to map rewrites how every consumer addresses its entries, so it belongs behind a major version.

## Three ways to hold many values `list(T)`, `set(T)` and `map(T)` are Terraform's collection types. What they share is the constraint that *every element has the same type* `T` — that is what separates them from `object` and `tuple`. What differs is how you address an element. - **`list(T)`** — ordered, indexed by integer position, duplicates allowed. `var.subnets[0]` is a meaningful expression. - **`set(T)`** — unordered, no duplicates, no indexing. You can iterate it or convert it, but `var.subnets[0]` is not valid on a set. - **`map(T)`** — keyed by arbitrary strings, no duplicate keys, no ordering guarantee you should rely on. `var.tags["Name"]` is how you reach a value. ## What actually happens to `["b", "a", "a"]` In HCL, a bracketed literal is not a list — it is a *tuple*, a fixed-length structural value with a type per position. Terraform then converts it to whatever the variable's constraint asks for: - Declared `list(string)`: three elements, order preserved, both `"a"`s intact. - Declared `set(string)`: the two `"a"`s are the same value, so the set has two members, `"a"` and `"b"`. The caller's ordering is gone. - Declared `map(string)`: conversion fails — a tuple has no keys. The de-duplication is silent. There is no warning that the caller wrote three entries and the module got two. If a repeated entry represents a real intent — two identical ingress rules, two copies of a value — a set is the wrong constraint. ```hcl variable "names" { type = set(string) } output "count_of_names" { value = length(var.names) # 2, given ["b", "a", "a"] } ``` ## Ordering is not part of a set's contract This trips people up when they convert back. `tolist(var.names)` on a set of strings does produce a deterministic result, and for strings it comes out in lexical order — but the caller's original ordering is not recoverable, and no code should assume the input order survived a round trip through a set. If order carries meaning (priority, sequence, position in a rule list), that fact alone decides the type: use a list. ## Maps and the shape of a real interface A `map(string)` is the natural constraint for tags, where each entry has a name and there is no ordering. It is also the natural constraint for "a named collection of things": `map(object({cidr = string, public = bool}))` gives every entry a stable key that the caller chose, rather than a position that shifts when someone edits the middle of a list. That stability is exactly why keyed collections are preferred for anything long-lived. ## Conversion between the three Terraform will convert a list to a set implicitly when the constraint asks for one, and there are explicit functions — `toset`, `tolist`, `tomap` — for doing it in an expression. Going list → set discards duplicates and order; going set → list invents an order. Converting a map to a list is not automatic, because keys have nowhere to go; you would use `values(var.m)` and accept that you have thrown the keys away. All of these conversions also require the element type to hold: a `set(number)` fed `["1", "2"]` succeeds because each string converts to a number, while `["one"]` fails. ## Choosing one as a module author The decision is about the caller's intent, not about convenience: - Does position mean something? → `list`. - Are repeats meaningless or a mistake, and is order irrelevant? → `set`. - Does each entry need a name that the caller controls and that stays stable across edits? → `map`. Making that choice deliberately is one of the small things that separates a module people can rely on from one whose behaviour depends on how a value happened to be written.

  • Why can you not write var.my_set[0] when the variable is typed set(string)?
    Because a set has no positions. Indexing requires an ordering to index into, and a set's contract is membership only. If you need an element by position you must convert first with `tolist(...)`, which forces you to acknowledge that the order you get is Terraform's, not the caller's.
  • A caller passes [1, 2, 3] to a variable typed set(string). Does it work?
    Yes. Each number converts to a string unambiguously, so the module receives the set of `"1"`, `"2"`, `"3"`. Terraform applies the element conversion before checking the collection. It would fail the other way round — `["one"]` cannot convert to `set(number)`.
  • When would you prefer map(object({...})) over list(object({...})) for a module input?
    When each entry needs a stable, caller-chosen name. With a list, an entry's identity is its position, so inserting or removing an item in the middle shifts everything after it. A map keys each entry by a name the caller controls, so edits to one entry leave the others untouched.

saying these in an interview costs you the question

  • Thinking a set preserves the order it was written in
  • Indexing a set with var.x[0]
  • Assuming duplicates in a set raise an error rather than collapsing
  • Believing lists and sets are interchangeable in a module interface
  • Claiming collections can mix element types

context