Create styled excel reports with declarative python.
Project description
xpyxl — Excel in Python
Compose polished spreadsheets with pure Python—no manual coordinates. You assemble rows/columns/cells; xpyxl handles layout, rendering, and styling with utility-style classes.
Core ideas
- Positionless composition: Build sheets declaratively from
row,col,cell,table,vstack, andhstack. - Composable styling: Tailwind-inspired utilities (typography, colors, alignment, number formats) applied via
style=[...]. - Deterministic rendering: Pure-data trees compiled into
.xlsxfiles with predictable output—ideal for tests and CI diffing.
Installation
uv add xpyxl
pip install xpyxl
Getting started
import xpyxl as x
report = (
x.workbook()[
x.sheet("Summary")[
x.row(style=[x.text_2xl, x.bold, x.text_blue])["Q3 Sales Overview"],
x.row(style=[x.text_sm, x.text_gray])["Region", "Units", "Price"],
x.row(style=[x.bg_primary, x.text_white, x.bold])["EMEA", 1200, 19.0],
x.row()["APAC", 900, 21.0],
x.row()["AMER", 1500, 18.5],
]
]
)
report.save("report.xlsx")
Rendering Engines
xpyxl supports multiple rendering engines, allowing you to choose the best one for your needs:
- openpyxl (default): Full-featured engine with comprehensive Excel support. Best for complex workbooks with advanced formatting.
- xlsxwriter: Fast, memory-efficient engine. Ideal for large datasets and performance-critical applications.
Using Different Engines
Specify the engine when saving:
import xpyxl as x
workbook = x.workbook()[
x.sheet("Data")[
x.row(style=[x.bold])["Name", "Value"],
x.row()["Item A", 100],
x.row()["Item B", 200],
]
]
# Use openpyxl (default)
workbook.save("output-openpyxl.xlsx", engine="openpyxl")
# Use xlsxwriter
workbook.save("output-xlsxwriter.xlsx", engine="xlsxwriter")
Both engines produce equivalent Excel files, but may have subtle differences in:
- File size and memory usage
- Rendering performance
- Support for advanced Excel features
Choose openpyxl for maximum compatibility and feature support, or xlsxwriter when performance and memory efficiency are priorities.
Performance Benchmarks
Benchmark results comparing the two rendering engines across different scenarios (averaged over 3 runs):
Big Tables
| Size | Engine | Time (s) | Memory (MB) |
|---|---|---|---|
| 100 | openpyxl | 0.0977 | 0.68 |
| 100 | xlsxwriter | 0.0325 | 0.50 |
| 1,000 | openpyxl | 0.9354 | 3.10 |
| 1,000 | xlsxwriter | 0.2565 | 2.04 |
| 10,000 | openpyxl | 11.2455 | 31.26 |
| 10,000 | xlsxwriter | 4.0539 | 17.81 |
| 50,000 | openpyxl | 55.5080 | 156.33 |
| 50,000 | xlsxwriter | 20.5686 | 94.43 |
Summary: xlsxwriter is 2.7-3.7x faster and uses 1.4-1.8x less memory for large tables. The openpyxl engine has been optimized with style object caching, providing ~15% performance improvement compared to previous versions.
Simple Layouts
| Engine | Time (s) | Memory (MB) |
|---|---|---|
| openpyxl | 0.0084 | 0.38 |
| xlsxwriter | 0.0058 | 0.34 |
Summary: xlsxwriter is 1.5x faster with similar memory usage.
Complex Layouts
| Engine | Time (s) | Memory (MB) |
|---|---|---|
| openpyxl | 0.1462 | 0.69 |
| xlsxwriter | 0.0454 | 0.52 |
Summary: xlsxwriter is 3.2x faster and uses 1.3x less memory for multi-sheet workbooks with styling.
Run benchmarks yourself:
uv run scripts/benchmark.py
Primitives
x.row(style=[x.bold, x.bg_warning])[1, 2, 3, 4, 5]
x.col(style=[x.italic])["a", "b", "c"]
x.cell(style=[x.text_green, x.number_precision])[42100]
row[...]accepts any sequence (numbers, strings, dataclasses…)col[...]stacks values verticallycell[...]wraps a single scalar- All primitives accept
style=[...]
Component: table
x.table(...) renders a header + body with optional style overrides. Combine with vstack/hstack for dashboards and reports.
sales_table = x.table(
header_style=[x.text_sm, x.text_gray, x.align_middle],
style=[x.table_bordered, x.table_compact],
)[
{"Region": "EMEA", "Units": 1200, "Price": 19.0},
{"Region": "APAC", "Units": 900, "Price": 21.0},
{"Region": "AMER", "Units": 1500, "Price": 18.5},
]
layout = x.vstack(
x.row(style=[x.text_xl, x.bold])["Q3 Sales Overview"],
x.space(),
x.hstack(
sales_table,
x.cell(style=[x.text_sm, x.text_gray])["Generated with xpyxl"],
gap=2,
),
)
Tables also accept pandas-friendly shapes:
- records:
table()[[{"region": "EMEA", "units": 1200}, ...]]derives the header from dict keys (missing keys are filled withNone). - dict of lists:
table()[{"region": ["EMEA", "APAC"], "units": [1200, 900]}]zips columns together (lengths must match). Headers are inferred from your keys and default to bold text on a muted background; override withheader_style=[...]when needed.
Utility styles (non-exhaustive)
- Typography:
text_xs/_sm/_base/_lg/_xl/_2xl/_3xl,bold,italic,mono - Text colors:
text_red,text_green,text_blue,text_orange,text_purple,text_black,text_gray - Backgrounds:
bg_red,bg_primary,bg_muted,bg_success,bg_warning,bg_info - Layout & alignment:
text_left,text_center,text_right,align_top/middle/bottom,wrap,nowrap,wrap_shrink,allow_overflow,row_height(...),row_width(...) - Use
allow_overflowwhen you want to keep a column narrow and let the text spill into adjacent empty cells,row_height(32)to force a specific row height, androw_width(12)to pin a column width. - Borders:
border_all,border_top,border_bottom,border_left,border_right,border_x,border_y,border_red,border_green,border_blue,border_orange,border_purple,border_black,border_gray,border_white,border_muted,border_primary,border_thin,border_medium,border_thick,border_dashed,border_dotted,border_double,border_none - Tables:
table_bordered,table_banded,table_compact - Number/date formats:
number_comma,number_precision,percent,currency_usd,currency_eur,date_short,datetime_short,time_short
Border utilities can sit on individual cells or be applied at the row/column level for fast outlines.
Mix and match utilities freely—what you see is what you get.
Layout helpers
vstack(a, b, c, gap=1, style=[x.border_all])vertically stacks components with optional blank rows and shared styles (great for card-like borders).hstack(a, b, gap=1, style=[x.border_all])arranges components side by side with configurable column gaps and shared wrapper styles.space(rows=1, height=None)inserts empty rows (optionally with a fixed height) in avstackor empty columns when dropped into anhstack.sheet(name, background_color="#F8FAFC")sets a sheet-wide background fill; the first ~200 rows and 80 columns are painted so the grid feels cohesive.
Examples & Tests
The tests/ directory contains example modules that demonstrate various features of xpyxl. Each module exports a build_workbook() function (or build_sample_workbook() for multi-sheet examples) that returns a SheetNode or list of SheetNode objects.
- Multi-sheet sales demo:
tests/multi_sheet_sales_demo.py- showcases tables, stacks, spacing, and utility styles across multiple sheets. - Border styles demo:
tests/border_styles_demo.py- demonstrates border utilities at cell, row, and column levels. - Wrap styles demo:
tests/wrap_styles_demo.py- shows text wrapping and overflow utilities. - Row height demo:
tests/row_height_demo.py- examples of manual row height and width controls. - Big table demo:
tests/big_table_demo.py- performance test with a 1k-row table.
Running Tests
Run all test modules to generate combined Excel files with both rendering engines:
uv run scripts/run_tests.py
This will:
- Collect sheets from all test modules
- Combine them into a single workbook
- Generate two output files in
.testing/:combined-output-openpyxl.xlsx(rendered with openpyxl engine)combined-output-xlsxwriter.xlsx(rendered with xlsxwriter engine)
Each test module contributes one or more sheets to the combined workbook, allowing you to compare rendering output between engines.
Types & ergonomics
- Modern Python with full type hints.
- Pure Python stack traces; easy to debug, script, and test.
- Deterministic rendering for stable diffs in CI.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xpyxl-0.5.1.tar.gz.
File metadata
- Download URL: xpyxl-0.5.1.tar.gz
- Upload date:
- Size: 22.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf3392c881facbeb322dba05affbbdb497cc363956ebfff1d527300cb21741e7
|
|
| MD5 |
46a44c5b3fc8f23aca5ac2badd958cc7
|
|
| BLAKE2b-256 |
c659701d6d71a4af75f5fe288d8a10c4930f4725a17a449c40a0c55c37b86683
|
Provenance
The following attestation bundles were made for xpyxl-0.5.1.tar.gz:
Publisher:
publish.yml on dakixr/xpyxl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xpyxl-0.5.1.tar.gz -
Subject digest:
cf3392c881facbeb322dba05affbbdb497cc363956ebfff1d527300cb21741e7 - Sigstore transparency entry: 738204908
- Sigstore integration time:
-
Permalink:
dakixr/xpyxl@f50395213677b6b227b5168e0dd61194e9188457 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/dakixr
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f50395213677b6b227b5168e0dd61194e9188457 -
Trigger Event:
push
-
Statement type:
File details
Details for the file xpyxl-0.5.1-py3-none-any.whl.
File metadata
- Download URL: xpyxl-0.5.1-py3-none-any.whl
- Upload date:
- Size: 22.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
970a1b3a40d7ebdf24ee45d551a85493bba56d475ec0f560c833fbdc3147d5e8
|
|
| MD5 |
08f4d0e0cbf54fe4d8112a2c11d9c60f
|
|
| BLAKE2b-256 |
9c2ef1758530e391f63506bfaa35acfb6eb449122b56845c60cfdca014650099
|
Provenance
The following attestation bundles were made for xpyxl-0.5.1-py3-none-any.whl:
Publisher:
publish.yml on dakixr/xpyxl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xpyxl-0.5.1-py3-none-any.whl -
Subject digest:
970a1b3a40d7ebdf24ee45d551a85493bba56d475ec0f560c833fbdc3147d5e8 - Sigstore transparency entry: 738204910
- Sigstore integration time:
-
Permalink:
dakixr/xpyxl@f50395213677b6b227b5168e0dd61194e9188457 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/dakixr
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f50395213677b6b227b5168e0dd61194e9188457 -
Trigger Event:
push
-
Statement type: