Set Workbook Properties Like Author and Title with Python
Workbooks produced by scripts tend to be anonymous. Open one in Excel and File → Info says the author is "openpyxl" or nothing at all, there is no title, and nobody can tell which job produced it or from what data. Setting the document properties fixes that in a few lines, and custom properties go further: they let a file carry facts about itself — its source query, its reporting period, its row count — that another program can read without touching a sheet. This page, part of Using openpyxl for Excel File Manipulation, covers both libraries and their differences.
Prerequisites
pip install openpyxl xlsxwriter pandas
Step 1 — Set the standard properties with openpyxl
Every openpyxl workbook has a properties object with the standard fields. Set the ones readers will see:
import datetime as dt
from openpyxl import Workbook, load_workbook
wb = Workbook()
wb.active["A1"] = "Weekly sales"
props = wb.properties
print(props.creator) # 'openpyxl' — the default
props.title = "Weekly sales report"
props.subject = "Sales"
props.creator = "Finance reporting"
props.keywords = "sales, weekly, regions"
props.description = "Generated nightly from the sales warehouse"
props.category = "Reports"
props.created = dt.datetime(2026, 9, 21, 6, 0)
wb.save("weekly_sales.xlsx")
back = load_workbook("weekly_sales.xlsx").properties
print(back.title, "|", back.creator, "|", back.created, "|", back.modified)
creator is what Excel shows as Author. Replacing the default openpyxl with a team or job name is worth doing on its own: it tells a reader where to ask questions. created is written as given. modified is not — openpyxl stamps it with the current time on every save, so assigning it has no effect.
Step 2 — Add custom properties
Custom properties are named, typed values stored alongside the standard ones. openpyxl exposes them as wb.custom_doc_props, a list you append typed property objects to:
from openpyxl.packaging.custom import StringProperty, IntProperty, BoolProperty, DateTimeProperty
wb = load_workbook("weekly_sales.xlsx")
wb.custom_doc_props.append(StringProperty(name="Source", value="warehouse.sales_daily"))
wb.custom_doc_props.append(IntProperty(name="RowCount", value=1250))
wb.custom_doc_props.append(BoolProperty(name="Validated", value=True))
wb.custom_doc_props.append(DateTimeProperty(name="PeriodEnd", value=dt.datetime(2026, 9, 20)))
wb.save("weekly_sales.xlsx")
for prop in load_workbook("weekly_sales.xlsx").custom_doc_props:
print(f"{prop.name:10} {prop.value!r:32} {type(prop).__name__}")
Types survive the round trip, so a reader gets an integer row count and a real date, not strings. In Excel they appear under File → Properties → Advanced Properties → Custom. Adding a property whose name already exists makes a duplicate entry, so when updating a file that may already carry one, remove the old entry first — the next step does.
Step 3 — Update a property without duplicating it
Jobs that rewrite the same workbook — refreshing a report in place — need "set" semantics, not "append". A small helper removes any entry with the same name first:
def set_custom(wb, prop):
existing = [p for p in wb.custom_doc_props if p.name == prop.name]
for p in existing:
wb.custom_doc_props.props.remove(p)
wb.custom_doc_props.append(prop)
wb = load_workbook("weekly_sales.xlsx")
set_custom(wb, IntProperty(name="RowCount", value=1310))
wb.save("weekly_sales.xlsx")
print([(p.name, p.value) for p in load_workbook("weekly_sales.xlsx").custom_doc_props])
Stamp every file with its run details
Rather than listing properties by hand in each job, write one function that records how the file was produced. It picks the right property class from each value's type, so callers pass plain Python values:
import getpass
import platform
from openpyxl.packaging.custom import FloatProperty
def stamp(wb, **facts):
kinds = [(bool, BoolProperty), (int, IntProperty), (float, FloatProperty),
(dt.datetime, DateTimeProperty), (str, StringProperty)]
for name, value in facts.items():
for py_type, prop_type in kinds:
if isinstance(value, py_type):
set_custom(wb, prop_type(name=name, value=value))
break
else:
set_custom(wb, StringProperty(name=name, value=str(value)))
wb = load_workbook("weekly_sales.xlsx")
stamp(wb, Job="weekly_sales", RunAt=dt.datetime(2026, 9, 21, 6, 0), Host=platform.node() or "unknown",
RunBy=getpass.getuser(), Rows=1310, Total=48210.5, Validated=True)
wb.save("weekly_sales.xlsx")
print(sorted(p.name for p in load_workbook("weekly_sales.xlsx").custom_doc_props))
bool is checked before int on purpose: in Python, True is also an integer, and without that order a flag would be stored as the number 1. When someone asks, months later, where a figure came from, the answer — job, time, host, row count — is inside the file they are holding, not in a log that has since been rotated away.
Step 4 — Set the same fields, plus company, with xlsxwriter
xlsxwriter sets everything in one dictionary, including the Company and Manager fields openpyxl cannot write, and a separate call adds custom properties. The value's Python type decides the property type:
import xlsxwriter
book = xlsxwriter.Workbook("weekly_sales_xw.xlsx")
book.set_properties({
"title": "Weekly sales report",
"author": "Finance reporting",
"company": "Example Ltd",
"manager": "Head of Finance",
"keywords": "sales, weekly, regions",
"comments": "Generated nightly from the sales warehouse",
"category": "Reports",
"status": "Final",
"created": dt.datetime(2026, 9, 21, 6, 0),
})
book.set_custom_property("Source", "warehouse.sales_daily")
book.set_custom_property("RowCount", 1250)
book.set_custom_property("Validated", True)
book.add_worksheet().write("A1", "Weekly sales")
book.close()
p = load_workbook("weekly_sales_xw.xlsx").properties
print(p.creator, "|", p.created, "|", p.modified)
xlsxwriter writes the created date into both the created and the modified field, so two runs with the same date and data produce identical property sections — useful when output files are compared byte for byte, as in Make Excel Output Byte-for-Byte Reproducible. Note the key names differ from openpyxl's: author rather than creator, comments rather than description.
Step 5 — Set properties when pandas writes the file
With pandas, reach the engine's workbook object through the writer. For xlsxwriter that is writer.book, and the calls are the same as above:
import pandas as pd
sales = pd.DataFrame({"region": ["North", "South"], "revenue": [1200, 950]})
with pd.ExcelWriter("pandas_props.xlsx", engine="xlsxwriter") as writer:
sales.to_excel(writer, sheet_name="Sales", index=False)
writer.book.set_properties({"title": "Revenue by region", "author": "Finance reporting"})
writer.book.set_custom_property("RowCount", len(sales))
print(load_workbook("pandas_props.xlsx").properties.title)
With the openpyxl engine, writer.book.properties is the openpyxl object from Step 1. Deriving values like the row count from the DataFrame being written keeps the property honest: it describes the file it is in.
Step 6 — Read properties to audit a folder of reports
Because properties live outside the sheets, reading them is cheap — no cell data is loaded when you only look at properties in read-only mode. That makes a quick audit of a reports folder easy:
from pathlib import Path
for path in sorted(Path(".").glob("*.xlsx")):
wb = load_workbook(path, read_only=True)
p = wb.properties
custom = {c.name: c.value for c in wb.custom_doc_props}
print(f"{path.name:22} author={p.creator!r:22} title={p.title!r:26} rows={custom.get('RowCount')}")
wb.close()
A file with the default author or no title stands out immediately, and custom properties such as Validated let a distribution step refuse to send anything that did not pass its checks.
Before sharing files outside the organisation
Properties travel with the file. An author name, a manager, a company, or a custom property naming an internal database are all visible to anyone who receives the workbook. For external recipients, set neutral values or clear them; Remove Hidden Data Before Sharing an Excel File covers properties along with comments, hidden sheets and other leftovers.
Common pitfalls
- Assigning
modifiedin openpyxl. It is overwritten on save. - Appending the same custom property twice. It creates duplicates; replace instead.
- Mixing up key names. xlsxwriter's
authorandcommentscorrespond to openpyxl'screatoranddescription. - Putting secrets in properties. A connection string or an API token stored as a custom property is readable by every recipient. Record the name of a source, never its credentials.
- Treating properties as live. A
RowCountproperty describes the file when the job wrote it; if someone edits the sheet afterwards, the property does not change. Re-stamp whenever your code rewrites the data. - Leaving the default author. "openpyxl" as the author tells readers nothing useful.
Performance notes
Properties are a few hundred bytes of XML and cost nothing measurable to write or read. Reading them from many files is fast because read-only mode does not parse the sheets unless you iterate them.
Conclusion
Set title, creator, keywords and friends on wb.properties with openpyxl, add typed custom properties through wb.custom_doc_props (replacing rather than appending when updating), and use xlsxwriter's set_properties when Company and Manager matter or when the modified date must be fixed. Derive property values from the data being written, audit folders by reading properties in read-only mode, and review them before files leave the organisation.
Frequently asked questions
Where do workbook properties appear in Excel? Under File → Info, in the properties panel, and in File → Properties → Advanced for the full list including custom properties. Windows Explorer also shows the title, authors and tags (the keywords field) in its file details.
Why does my modified date always show the time the script ran? openpyxl sets the modified timestamp to the current time every time it saves, whatever you assign. xlsxwriter uses the created date you pass for both fields, which is useful for reproducible output.
Can openpyxl set the Company field? Not through a public API — openpyxl writes its own extended properties when it saves. Use xlsxwriter's set_properties, which supports company and manager, when those fields matter.
What are custom properties good for? Machine-readable facts about the file: which query produced it, the reporting period, the row count, whether it passed validation. Another script can read them without opening any sheet.
Related
- Up one level: Using openpyxl for Excel File Manipulation — the workbook-level API.
- Building Excel Reports with xlsxwriter — the rest of xlsxwriter's workbook options.
- Validate an Excel Report Before Sending It — where a Validated property fits in a job.
- Set the Active Sheet and Tab Colour with openpyxl — other finishing touches for generated workbooks.
More on report layouts & templates
- GuideBuilding Finance and Operations Reports in Excel with Python
- How-toAllocate Shared Costs Across Departments with Python
- How-toBuild a Bank Reconciliation Report with Python and Excel
- How-toBuild a Bill of Materials Cost Rollup
- How-toBuild a Break-Even Analysis in Excel with Python
- ComparisonBuild a Budget vs Actual Variance Report in Excel with Python