PowerShell Hashtables and Dictionaries: The Complete Guide

A PowerShell dictionary, usually called a hashtable, stores key and value pairs so you can look up a value by its key. Create one with @{ key = value } and read it with $table['key']:

$capitals = @{
    Texas    = 'Austin'
    Colorado = 'Denver'
    Florida  = 'Tallahassee'
}

"Capital of Texas: $($capitals['Texas'])"
"Same with dot notation: $($capitals.Colorado)"
"Number of entries: $($capitals.Count)"

Output:

Capital of Texas: Austin
Same with dot notation: Denver
Number of entries: 3

Hashtables are PowerShell’s built-in dictionary, and some older posts call them associative arrays. I ran every example in PowerShell 7.6 and Windows PowerShell 5.1 and point out where they differ.

Add, update and remove entries

Assigning to a key adds it if it’s new and updates it if it exists. Add() only adds, and fails if the key is already there:

$stock = @{ Laptops = 12; Monitors = 30 }

$stock['Keyboards'] = 45      # adds a new key
$stock['Laptops']   = 10      # updates an existing key
$stock.Remove('Monitors')

try { $stock.Add('Laptops', 99) } catch { 'Add() failed. Laptops is already a key' }
$stock.GetEnumerator() | Sort-Object -Property Name | ForEach-Object { '{0} = {1}' -f $_.Name, $_.Value }

Output:

Add() failed. Laptops is already a key
Keyboards = 45
Laptops = 10

I use the bracket form almost everywhere, since it never fails. Remove() is also safe when the key doesn’t exist.

Missing keys and case

Reading a key that doesn’t exist returns $null instead of an error, so check with ContainsKey when it matters. Keys ignore case by default:

$capitals = @{ Texas = 'Austin' }

"Missing key returns: [$($capitals['Ohio'])]"
"ContainsKey('Ohio'):  $($capitals.ContainsKey('Ohio'))"
"Keys ignore case:     $($capitals['TEXAS'])"

Output:

Missing key returns: []
ContainsKey('Ohio'):  False
Keys ignore case:     Austin

Number keys and text keys are different

The key 101 and the key ‘101’ aren’t the same. This bites when the lookup value comes from a CSV file or Read-Host, which are always text:

$rooms = @{ 101 = 'Sales'; 102 = 'Finance' }

"Number key 101:  [$($rooms[101])]"
"Text key '101':  [$($rooms['101'])]"
"Cast first:      [$($rooms[[int]'101'])]"

Output:

Number key 101:  [Sales]
Text key '101':  []
Cast first:      [Sales]

Keep keys in order with [ordered]

A plain hashtable doesn’t keep your keys in the order you typed them. Add [ordered] to keep that order:

$plain   = @{ First = 1; Second = 2; Third = 3; Fourth = 4 }
$ordered = [ordered]@{ First = 1; Second = 2; Third = 3; Fourth = 4 }

"@{} order:        $($plain.Keys -join ', ')"
"[ordered] order:  $($ordered.Keys -join ', ')"

Output in Windows PowerShell 5.1:

@{} order:        Third, First, Second, Fourth
[ordered] order:  First, Second, Third, Fourth
PowerShell hashtable key order vs ordered dictionary
@{} shuffles the keys, [ordered] keeps them (Windows PowerShell 5.1)

In PowerShell 7, the @{} order was different every time I ran the script, because .NET randomizes string hash codes per process. Use [ordered] whenever order matters, like report columns.

Loop through a hashtable

Loop over .Keys, or use GetEnumerator() to get Name and Value pairs you can sort and filter:

$sales = [ordered]@{ Austin = 18250; Denver = 12400; Miami = 20975 }

foreach ($city in $sales.Keys) {
    '{0,-7} {1,10:C0}' -f $city, $sales[$city]
}

$sales.GetEnumerator() | Sort-Object -Property Value -Descending |
    Select-Object -First 1 | ForEach-Object { "Top city: $($_.Name)" }

Output:

Austin     $18,250
Denver     $12,400
Miami      $20,975
Top city: Miami

Each item from GetEnumerator() has a Name (the key) and a Value. foreach vs ForEach-Object covers the two loop styles.

Don’t change a hashtable while looping over its keys

Updating values inside a loop over $table.Keys fails, because the key collection changes under the loop:

