skip to content

What does a Terraform for expression produce, and how do you write one that yields a map instead of a list?

level: middleimportance: must knowfreq 72%

answer

  1. it is a comprehension, not a loop
  2. brackets decide the result type
  3. arrow means you are building keys
  4. trailing if filters, trailing dots group
  5. sets bind exactly one symbol

basics

~20 s

A for expression transforms one collection into another. Square brackets produce a list-like tuple: [for s in var.names : upper(s)]. Curly braces with key => value produce a map-like object: {for s in var.names : s => upper(s)}. An optional if clause filters elements out.

solid answer

~50 s

A `for` expression is Terraform's comprehension: it walks a collection and builds a new value. The brackets decide the result type — `[for x in var.list : upper(x)]` gives a tuple (list-like), and `{for x in var.list : x => upper(x)}` gives an object (map-like), where the `=>` separates key from value. You can filter with a trailing `if`: `[for u in var.users : u.name if u.active]`. Iterating a map or object binds two symbols, `for k, v in var.map`; over a list, two symbols give index and value; over a set, only one symbol is allowed because a set has neither. The object form errors on duplicate keys unless you add `...` after the value to switch on grouping mode, which collects colliding keys into lists. This is the everyday tool for reshaping a variable into the exact structure a resource argument wants.

code

hcl · 18 lines
hcl
variable "users" {
  type = list(object({
    name   = string
    email  = string
    active = bool
  }))
}

locals {
  # tuple of names, filtered
  active_names = [for u in var.users : u.name if u.active]

  # object keyed by name
  by_name = { for u in var.users : u.name => u.email }

  # grouping mode: one key, many values
  emails_by_status = { for u in var.users : u.active => u.email... }
}

go deeper

for a junior

Be able to write both forms from memory and say what each returns: brackets give a list-like tuple, braces with key => value give a map-like object. Know that a trailing if filters elements.

for a middle

Explain the symbol binding rules across list, map and set, why duplicate keys are an error, and what the trailing ... grouping marker does. Be ready to reshape a list of objects into a map keyed by an id on a whiteboard.

for a senior

Demonstrate judgment about data shape: pick keys that stay stable as inputs change, know that map and set iteration is sorted so plans stay deterministic, and lift complex transformations out of resource arguments so a reviewer can follow them.

for a principal

Own where the transformation lives at all — reshaping in Terraform versus feeding the module a well-shaped input from the start. Deeply nested comprehensions are a signal that the module's interface is wrong, not that the language needs more cleverness.

## The shape of the expression A `for` expression has four parts: the brackets, the iteration clause, the result expression, and an optional filter. ```hcl [for s in var.names : upper(s)] # tuple ["A", "B"] {for s in var.names : s => upper(s)} # object {a = "A", b = "B"} [for s in var.names : upper(s) if s != ""] # filtered ``` The **brackets choose the result type**, and this is the part interviewers actually probe. `[ ]` produces a *tuple* — an ordered sequence, which converts freely to a list. `{ }` produces an *object* — an unordered key/value structure, which converts freely to a map. Because an object needs a key, the object form requires the `key => value` arrow; leaving it out inside braces is a syntax error. ## Binding symbols to the source collection What the temporary symbols mean depends on what you are iterating: ```hcl [for v in var.list : v] # each element [for i, v in var.list : "${i}-${v}"] # index and element [for k, v in var.map : "${k}=${v}"] # key and value [for v in var.set : upper(v)] # sets allow ONE symbol only ``` A set has no index and no key, so asking for two symbols over a set is an error. This matters because `toset()` shows up constantly in Terraform code, and the collection you are iterating may have quietly become a set. ## Keys must be unique — or you opt into grouping The object form converts every key to a string and refuses duplicates: *"Two different items produced the key ... in this 'for' expression"*. That error is common when you key by a non-unique attribute, for example grouping instances by their type. The fix is **grouping mode**, marked by `...` after the value expression: ```hcl {for inst in var.instances : inst.type => inst.name...} # => { "t3.micro" = ["web-a", "web-b"], "m5.large" = ["db"] } ``` Each key now maps to a *list* of all matching values instead of a single one. ## Ordering and determinism A tuple built from a list keeps the source order. A tuple built from a **map or set** comes out in lexical order of the keys/elements, because Terraform iterates maps and sets in sorted order — that is a deliberate design choice so that repeated plans are deterministic and diffs are stable. If you need a specific order that is not lexical, you have to encode it in the data, for example by sorting on a field with `sort()` or by keying on a zero-padded string. ## Nesting and chaining Result expressions can themselves be `for` expressions, which is how you flatten or pivot nested data: ```hcl # every (subnet, port) pair from a map of subnets and a list of ports locals { rules = flatten([ for name, cidr in var.subnets : [ for port in var.ports : { name = name, cidr = cidr, port = port } ] ]) } ``` `flatten()` collapses the list-of-lists into one list. This flatten-a-nested-comprehension shape is the canonical answer when someone asks how to produce a cross product of two inputs, and it is worth being able to write it from memory. ## for expression vs. the for_each meta-argument These are different things that share a word. A `for` expression is a *value* — it computes a collection anywhere an expression is allowed. It is one of the main ways people *build* the collection they later hand to a resource, but the expression itself creates nothing. Reshaping a list of objects into a map keyed by a stable identifier is a value-level transformation, and the for expression is the tool that does it. ## Type conversion at the edges A tuple is not literally a list and an object is not literally a map — they are the structural types Terraform infers, where each element may have its own type. Terraform converts automatically wherever the target's type constraint demands it, and `tolist()`, `toset()` and `tomap()` force the issue. The conversion can fail when elements have genuinely different types: `tomap()` over an object whose values are a mix of strings and numbers will unify to string if it can, and error if it cannot. When a variable is declared with an exact type constraint, that constraint is what the for expression's output must satisfy. ## Readability A three-level nested comprehension inside a resource argument is technically valid and practically unreadable. Lifting the transformation out into a named intermediate value and giving it a name that says what it produces is the difference between code a reviewer can approve and code they cannot.

  • Your for expression fails with "Two different items produced the key". What is happening and how do you fix it?
    The object form requires unique keys and you are keying on an attribute that repeats. Either key on something genuinely unique — an id, or a composite string built from two fields — or switch to grouping mode by appending `...` to the value expression, which maps each key to a list of all matching values instead of a single one.
  • How would you build a cross product of two input collections in Terraform?
    Nest one `for` expression inside another's result expression and wrap the whole thing in `flatten()`: the outer loop yields a list per outer element, and flatten collapses those into a single list of objects. Give the result a name; a nested comprehension inline in a resource argument is unreadable.
  • If you iterate a map with a for expression, what order do the results come out in?
    Lexical order of the keys. Terraform iterates maps and sets in sorted key order deliberately, so that a re-run produces the same result and the plan diff stays stable. If you need a different order you have to encode it in the data itself — sort on a field, or pad numeric keys so string sorting matches numeric order.

saying these in an interview costs you the question

  • Thinks square brackets and braces are interchangeable styling
  • Uses two symbols to iterate a set
  • Assumes map iteration follows insertion order
  • Keys the object form on a non-unique attribute and blames Terraform
  • Believes a for expression creates resources by itself

context