PoppyOS

Home › Guides

How to compare two CSV files and see exactly what changed

You have last month's export and this month's export, and you need to know what is different: which rows are new, which are gone, and which values changed. Excel can do it with some effort, a script does it in a few lines, and a purpose-built tool does it with a click. This guide shows the script route in full (it is short), the PowerShell route for Windows, and the spreadsheet route in outline, with what each can and cannot see.

First decide how rows are matched

"Different" only means something once you say which row in the old file corresponds to which row in the new file. Almost every real comparison needs a key column, such as an order number or customer ID. Comparing line by line (what a plain text diff does) reports rows as changed as soon as the file is re-sorted or one row is inserted at the top. With a key, a re-sorted file with one changed price produces exactly one difference.

Python, no extra packages

This uses only Python's standard library. It reads both files, matches rows by the id column, and reports added rows, removed rows and changed cells. It also warns if an ID appears twice in one file, because a repeated key would otherwise silently overwrite the earlier row.

import csv

def load(path, key):
    rows = {}
    with open(path, newline="", encoding="utf-8-sig") as f:
        for row in csv.DictReader(f):
            if row[key] in rows:
                print(f"warning: {key} {row[key]} appears more than once in {path}")
            rows[row[key]] = row
    return rows

old, new = load("old.csv", "id"), load("new.csv", "id")

print("added:  ", [k for k in new if k not in old])
print("removed:", [k for k in old if k not in new])
for k in old:
    if k in new:
        for col, before in old[k].items():
            after = new[k].get(col)
            if after != before:
                print(f"id {k}: {col} changed from {before!r} to {after!r}")
added:   ['5']
removed: ['3']
id 2: price changed from '14.50' to '15.50'

Using utf-8-sig means a file saved by Excel with a byte-order mark is read correctly, instead of giving a first column name with an invisible character in front of it (id would no longer equal "id"). Everything is compared as text, so 14.50 and 14.5 count as different. If you want them to be equal, convert numeric columns with float() before comparing.

Windows PowerShell

Compare-Object compares whole rows. A row that exists on only one side shows <= (old file) or => (new file); a changed row shows up as one of each.

$old = Import-Csv old.csv
$new = Import-Csv new.csv
Compare-Object $old $new -Property id,name,price |
  Sort-Object id,SideIndicator |
  ForEach-Object { "{0} {1} {2} {3}" -f $_.SideIndicator, $_.id, $_.name, $_.price }
<= 2 Gadget 14.50
=> 2 Gadget 15.50
<= 3 Gizmo 4.00
=> 5 Thingamajig 7.25

Reading the output: id 2 appears on both sides with different prices (a change), id 3 only on the old side (removed), id 5 only on the new side (added). PowerShell does not tell you which column changed; you work that out by eye, which gets tedious past a few columns.

In a spreadsheet

The usual approach is to put both files on separate sheets and use a lookup formula (VLOOKUP, or XLOOKUP in recent versions of Excel and in Google Sheets) to pull the old value next to the new one, then a column that tests whether they are equal. This is fine for a few thousand rows and one or two columns. It gets slow and error-prone with many columns, it needs a second pass to find removed rows, and spreadsheets can silently change values when they open a CSV (see leading zeros). We have not timed spreadsheet formulas ourselves, so treat that as general experience, not a measurement.

What to watch for, whichever way you do it

Last updated 2026-10-27. Code on this page was run on that date and the output shown is the real output. Spreadsheet menu steps are written from Microsoft's and Google's documented behaviour; where we have not run something ourselves, the page says so.

More guides

All guides