Clean CSV Headers

Header cleaning rewrites the first row of a CSV into one consistent convention. Order ID, Customer Name and Total (USD) become order_id, customer_name and total_usd. Duplicates are numbered, blanks are named, and every change is printed so you can check it. Nothing is uploaded.

Want to see what the columns hold before renaming them? Profile the file first.

What a real header row looks like

Nobody designs the header row of an export. It accumulates. A column gets added by someone who writes Order ID, another by someone who writes customer_name, a third by a report builder that emits Total (USD), and somewhere in the middle there is a column with no name at all because a formula was deleted and the cell above it was not. Twice, there is a column called notes.

Each of those is fine on its own and the combination is not, because the next thing that reads the file has to guess. A pandas read_csv gives you a frame where some columns are attribute-accessible and some are not. A database import fails on the brackets. A JSON conversion produces keys with spaces in them that every consumer then has to quote. And the two notes columns quietly become one, because most tools that turn rows into objects keep the last value for a repeated key.

This page settles all of it in one pass, and then tells you exactly what it did.

Worked example: six columns, five problems

Header row first, with the whitespace made visible by the quoting:

Order ID , Customer Name ,Total (USD),,notes,notes
1,Ada,10,x,a,b

Leave everything on its default and pick snake_case:

order_id,customer_name,total_usd,column_4,notes,notes_2
1,Ada,10,x,a,b

And the mapping printed above it:

6 columns
snake_case style
5 headers renamed
1 blank name filled in
1 duplicate numbered
Order ID  → order_id
 Customer Name  → customer_name
Total (USD) → total_usd
(blank) → column_4
notes → notes_2

Five things happened. The trailing space on Order ID was trimmed. The double space inside Customer Name collapsed, because the word splitter treats any run of separators as one boundary. The brackets in Total (USD) became a word boundary rather than surviving as total_(usd). The nameless column became column_4, numbered by position so it is findable. And the second notes became notes_2, so nothing is lost when the file becomes objects.

The nine styles, and which one to pick

Order ID  →  snake_case      order_id
             camelCase       orderId
             PascalCase      OrderId
             kebab-case      order-id
             CONSTANT_CASE   ORDER_ID
             Title Case      Order Id
             lowercase       order id
             UPPERCASE       ORDER ID
             leave the case  Order ID
  • snake_case for anything heading into Python, PostgreSQL, BigQuery or a data warehouse. It is the default because it is the safest: no case sensitivity to argue about, no characters a SQL parser dislikes.
  • camelCase for JavaScript and JSON APIs. PascalCase for C#, Go exports and TypeScript interfaces.
  • kebab-case for URL segments and CSS-adjacent work, and for CLI tools that expect flags in that shape.
  • CONSTANT_CASE for environment variables and for the fixed-column extracts some ERP systems insist on.
  • Title Case when the file is going to a person rather than a program: a report, a shared spreadsheet, a mail merge. Short words in the middle stay lowercase, so date of birth becomes Date of Birth and not Date Of Birth.

The five identifier styles strip punctuation, because the output is meant to be usable as a field name and total_(usd) is not. The prose styles keep it, because Total (USD) in Title Case is still meant to read as a label. Apostrophes are removed rather than turned into a boundary in the identifier styles, so Customer's Name gives customers_name rather than customer_s_name.

Splitting a name into words is the hard part

Every case style rests on the same question: where do the words in this header end? Real files answer it four different ways at once, sometimes within one row.

Order ID       →  Order · ID
order_id       →  order · id
orderID        →  order · ID
OrderIDNumber  →  Order · ID · Number
order-id.v2    →  order · id · v · 2

Separators are the easy case. The interesting one is OrderIDNumber. Splitting on every lower-to-upper transition alone would give Order I D Number, breaking the acronym apart. Splitting on separators alone would leave the whole thing as one word. The rule that works is to break at a lower-to-upper transition and before the last capital of a run that is followed by a lowercase letter, which keeps ID together while still finding Number. A digit next to a letter is also a boundary, so v2 is two words and q1_revenue survives as q_1_revenue in snake_case, which is worth knowing before you run it on a quarterly report.