$prices = @{ Coffee = 3.50; Bagel = 2.75; Muffin = 3.25 }

try {
    foreach ($item in $prices.Keys) { $prices[$item] += 0.25 }
}
catch { "Failed. $($_.Exception.Message)" }

Output:

Failed. Collection was modified; enumeration operation may not execute.

Copy the keys into an array first with @(), and the same loop works:

$prices = @{ Coffee = 3.50; Bagel = 2.75; Muffin = 3.25 }

foreach ($item in @($prices.Keys)) { $prices[$item] += 0.25 }
'Coffee now costs {0:C}' -f $prices['Coffee']

Output:

Coffee now costs $3.75

Hashtables in the pipeline

A hashtable goes down the pipeline as one object, not one item per entry. Use GetEnumerator() to send the entries one at a time:

$capitals = @{ Texas = 'Austin'; Colorado = 'Denver'; Florida = 'Tallahassee' }

"Piped to Measure-Object: $(($capitals | Measure-Object).Count)"
"Using GetEnumerator():   $(($capitals.GetEnumerator() | Measure-Object).Count)"
"Values with an 'a':      $(($capitals.GetEnumerator() | Where-Object Value -like '*a*' | Sort-Object Name).Name -join ', ')"

Output:

Piped to Measure-Object: 1
Using GetEnumerator():   3
Values with an 'a':      Florida, Texas
PowerShell hashtable in the pipeline counts as one object without GetEnumerator
Measure-Object sees 1 object until you call GetEnumerator() (PowerShell 7)

The same applies to Where-Object, Sort-Object and ForEach-Object. Without GetEnumerator(), they get the whole table at once.

Find a key or value in a hashtable

ContainsKey and ContainsValue answer yes or no. To find which key holds a value, or to search with wildcards, filter the entries:

$phoneBook = @{ 'Alice Johnson' = '512-555-0142'; 'Bob Smith' = '303-555-0187'; 'Carol Diaz' = '512-555-0199' }

"ContainsValue('303-555-0187'): $($phoneBook.ContainsValue('303-555-0187'))"
"Key for 303-555-0187: $(($phoneBook.GetEnumerator() | Where-Object Value -eq '303-555-0187').Name)"
"Austin (512) numbers: $(($phoneBook.GetEnumerator() | Where-Object Value -like '512-*' | Sort-Object Name).Name -join ', ')"

Output:

ContainsValue('303-555-0187'): True
Key for 303-555-0187: Bob Smith
Austin (512) numbers: Alice Johnson, Carol Diaz

ContainsValue is exact and case-sensitive, while -like and -eq in Where-Object ignore case. Checking arrays with -contains covers the array side.

An array of hashtables

A list of hashtables is a quick way to hold records, but convert each one to [pscustomobject] before you export or format it. Windows PowerShell 5.1 exports the hashtable’s own properties instead of your keys:

$people = @(
    [ordered]@{ Name = 'Alice Johnson'; State = 'TX' }
    [ordered]@{ Name = 'Bob Smith';     State = 'CO' }
)
$people | Export-Csv -Path C:\psfaqs\Hash\hashtables.csv -NoTypeInformation
"Hashtables give these columns: $((Get-Content -Path C:\psfaqs\Hash\hashtables.csv -TotalCount 1))"

$people | ForEach-Object { [pscustomobject]$_ } | Export-Csv -Path C:\psfaqs\Hash\objects.csv -NoTypeInformation
"Objects give these columns:    $((Get-Content -Path C:\psfaqs\Hash\objects.csv -TotalCount 1))"

Output in PowerShell 7:

Hashtables give these columns: "Name","State"
Objects give these columns:    "Name","State"

Output in Windows PowerShell 5.1:

Hashtables give these columns: "Count","IsReadOnly","Keys","Values","IsFixedSize","SyncRoot","IsSynchronized"
Objects give these columns:    "Name","State"
Windows PowerShell 5.1 Export-Csv writes hashtable properties instead of keys
5.1 wrote IsReadOnly, Keys and Values as columns (Windows PowerShell 5.1)

PowerShell 7 exports the keys, but converting to objects works in both and also gives you tidy Format-Table output.

Convert an array to a hashtable

To look items up by an ID, loop once and store each item under its key. Group-Object can build the table for you:

