To loop through a CSV file in PowerShell, read it with Import-Csv and use a foreach loop. Each row becomes an object, and each column header becomes a property:
$customers = Import-Csv -Path C:\psfaqs\customers.csv
foreach ($customer in $customers) {
"$($customer.Name) from $($customer.State) spent `$$($customer.Total)"
}
Output:
Alice Johnson from TX spent $1250
Bob Smith from CO spent $980
Carol Davis from WA spent $15000
Dan Miller from FL spent $425

The file customers.csv has the columns Id, Name, Company, State and Total, with four customers. I use it in every example below.
I ran each script in PowerShell 7.6 and Windows PowerShell 5.1. The results matched except for one case with single-row files, which I show below.
foreach vs ForEach-Object with Import-Csv
You can also pipe Import-Csv straight into ForEach-Object, where $_ is the current row:
Import-Csv -Path C:\psfaqs\customers.csv | ForEach-Object {
'{0}. {1} ({2})' -f $_.Id, $_.Name, $_.Company
}
Output:
1001. Alice Johnson (Acme, Inc.)
1002. Bob Smith (Contoso)
1003. Carol Davis (Fabrikam)
1004. Dan Miller (Northwind)
| foreach ($row in $data) | Import-Csv | ForEach-Object | |
|---|---|---|
| Reads the file | All rows into memory first | One row at a time |
| Current row | Your loop variable | $_ |
| Skip a row | continue | return |
| Best for | Small and medium files you’ll reuse | Large files you read once |
For most CSV files, pick whichever reads better to you. On a file with millions of rows, the pipeline keeps memory use low because it never holds the whole file. ForEach-Object has more on the pipeline version.
Import a CSV file into an array
Import-Csv already returns an array, so you can count rows, pick one by index or grab a whole column:
$customers = Import-Csv -Path C:\psfaqs\customers.csv
"Type: $($customers.GetType().Name)"
"Rows: $($customers.Count)"
"First: $($customers[0].Name)"
"Last: $($customers[-1].Name)"
"Names: $($customers.Name -join ', ')"
Output:
Type: Object[]
Rows: 4
First: Alice Johnson
Last: Dan Miller
Names: Alice Johnson, Bob Smith, Carol Davis, Dan Miller
$customers.Name collects the Name of every row. It’s the quickest way to get a single column as a list.
Why .Count is empty for a one-row CSV
If the file has only one data row, you get a single object, not an array. In Windows PowerShell 5.1, .Count on that object returns nothing:
$one = Import-Csv -Path C:\psfaqs\one-customer.csv
"Without @(): $($one.GetType().Name), Count = [$($one.Count)]"
$one = @(Import-Csv -Path C:\psfaqs\one-customer.csv)
"With @(): $($one.GetType().Name), Count = [$($one.Count)]"
Output in Windows PowerShell 5.1:
Without @(): PSCustomObject, Count = []
With @(): Object[], Count = [1]
PowerShell 7 returns 1 in both cases, but wrapping the call in @() keeps your script safe in both versions. I do it whenever a file might have a single row.
Why don’t numbers from a CSV sort correctly?
Every value from Import-Csv is a string. Sorting by Total compares text, so 15000 lands before 425:
$customers = Import-Csv -Path C:\psfaqs\customers.csv
"Sorted as text: $(($customers | Sort-Object -Property Total).Total -join ', ')"
"Sorted as numbers: $(($customers | Sort-Object -Property { [int]$_.Total }).Total -join ', ')"
"Total spent: `$$(($customers | Measure-Object -Property Total -Sum).Sum)"
Output:
Sorted as text: 1250, 15000, 425, 980
Sorted as numbers: 425, 980, 1250, 15000
Total spent: $17655

Cast the value with [int], or [decimal] for do