Chapter 6: Iteration and Conditionals
Topic 38

for Expressions

Expressions

A for expression transforms one collection into another inside an expression — distinct from for_each, which stamps out resources. You write [for s in var.sources : upper(s)] to reshape a list, or {for k, v in var.map : k => v.region} to reshape a map, and an optional if clause filters as it goes. This is how you turn a raw input variable into exactly the map a for_each wants.

The pairing matters. for_each wants a clean keyed map, but the input you actually receive from a variable is often a list of objects, or a map that needs filtering, or a nested structure that has to be flattened. The for expression is the reshaping tool that sits between the messy input and the tidy collection the resource consumes. Keep the two straight: for builds values, for_each builds resources.

List and Map Comprehensions

The bracket style decides the output type, not the input. [for x in coll : expr] with square brackets produces a list; {for k, v in coll : k => expr} with braces produces a map. You can iterate a map and emit a list, or iterate a list and emit a map — the output collection is whatever the bracket says, and the => in the brace form is what names each key.

List in, keyed map out
# a list of source objects from a variable...
variable "sources" {
  type = list(object({ name = string, location = string }))
}

# ...projected into the map for_each consumes
locals {
  source_map = { for s in var.sources : s.name => s }
}

Iterating Maps and Lists

Over a list you get each element in turn; over a map you get k, v pairs. When position matters on a list, the form for i, x in list gives you the index alongside the element. The iteration variables are yours to name — s, k, v, i are conventions, not keywords — and the expression after the colon can reference any of them plus anything else in scope.

Filtering with if

A trailing if clause drops elements as the expression runs. [for s in var.sources : s.name if s.enabled] keeps only the enabled sources, computing the filter inline instead of in a separate local. The filter runs before the key/value projection, so an element that fails the if never reaches the output at all — it is not emitted with a null, it is simply absent.

Filter and project in one expression
locals {
  enabled_topics = {
    for s in var.sources : s.name => s
    if s.enabled
  }
}

Building a Map for for_each

This is the workhorse pattern of the whole chapter: take a list of source objects from a variable and project it into {for s in var.sources : s.name => s}, producing the keyed map that for_each consumes. List in, keyed map out. The resource block then sets for_each = local.source_map and reads each.value for each source's settings. The variable stays a friendly list for whoever fills it in, and the for expression adapts it to the shape Terraform's iteration needs.

Build that map in a named local, not inline in the for_each argument. A multi-step transform wedged into the resource block is unreadable, and the named local gives the plan and the diff a stable, legible reference. The key you project on must be unique — which is exactly the property that makes it a good for_each key — so choose a field that cannot collide across entries.

Grouping and Nesting

Two advanced forms cover the harder reshapes. The ... grouping form collects values under shared keys — group event types by tenant, for instance, so each tenant key maps to a list of its event types rather than overwriting on collision. And for expressions nest: an outer for over tenants and an inner for over each tenant's events flattens or pivots a nested input into the flat map a resource needs. These appear the moment your input variable is more nested than the resource's flat collection.

Common Mistakes
  • Using [...] brackets when you meant a map and getting a list back, or the reverse — the bracket type, not the input, determines the output collection.
  • Projecting a list to a map keyed on a non-unique field, so two source rows collide on the same key and one is silently dropped.
  • Writing a heavy multi-step transform inline in a for_each argument where it is unreadable — build the map in a named local so the plan and the diff are legible.
  • Forgetting the if clause filters before the key/value projection, then being surprised that a filtered-out element never reaches the map at all.
  • Reaching for a for expression to create resources — it only transforms values; resource fan-out is for_each or count, and confusing the two is a beginner trap.
  • Building a deeply nested transform in one expression where a grouping form or a chain of named locals would read clearly — cleverness here costs the next reader an hour.
Best Practices
  • Build the for_each map in a named local with a for expression, keyed on a stable unique field, rather than inlining the transform.
  • Use the if clause to filter collections in one expression instead of pre-filtering in extra locals.
  • Pick [...] versus {...} deliberately based on whether the consumer wants a list or a keyed map.
  • Keep each for expression to one clear transformation; chain through named locals when the reshape has multiple steps.
  • Reach for the grouping ... form when keys repeat, so collisions become lists instead of silently dropping entries.
Comparable approaches Pulumi Python or TypeScript list and dict comprehensions Deployment Manager Jinja loops jq map and select as the closest CLI analog gcloud no direct equivalent, this is an HCL language feature

Knowledge Check

What determines whether a for expression returns a list or a map?

  • The bracket style — [...] yields a list, {...} with => yields a map — regardless of the input type
  • The input type — iterating a list always yields a list, while iterating a map always yields a map back out
  • Whether a trailing if filter clause is present in the expression
  • The number of iteration variables you declare before the colon

When does the if clause run relative to the key/value projection?

  • Before — a filtered-out element is never emitted, so it never reaches the output collection
  • After — the element is projected into the output first, then nulled out afterward by the filter clause
  • In parallel — projection and filtering are independent passes
  • Only on map-producing expressions, never on list-producing ones

Why must the key you project on when building a map be unique?

  • Two entries sharing a key collide, and one is silently dropped from the resulting map
  • Non-unique keys quietly cause the expression to fall back to returning a list instead of a map
  • Terraform sorts the map by key and duplicates break the sort
  • Maps require integer keys, which are always unique

How does a for expression differ from for_each?

  • A for expression transforms values into a new collection; for_each stamps out resource instances
  • They are fully interchangeable syntaxes for the exact same underlying iteration and expansion operation
  • A for expression stamps out resource instances, while for_each only reshapes data
  • for works on maps, for_each works only on lists

You got correct