Hashtables, Ordered Dictionaries, Lookup Tables, and Enumeration
Use key/value dictionaries for configuration and lookup state, enumerate entries correctly, preserve order intentionally, and mutate collections without invalidating active enumeration.
Learning objectives
- Explain key/value storage and choose it when named lookup is more meaningful than numeric array indexing.
- Create, read, update, remove, and test hashtable keys using the appropriate properties and methods.
- Explain why a hashtable is one pipeline object and use GetEnumerator() for entry-by-entry processing.
- Use [ordered] dictionaries only when insertion order is intentionally part of the data contract.
- Build environment-to-server lookup tables that fail explicitly for unknown keys.
- Avoid modifying a dictionary structurally while its live enumerator is active.
1. Hashtables replace “position 0” with meaningful keys
An array answers “what is element 0?” A hashtable answers “what value is associated with this key?” A key is a unique lookup label, and a value is the data stored under that label. This is useful when configuration naturally has names such as Region, ApiUrl, or Retries.
$config = @{
Region = 'eu-west'
ApiUrl = 'https://api.example.invalid'
Retries = 3
}
$config['Region']
$config.ApiUrl
$config.CountPowerShell hashtables are System.Collections.Hashtable objects. Their keys and values can be many .NET types, although strings are common for configuration keys.
2. Add, update, remove, and test keys deliberately
Hashtable assignment uses the same square-bracket lookup syntax for both reading and writing. Assigning a new key adds it; assigning an existing key replaces its value. ContainsKey() lets you distinguish “this key exists” from “this value happens to be null”.
$config = @{ Region = 'eu-west'; Retries = 3 }
$config['TimeoutSeconds'] = 30
$config['Retries'] = 5
$config.ContainsKey('Region')
$config.ContainsKey('MissingKey')
$config.Remove('TimeoutSeconds')
$config.CountRemove() returns a Boolean indicating whether a key was actually removed. In automation, this can be useful when validating a transformation, but do not rely on incidental method output when you do not need it.
3. Keys and Values expose views of the dictionary
The Keys and Values properties expose the key and value collections. They are useful for inspection, but remember that an ordinary hashtable does not guarantee a stable presentation order.
$config = @{
Environment = 'staging'
Region = 'eu-west'
Replicas = 3
}
$config.Keys
$config.Values
"Entries: $($config.Count)"If the logic depends on a particular sequence, encode that requirement explicitly instead of hoping the ordinary hashtable happens to enumerate in the same order on every run.
4. A hashtable is one dictionary object; GetEnumerator() exposes its entries
Arrays naturally enumerate their elements into the pipeline. A hashtable behaves differently: the hashtable itself is the object entering the pipeline. To process each key/value pair as a separate object, call GetEnumerator(). Each emitted entry has Key and Value properties.
$config = @{ dev = 'dev-01'; stage = 'stage-01'; prod = 'prod-01' }
$config | ForEach-Object {
"Pipeline type: $($_.GetType().FullName)"
}
$config.GetEnumerator() |
Sort-Object Key |
ForEach-Object { "{0} -> {1}" -f $_.Key, $_.Value }This distinction matters because commands such as Where-Object, Sort-Object, and ForEach-Object can only operate on individual key/value entries after you enumerate them.
5. [ordered] dictionaries make insertion order part of the data contract
An ordinary hashtable is optimized for key lookup, not presentation order. When predictable insertion order matters—for example, producing a review-friendly configuration summary—you can create an ordered dictionary with the [ordered] accelerator.
$release = [ordered]@{
Application = 'catalog'
Version = '2.4.0'
Environment = 'staging'
Region = 'eu-west'
}
$release.GetType().FullName
$release.KeysAn ordered dictionary is a different .NET type from Hashtable. Choose it because ordering is meaningful, not because it looks nicer in one terminal.
6. Lookup tables turn environment names into explicit configuration
A hashtable is especially effective when a short identifier maps to a larger value. Instead of a long if/elseif chain, a lookup table can make the mapping visible and testable.
$serverByEnvironment = @{
dev = 'dev-api-01'
staging = 'stage-api-01'
production = 'prod-api-01'
}
$requestedEnvironment = 'staging'
if (-not $serverByEnvironment.ContainsKey($requestedEnvironment)) {
throw "Unknown environment: $requestedEnvironment"
}
$server = $serverByEnvironment[$requestedEnvironment]
"Target server: $server"The key check makes failure explicit. Without it, a missing key would usually produce $null, and the script could continue with an invalid target.
7. Mutating a hashtable while enumerating it invalidates the enumerator
An enumerator tracks a collection while you walk through it. If the collection changes underneath that enumerator, the traversal can no longer make a reliable promise about what comes next. PowerShell therefore surfaces an error when you modify the hashtable during active enumeration.
$config = @{ a = 1; b = 2; c = 3 }
# Deliberately incorrect: this can throw because the collection
# is modified while its enumerator is active.
try {
foreach ($entry in $config.GetEnumerator()) {
if ($entry.Value -lt 3) {
$config.Remove($entry.Key) | Out-Null
}
}
}
catch {
$_.Exception.Message
}The lesson is not “never remove keys.” The lesson is to separate traversal from mutation. First decide what should change, then apply the changes using a stable snapshot.
8. Snapshot the keys or build a new hashtable before changing structure
A simple safe pattern is to copy the key list into an array before modifying the dictionary. Another pattern is to construct a new dictionary containing only the desired entries. The second pattern is often easier to reason about because it avoids in-place mutation.
$config = @{ a = 1; b = 2; c = 3 }
foreach ($key in @($config.Keys)) {
if ($config[$key] -lt 3) {
$config.Remove($key) | Out-Null
}
}
$config$source = @{ a = 1; b = 2; c = 3 }
$filtered = @{}
foreach ($entry in $source.GetEnumerator()) {
if ($entry.Value -ge 3) {
$filtered[$entry.Key] = $entry.Value
}
}
$filteredThe “build a new value” style is a useful preparation for production automation because intermediate state can be inspected and compared before the original configuration is replaced.
9. Hashtables are excellent configuration and lookup structures
Use hashtables for option maps, environment lookups, headers, parameter splats, cache keys, feature flags, and configuration fragments. A hashtable is not automatically a good report row, however. When the data describes one record with named properties that should flow through the pipeline, Lesson 03 will convert that shape into a PSCustomObject.
10. Diagnose hashtable mistakes by distinguishing missing keys from false-like values
A value of $false, 0, empty string, or $null may all have different configuration meaning. Testing only truthiness cannot tell you whether a key was absent. Use ContainsKey() when key existence itself matters.
$feature = @{
Enabled = $false
RetryCount = 0
Note = ''
OptionalValue = $null
}
foreach ($key in 'Enabled','RetryCount','Note','OptionalValue','Missing') {
[pscustomobject]@{
Key = $key
Exists = $feature.ContainsKey($key)
Value = $feature[$key]
}
}This is a common configuration-validation boundary: “not provided” and “provided with a false-like value” must not be silently collapsed into the same case.
11. Lab: build and validate an environment-to-server lookup
This lab creates an in-memory environment map, validates a requested environment, and emits a small reusable record rather than concatenating a status string.
$serverByEnvironment = [ordered]@{
dev = 'dev-api-01'
staging = 'stage-api-01'
production = 'prod-api-01'
}
$requested = 'staging'
if (-not $serverByEnvironment.Contains($requested)) {
throw "Unknown environment: $requested"
}
$selected = [pscustomobject]@{
Environment = $requested
Server = $serverByEnvironment[$requested]
AvailableEnvironments = @($serverByEnvironment.Keys)
}
$selected | Format-List
$serverByEnvironment.GetEnumerator() |
ForEach-Object { [pscustomobject]@{ Environment=$_.Key; Server=$_.Value } } |
Format-Table -AutoSizeFor OrderedDictionary, the key-test method is Contains(). For a normal Hashtable, Lesson 02 used ContainsKey(). That difference is a reminder to inspect object types and available methods rather than assuming every dictionary has the same API.
Verification checklist
12. Common mistakes to avoid
Piping a hashtable and expecting one object per entry. Call GetEnumerator() when you need entry objects.
Depending on ordinary hashtable display order. Use [ordered] or explicit sorting when order matters.
Testing only the retrieved value for missing-key detection. Use ContainsKey() or the appropriate dictionary method.
Changing keys while walking the live enumerator. Snapshot keys or build a new dictionary first.
13. Knowledge check
Question 1. What is the main conceptual difference between an array index and a hashtable key?
Question 2. Why does $hash | ForEach-Object not automatically process each key/value pair?
Question 3. When should you use [ordered]@{...}?
Question 4. Why is ContainsKey() safer than testing only $hash[$key]?
Question 5. How can you safely remove several keys?
14. Summary
Hashtables model named key/value state. Use bracket or property-style lookup, ContainsKey(), Keys, Values, Count, and GetEnumerator() intentionally. Ordinary hashtables do not promise insertion order; [ordered] dictionaries do. Because dictionary enumeration is a live traversal, separate structural mutation from enumeration.
15. Further reading
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.