Accents are folded before any of this by default, and the fold covers the letters that Unicode decomposition alone misses. ß becomes ss, ø becomes o, æ becomes ae. Turn the switch off if the header is going somewhere that handles Unicode field names properly and you would rather keep them.

Duplicates, blanks and the mapping

Two headers with the same name is the failure that costs the most and shows the least. Nothing goes wrong while the file is a table: two columns called notes sit there quite happily. The damage happens at the moment the rows become objects, which is what every JSON conversion, every DictReader and most database loaders do, and at that moment one of the two columns disappears without a word.

So Duplicate names is on, and the second occurrence becomes notes_2, the third notes_3. The suffix follows the style, so kebab-case gives notes-2. Turning it off is allowed and produces a warning that says plainly what will happen to the file next. Note also that a style change can create a collision that was not there before: Order ID and order_id are two different headers until snake_case makes them one, and the numbering is what saves you.

Blank headers get column_N, numbered by position from 1, so column_4 really is the fourth column and you can find it. Turn the switch off and the blank stays blank, which some downstream tools handle and most do not.

The before-and-after list is the reason to use this page rather than a text editor. Renaming columns is a breaking change for anything downstream, and doing it silently is how a scheduled job starts failing on a Monday morning for reasons nobody can reconstruct. The list is printed above the result, capped at forty entries with a count of the rest, and it is worth reading before you download.

Practical notes

  • The data rows are not touched at all. Only the first row changes. Values keep their leading zeros, their currency symbols and their exact spelling, because this page has no opinion about any of them.
  • Max length trims cleanly. Capping at fifteen characters cuts the name and then removes a separator left dangling at the cut, so you get customer_name rather than customer_name_. A warning appears when anything was shortened, because truncation is the other way two names end up identical.
  • A byte order mark never survives. The invisible marker at the start of a Windows Excel export attaches itself to the first column name and then that column refuses to match anything by name. It is stripped when the file is read, on this page and every other CSV page on the site.
  • Zero-width characters in a header are removed. They arrive when a header has been pasted out of a web page or a document, they are completely invisible, and they are why a column called email sometimes will not match the string email.
  • Leaving the case alone is a real option. Set Style to Leave the case alone and you still get trimming, accent folding, duplicate numbering and blank naming, which is sometimes all a file needs.

Frequently Asked Questions

Which style should I use?

snake_case for Python, PostgreSQL and warehouses, which is why it is the default. camelCase for JavaScript and JSON APIs, PascalCase for C#, Go and TypeScript types, kebab-case for URLs, CONSTANT_CASE for environment variables. Title Case when the file is going to a person rather than a program.

What happens to two columns with the same name?

The second becomes notes_2 and the third notes_3, with the suffix following the chosen style. This matters more than it looks: a duplicate name is harmless while the file is a table and silently loses a column the moment the rows become objects, which is what every JSON conversion and most database loaders do.

Can a style change create a duplicate that was not there before?

Yes, and that is one of the reasons numbering is on by default. Order ID and order_id are two different headers until snake_case makes them the same one. The numbering catches it and the printed mapping shows you it happened.

What happens to a column with no name?

It becomes column_4, numbered by its position in the row starting at 1, so the name tells you where to find it. Switch Blank names off and the blank is left alone, which some downstream tools cope with and most do not.

Why did Total (USD) lose its brackets?

Because snake_case, camelCase, PascalCase, kebab-case and CONSTANT_CASE are meant to produce something usable as a field name, and total_(usd) is not. Those five styles treat punctuation as a word boundary. Title Case, lowercase, UPPERCASE and leaving the case alone all keep it.

Are the data rows changed at all?

No. Only the first row is rewritten. Every value below it is copied through exactly as it was, so leading zeros, currency symbols and spelling all survive untouched.

Can I see what changed before downloading?

Yes, and it is printed by default. Every rename appears above the result as the old name followed by the new one, along with counts of the blanks filled in, the duplicates numbered and the names shortened. Renaming a column is a breaking change downstream, so it should never happen silently.

Does the file leave my computer?

No. There is no upload endpoint on this page. The header row is read and rewritten by JavaScript in your own tab, and nothing is stored between visits.

One convention, one pass

Free, no account, no upload. Pick a style, read the mapping, take the CSV.

Back to the header cleaner