How-to
Getting Started With Python Excel AutomationDeep dive

Set Workbook Properties Like Author and Title with Python

Give generated workbooks a proper title, author, company, keywords and custom properties such as the data source or row count. See what openpyxl and xlsxwriter can set, which fields each one overwrites on save, and how to read the properties back for auditing.

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.

Where an .xlsx file keeps its properties Three stacked parts inside the workbook archive. docProps/core.xml holds the standard fields: title, subject, creator, keywords, description, category and the created and modified dates. docProps/app.xml holds application fields such as company and manager. docProps/custom.xml holds custom name and value pairs with types. Three parts of the zip archive docProps/core.xml title, subject, creator, keywords, description, category, created, modified docProps/app.xml application, company, manager docProps/custom.xml your own named, typed values

Prerequisites

Bash
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:

Python
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:

Python
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.

Custom properties as another script sees them A table of the four custom properties read back from weekly_sales.xlsx: Source is the string warehouse.sales_daily, RowCount is the integer 1250, Validated is the boolean true, and PeriodEnd is the date 2026-09-20. Each keeps its type. load_workbook(path).custom_doc_props A B C 1 name value type 2 Source warehouse.sales_daily StringProperty 3 RowCount 1250 IntProperty 4 Validated True BoolProperty 5 PeriodEnd 2026-09-20 DateTimeProperty

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:

Python
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:

Python
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:

Python
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.

Which library can set which property A comparison of openpyxl and xlsxwriter. Both can set title, author, subject, keywords, description, category and custom properties. The created date is honoured by both. The modified date is overwritten with the save time by openpyxl and follows the created date in xlsxwriter. Company and manager are available only in xlsxwriter. Editing an existing file is possible only with openpyxl. openpyxl xlsxwriter Title, author, keywords, category Custom properties Created date Modified date set to save time same as created Company, manager Edit an existing file

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:

Python
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:

Python
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 modified in openpyxl. It is overwritten on save.
  • Appending the same custom property twice. It creates duplicates; replace instead.
  • Mixing up key names. xlsxwriter's author and comments correspond to openpyxl's creator and description.
  • 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 RowCount property 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.

More on report layouts & templates

More on report layouts & templates →