Chapter 5: Iteration and Conditionals
Topic 32

for Expressions

HCL

A for expression transforms one collection into another — mapping, filtering, and reshaping lists and maps inline. [for s in var.names : upper(s)] builds a new list; { for k, v in var.map : k => v.cidr } builds a map. This is HCL's functional data-transformation tool, and it is how you derive the structures that feed for_each, dynamic blocks, and outputs without an imperative loop.

The bracket you open decides the output type: square brackets produce a list or tuple, curly braces produce an object or map. Everything else — filtering, key reshaping, grouping — is a variation on those two forms.

List and Map Comprehensions

[for ...] yields a list; {for ...} yields a map and requires a key => value arrow. Iterating a list gives you one variable (the element); iterating a map gives you two (key and value). The expression after the colon is evaluated for every element to build the result.

list out, map out
# List: uppercase every name
locals {
  upper_names = [for n in var.names : upper(n)]
}

# Map: name => its CIDR, from a map of objects
locals {
  cidrs = { for k, v in var.subnets : k => v.cidr }
}

Filtering with if

An optional if clause at the end keeps only the elements that match, so you filter inline instead of post-processing with extra functions. The condition runs per element and drops the ones that evaluate false — building exactly the subset you want in a single pass.

keep only the public subnets
locals {
  public_ids = {
    for k, v in var.subnets : k => v.id
    if v.public
  }
}

Transforming Keys and Values

Because a {for} expression lets you write any key and any value, you can re-key a collection entirely — turning a list of objects into a map keyed by one of their fields, the classic move for feeding for_each. { for s in var.servers : s.name => s } takes a list and produces a map keyed by each server's name, giving every element the stable key for_each demands.

Grouping Mode

Adding ... after the value switches a {for} into grouping mode: instead of one value per key, it collects every value that maps to a key into a list under that key. This is how you bucket items — grouping subnets by availability zone, or rules by protocol — without a manual merge.

group subnet names by availability zone
locals {
  by_az = {
    for k, v in var.subnets : v.az => k...
  }
  # { "us-east-1a" = ["public"], "us-east-1b" = ["private"] }
}

Feeding for_each

for_each needs a map or set with stable keys, and a for expression is how you build it from whatever shape your input arrives in. Take a list of objects, project it into a map keyed by a name field, and hand that straight to for_each. Building the map up front — rather than passing a list and converting awkwardly downstream — keeps the keys explicit and the plan readable.

Common Mistakes
  • Producing a map with duplicate keys from a for expression, silently collapsing multiple entries into one without an error.
  • Building a list when for_each needs a map or set, then converting awkwardly downstream instead of keying the data up front.
  • Writing one deeply nested for expression that would read far clearer as a named local plus a second expression.
  • Forgetting the if clause and filtering after the fact with extra functions, doing in two passes what one would do.
  • Reaching for grouping mode (...) when you actually wanted one value per key, getting lists where you expected scalars.
Best Practices
  • Use for expressions to shape data into exactly the structure for_each or a dynamic block needs.
  • Filter inline with the if clause rather than post-processing the result.
  • Break a complicated comprehension into named locals so each step reads on its own.
  • Build maps, not lists, when the result feeds for_each — the keys become the resource identities.
  • Reach for grouping mode (...) only when you genuinely want multiple values bucketed under one key.
Comparable tools Pulumi host-language map / filter / comprehensions CloudFormation no equivalent transformation Terraform for expressions are HCL's functional reshaping tool

Knowledge Check

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

  • The brackets: [for ...] yields a list, {for ...} yields a map
  • Whether the input is a list or a map — the output always matches the input
  • A type argument you pass to the expression
  • It is always a tuple; you convert it afterward

What does the optional if clause in a for expression do?

  • Includes only the elements for which the condition is true, filtering inline
  • Branches the entire expression between two distinct output collections, like a ternary
  • Stops iteration early at the first false element
  • Marks the result as conditional so it is recomputed each apply

What does the ... grouping mode in a {for} expression produce?

  • A map where each key holds a list of every value that mapped to it
  • A single flattened list of all the values, dropping the keys entirely
  • A set with duplicates removed
  • The last value for each key, overwriting earlier ones

Why reshape a list of objects into a map before passing it to for_each?

  • for_each needs stable keys, and a map keyed by a name field gives each instance one
  • Lists are not valid inputs to any iteration expression anywhere in HCL
  • Maps are indexed internally, so they apply measurably faster than equivalent lists during the plan phase
  • It is required only when there are more than ten elements

You got correct