Naming Sheets and Columns for Machines in Excel Templates
Name sheets and columns in Excel templates so code and databases can use them: lowercase snake case, no punctuation, stable keys, label mapping.

A column is called "Unit Price ($)" in the template, unit_price in the database, unitPrice in the API and "Price per unit" in the report. Every translation between them is a place for an error, and every rename is a break waiting to happen. Names are the cheapest part of a template to get right and the most expensive to change later, because they are used as keys by everything downstream. This article gives the rules for names that machines can use, the limits Excel imposes, how to keep a friendly label alongside and how to catch mismatches. It is one of the layout rules in building Excel templates that import cleanly, and it connects to one header row and no merged cells in import templates.
What a machine name is
A machine name is a name that works as an identifier without being quoted, escaped or translated. In SQL, unit_price is a valid column name everywhere; Unit Price ($) needs double quotes and is case-sensitive then; price-eur is parsed as a subtraction. In Python, df.unit_price works as an attribute and df["Unit Price ($)"] is the only way for the second. In JSON, snake_case or camelCase keys are idiomatic. In URLs and file paths, spaces and symbols need encoding.
The safe subset is simple: lowercase ASCII letters, digits and underscores, starting with a letter. That is valid in PostgreSQL, MySQL, SQL Server, SQLite, DuckDB, Python, JavaScript property names, R, and CSV headers that tools like pandas turn into attributes. Names in this form can travel from the template to the table to the API without change.
Lowercase matters because databases fold case differently: PostgreSQL folds unquoted names to lowercase, Oracle to uppercase, and MySQL's behavior depends on the operating system. A name with mixed case such as OrderNo becomes orderno in PostgreSQL unless quoted, and then your code and your table disagree on spelling.
Rules that keep names safe
Start with a letter. A leading digit (2026_sales) is invalid unquoted in SQL and in Python identifiers.
Use underscores, not spaces or hyphens. order_no, not Order No or order-no.
No punctuation or symbols. Parentheses, dollar signs, percent signs, slashes and accents cause trouble. Put the unit in the name if needed: price_usd, weight_kg, rate_pct.
Keep them short but unambiguous. qty and quantity are both fine; choose one and use it everywhere. Avoid abbreviations that only the author understands. 30 characters or fewer keeps SQL readable; PostgreSQL's limit is 63 bytes and silently truncates longer names, which can create duplicates.
Avoid reserved words. order, group, user, table, select, end and year are reserved or special in some databases. Use order_no, group_code, user_id, ends_on. If you cannot avoid one, the loader must quote it.
Be consistent. One style across the whole template: snake_case for columns; plural nouns for tables, or singular, but not both; the same word for the same idea (customer_id, never cust_id in one place and customer_no in another).
Be unique. Two columns called date in one sheet cannot both be addressed by name. If a sheet needs two dates, name them for what they are: ordered_on and shipped_on.
Make types visible where it helps. Suffixes such as _on or _at for dates and timestamps, _id for identifiers, _qty or _count for counts, _pct for percentages, _usd for money in a currency, make the intent clear without a data dictionary.
Excel's own limits on sheet names
Sheet names are the first thing a loader looks up. Excel imposes rules:
At most 31 characters.
Not blank.
None of the characters
\ / ? * [ ] :.Not starting or ending with an apostrophe.
Unique within the workbook, compared case-insensitively (
Dataanddataclash).The name
Historyis reserved by Excel.
Libraries enforce some of these and let others slip through into a file that Excel then repairs on open. XlsxWriter raises an exception for an invalid name; openpyxl warns for some and writes a file that opens with an error for others. Check in the generator: assert len(name) <= 31 and not re.search(r"[\\/?*\[\]:]", name).
Use short, lowercase sheet names for data that code reads: data, lists, notes. Reserve the capitalized or friendly ones (Instructions, README) for sheets only people read, and have the loader find the data sheet by exact name, falling back to an error with the sheet names found.
Table names (Excel tables) have rules too: letters, digits, underscores and periods, starting with a letter or underscore, no spaces, unique in the workbook, and not the same as a cell reference (A1, R1C1). A table called Orders is good; Table1 says nothing.
Keep labels for people
People like readable labels: "Unit price (USD)". Machines like unit_price. Do not compromise either one: keep both, mapped.
Options:
Machine names in the header, labels on the instructions sheet. The header says
unit_price; the instructions list each column, its label, its meaning and an example. Simple and dependable, and it asks people to read machine names, which most can.Labels in the header, mapping in the loader. The header says "Unit price (USD)", and a dictionary in the loader maps it to
unit_price. Friendlier for the sender and a place for errors: a label edited by a well-meaning person breaks the match. If you do this, normalize before matching (trim, collapse spaces, lower-case) and report unknown labels.Two header rows, with labels above names. This breaks the "one header row" rule; avoid it.
Cell comments or a note on each header cell. The comment shows the label and the explanation on hover.
Option 1 plus cell comments is a good balance for staff templates. Option 2 suits external senders who will not learn identifiers, with the mapping kept in the schema file that also generates the template.
Names are a contract
The moment a loader, a report or a formula depends on a name, the name is part of an interface. Renaming qty to quantity breaks the loader, the query that reads the staging table and every dashboard filter. So treat a rename like any interface change: add the new name, accept both for a period, announce a cut-over, remove the old. Record it in the version history of the template (versioning template layouts so old files still import).
In Excel itself, renaming a table column updates the formulas that use structured references, which is convenient and can hide the change from downstream code. Renaming a sheet updates formulas in the workbook and not external references. A loader reading by name finds nothing. Add a test that the expected sheet and columns exist.
Catch mismatches that look identical
Many name bugs cannot be seen. A header with a trailing space, qty , looks like qty. A non-breaking space between words, a zero-width character from a pasted heading, a capital letter where you expected lowercase, a Cyrillic а that looks like a Latin a. The loader's match fails and the error message prints names that appear the same. Normalize before comparing, and when reporting a mismatch, show the repr and the code points:
def clean(h):
s = str(h).replace(" ", " ").strip().lower()
return re.sub(r"\s+", "_", s)
def explain(h):
return f"{h!r} -> {[hex(ord(c)) for c in str(h) if ord(c) > 127]}"If explain prints a non-empty list, there are non-ASCII characters in the name. For pandas, df.columns = [clean(c) for c in df.columns] is a sensible first step on any input, followed by a duplicate check: if df.columns.duplicated().any(): raise. Excel renames duplicate headers silently in a table (adding a number), and pandas does too (qty, qty.1), so look for the suffixes.
Generating names from labels
When you inherit a sheet with friendly headers and need machine names, generate them with a function and review the result by eye:
import re, unicodedata
def to_name(label):
s = unicodedata.normalize("NFKD", str(label)).encode("ascii", "ignore").decode()
s = re.sub(r"[^0-9a-zA-Z]+", "_", s).strip("_").lower()
return ("c_" + s) if s[:1].isdigit() else (s or "col")"Unit Price ($)" becomes unit_price, "2026 Sales" becomes c_2026_sales, and "Número de pedido" becomes numero_de_pedido. Review for collisions and for names that lost meaning ("%" alone becomes empty and falls back to col). Store the mapping from label to name, so the original label can be shown in reports.
When friendly names matter more than machine names
Friendly names matter in a report that people read: a header "Net revenue (USD thousands)" is better than net_revenue_kusd for a director. Generate the friendly version at presentation time from the machine name through the mapping, and keep the data sheet plain. They also matter on forms for external senders who will see only the template. There the label can lead and the machine name hide in the loader. The one place to avoid friendly names is anywhere code uses them as keys. The PostgreSQL documentation on identifiers explains case folding and length limits, and Microsoft lists the rules for naming worksheets.