$employees = @(
    [pscustomobject]@{ Id = 'E100'; Name = 'Alice Johnson' }
    [pscustomobject]@{ Id = 'E101'; Name = 'Bob Smith' }
    [pscustomobject]@{ Id = 'E102'; Name = 'Carol Diaz' }
)

$byId = @{}
foreach ($e in $employees) { $byId[$e.Id] = $e.Name }
"E101 is $($byId['E101'])"

$grouped = $employees | Group-Object -Property Id -AsHashTable -AsString
"Group-Object value type: $($grouped['E101'].GetType().Name), name: $($grouped['E101'].Name)"

Output:

E101 is Bob Smith
Group-Object value type: Collection`1, name: Bob Smith

With Group-Object, each value is a collection of the matching objects, not the object itself. Add -AsString so keys are plain text, or lookups by string can fail.

Hashtable vs array: when to use which

Use an array for an ordered list you walk through. Use a hashtable when you look things up by a key, because a lookup doesn’t scan every item:

$ids   = 1..100000 | ForEach-Object { "EMP$_" }
$index = @{}
foreach ($id in $ids) { $index[$id] = $true }

$arrayTime = (Measure-Command { 1..200 | ForEach-Object { $ids -contains 'EMP99999' } }).TotalMilliseconds
$hashTime  = (Measure-Command { 1..200 | ForEach-Object { $index.ContainsKey('EMP99999') } }).TotalMilliseconds
"Hashtable lookups were more than 10 times faster: $($arrayTime / $hashTime -gt 10)"

Output:

Hashtable lookups were more than 10 times faster: True

In my runs the gap was about 50 times in PowerShell 7.6 and about 300 times in 5.1, with 100,000 items. For a few dozen items, either is fine.

Use a typed .NET Dictionary

[System.Collections.Generic.Dictionary[string, int]] enforces the key and value types. Unlike @{}, its keys are case-sensitive unless you pass a comparer:

$scores = [System.Collections.Generic.Dictionary[string, int]]::new()
$scores['Alice'] = 92

"Exact case 'Alice': [$($scores['Alice'])]"
"Other case 'ALICE': ContainsKey = $($scores.ContainsKey('ALICE'))"

$ci = [System.Collections.Generic.Dictionary[string, int]]::new([StringComparer]::OrdinalIgnoreCase)
$ci['Alice'] = 92
"With OrdinalIgnoreCase: ContainsKey('ALICE') = $($ci.ContainsKey('ALICE'))"

try { $scores['Bob'] = 'high' } catch { "A Dictionary[string, int] won't take 'high' as a value" }

Output:

Exact case 'Alice': [92]
Other case 'ALICE': ContainsKey = False
With OrdinalIgnoreCase: ContainsKey('ALICE') = True
A Dictionary[string, int] won't take 'high' as a value

I reach for a typed Dictionary in larger scripts where a wrong value type should fail right away. The Dictionary class reference lists its methods, like TryGetValue.

Pass parameters with splatting

A hashtable can also hold a command’s parameters. Put @ instead of $ before the name to pass them:

$params = @{
    Path    = 'C:\psfaqs\Hash'
    Filter  = '*.csv'
    File    = $true
}
(Get-ChildItem @params).Name

Output:

q3-sales.csv

Splatting keeps long commands readable and lets you add parameters conditionally. Microsoft explains it in about_Splatting, and the hashtable deep dive and about_Hash_Tables cover the rest.

Frequently Asked Questions

How do I create a hashtable in PowerShell?

Use @{ Key1 = 'Value1'; Key2 = 'Value2' }. For an empty one, use @{}, and add keys with $table['Key'] = 'Value'.

Is a PowerShell hashtable the same as a dictionary?

It’s PowerShell’s built-in dictionary type. You can also use a .NET Dictionary, which enforces key and value types and has case-sensitive keys by default.

How do I keep the order of keys in a hashtable?

Use [ordered]@{ ... }. A plain @{} doesn’t keep insertion order, and in PowerShell 7 its order can change between runs.

How do I loop through a hashtable in PowerShell?

Use foreach ($key in $table.Keys), or $table.GetEnumerator() to get Name and Value pairs you can sort and filter.

How do I check if a key exists in a hashtable?

Use $table.ContainsKey('Key'). Use ContainsValue to check values.

More collection guides worth reading next: