Introduction

In PowerShell, we have two types that at first glance look quite similar: Hashtable and PSCustomObject. Although both support key/value pair structures, they are fundamentally different. In this post, I’ll explain the differences and provide guidance on when to use each type.

Structure and Purpose

A Hashtable is essentially a dictionary. A PSCustomObject, on the other hand, is an object. The key difference between a dictionary and an object is that a dictionary is a flexible, dynamic data structure for storing arbitrary key-value pairs, while an object typically has a predefined, structured set of properties and methods. All dictionaries are objects, but not all objects are dictionaries.

Okay, that still sounds a bit abstract. Let me try to make it clearer. Think of a dictionary like a real-world dictionary: you look up a word (the key) and get its definition (the value). An object, however, is more like something with specific characteristics. Think of a car. It has wheels, a steering wheel, an engine, and seats. This goes beyond just key-value pairs. And to make things even more interesting, the value in a hashtable can actually be an object.

With a hashtable, it’s easy to add or remove entries. With an object, that’s more difficult. An object has a fixed set of properties. You can add new properties to an object, but that’s not the standard way of working. For this reason, we often use hashtables for temporary data storage, while PSCustomObjects are used to represent structured data.

As we’ll see later, you’ll often use hashtables within your code, while PSCustomObjects are typically used as output from a function or cmdlet.

Syntax

Creating

How do we define both data types? This is where a significant overlap exists. We can create both types in (almost) the same way. Here’s an example:

## Hashtable
$hash = @{
    ServerName = "SRV01"
    Role = "Web Server"
}

## PSCustomObject
$obj = [PSCustomObject]@{
    ServerName = "SRV01"
    Role = "Web Server"
}

The key difference lies in casting to a PSCustomObject. This turns the hashtable into an object.

Accessing Data

When retrieving data, we also see a clear difference. PSCustomObject uses dot notation, which feels more natural and intuitive to many people.

## Hashtable
$hash["ServerName"]  # Output: SRV01

## PSCustomObject
$obj.ServerName  # Output: SRV01

Adding Entries

Adding a new key-value pair to a hashtable is straightforward:

$hash["IP"] = "8.8.8.8" 

But doing the same for a PSCustomObject requires a bit more work:

$obj | Add-Member -MemberType NoteProperty -Name 'IP' -Value '8.8.8.8'

It’s certainly possible, but it clearly shows that PSCustomObject isn’t designed for dynamically adding or removing values.

Output Differences

When we compare the output, we also see noticeable differences.

## Hashtable
$hash | Format-Table

Name                           Value
----                           -----
ServerName                     SRV01
IP                             8.8.8.8
Role                           Web Server


## PSCustomObject
$obj | Format-Table

ServerName Role       IP
---------- ----       --
SRV01      Web Server 8.8.8.8

What stands out here is that the order of properties in a hashtable can change, while in a PSCustomObject the order remains consistent.

Performance

Another difference is performance. This is especially relevant when working with large datasets, loops, or when creating or modifying many objects. To make the comparison clearer, I’ll break it down into four areas:

  • Lookup Time
  • Memory Usage
  • Iteration and Manipulation
  • Use in Pipelines and Cmdlets

Each of these aspects highlights how the two types behave under different conditions and helps determine which is better suited for your specific scenario.

Lookup Time

Hashtable: A hashtable is based on a hashing algorithm, which gives it a constant lookup time of O(1). This means that no matter how large the dataset is, access speed remains consistent. This advantage becomes especially noticeable when you’re frequently accessing data by key.

PSCustomObject: Internally, the data is stored in a dictionary-like structure. However, accessing values via dot notation is slightly slower than a direct hashtable lookup. The lookup time is still generally O(1), but with added overhead from the object structure.

Conclusion: Hashtables are faster when it comes to direct key-based access.

Memory Usage

Hashtable: Hashtables are memory-efficient, especially noticeable with dynamic and small datasets. They don’t carry metadata or type information like objects do.

PSCustomObject: Includes additional overhead: type information, methods, and metadata. With large numbers of objects, this can lead to increased memory usage.

Conclusion: Hashtables are lighter in terms of memory consumption.

Iteration and Manipulation

Hashtable: Iterating over keys/values is possible but requires a deeper understanding of how hashtables work. However, hashtables have a major advantage in terms of manipulation—they can be dynamically expanded at runtime.

PSCustomObject: Iterating over properties is simple and readable. However, the properties are mostly fixed after creation, making PSCustomObject less flexible.

Conclusion: PSCustomObject is more user-friendly, but hashtables are more flexible and easier to manipulate.

Use in Pipelines and Cmdlets

Hashtable: To use a hashtable in a pipeline, extra steps are often required. Due to its structure, it’s also less suitable for use with various cmdlets like Select-Object, Export-Csv, and Format-Table.

PSCustomObject: Optimized for use in pipelines. Cmdlets like Select-Object, Export-Csv, and Format-Table work very efficiently with PSCustomObject.

Conclusion: PSCustomObject is slower to create but more efficient in pipelines and workflows.

Practical Example

One situation where I like to use a hashtable is when I have a list of user GUIDs from, for example, a log file, and I want to quickly convert them to a UserPrincipalName (or to a DistinguishedName). Of course, I could run a separate query to Active Directory for each GUID, but that would quickly create a significant load on the servers. A better approach is to retrieve all users once and build a dictionary (hashtable) with the required data. Then, when I want to display the parsed log entries on screen, I can use a PSCustomObject to present the data in a clean and readable format.

$AllUsers = Get-ADUser -filter * -SearchScope "OU=Users,...."

$UserDictionary = @{}
Foreach ($UserObject in $AllUSers) {
    $UserDictionary["$($UserObject.ObjectGuid.ToString())"] = $UserObject.UserPrincipalName
}

$LogFileLines = Get-Content "C:\Logs\VeryImportantLog.log"

Foreach ($Line in $LogFileLines) {
    ## Example Line: "20251031154952.856 | a67e710d-4073-4da5-b592-43f8bf0e69e3 | Install of very important program was started."
    
    $LineSplittedParts = $Line -split " | "
    $TimeStamp = $LineSplittedParts[0] 
    $GUID = $LineSplittedParts[1]
    $Message = $LineSplittedParts[2]

    $UserUPN = $UserDictionary["$GUID"]

    [PSCustomObject]@{
        Time = [datetime]::ParseExact($TimeStamp,"yyyyMMddHHmmss.fff",$null)
        UserPrincipalName = $UserUPN
        Message = $Message
    }
}

## Example Output:
## Time                UserPrincipalName    Message
## ----                -----------------    -------
## 31/10/2025 15:49:52 user001@mydomain.org Install of very important program was started.
## 31/10/2025 15:49:54 user002@mydomain.org User entered the wrong password.

Summary

While Hashtable and PSCustomObject may look similar at first glance, they serve different purposes in PowerShell. Hashtables are ideal for fast, dynamic data access and manipulation, especially in internal logic. PSCustomObjects, on the other hand, are better suited for structured, readable output and integration with cmdlets and pipelines. Choosing the right type depends on your specific use case, performance, flexibility, and readability all play a role.

Refer to the table below for a quick overview.

FeaturePSCustomObjectHashtable
Dot notation
Suitable for output
Dynamically expandable
Lookup speed
Object-based

Leave a Reply

Your email address will not be published. Required fields are marked *