Write Excel XLSX declaratively.
Project description
Poi
Declarative Excel for Python.
Build complex spreadsheets like you build React components — no coordinates, no merge_range math.
Before & After
A simple report: a title row spanning four columns, two KPI cards, and a data table below.
With xlsxwriter — you manage every coordinate:
import xlsxwriter
wb = xlsxwriter.Workbook("report.xlsx")
ws = wb.add_worksheet()
title = wb.add_format({"bold": True, "font_size": 18, "align": "center"})
kpi = wb.add_format({"bold": True, "align": "center", "border": 1, "font_size": 14})
header = wb.add_format({"bold": True, "bg_color": "#EFEFEF", "border": 1})
cell = wb.add_format({"border": 1})
ws.merge_range("A1:D1", "Q4 Report", title)
ws.merge_range("A3:B5", "Revenue $1.28M", kpi)
ws.merge_range("C3:D5", "Growth +18%", kpi)
for col, h in enumerate(["Region", "Q3", "Q4", "Δ"]):
ws.write(6, col, h, header)
for row, r in enumerate(
[("North", 320, 410, "+28%"), ("South", 280, 305, "+9%")], start=7
):
for col, val in enumerate(r):
ws.write(row, col, val, cell)
wb.close()
With Poi — you describe the structure:
from poi import Sheet, Col, Row, Cell, Table
regions = [
{"region": "North", "q3": 320, "q4": 410, "delta": "+28%"},
{"region": "South", "q3": 280, "q4": 305, "delta": "+9%"},
]
Sheet(root=Col(
Cell("Q4 Report", colspan=4, style="font_size: 18; bold: true; align: center"),
Row(
Cell("Revenue\n$1.28M", colspan=2, style="border: 1; bold: true"),
Cell("Growth\n+18%", colspan=2, style="border: 1; bold: true"),
),
Table(
data=regions,
columns=[("region", "Region"), ("q3", "Q3"), ("q4", "Q4"), ("delta", "Δ")],
border=1,
),
)).write("report.xlsx")
No row indexes. No merge_range strings. Insert a new section anywhere — everything below shifts automatically.
Quick Start
pip install poi
from poi import Sheet, Table
data = [
{"name": "iPhone", "price": 999},
{"name": "iPad", "price": 599},
]
Sheet(root=Table(
data=data,
columns=[("name", "Product"), ("price", "Price")],
col_width="auto",
border=1,
)).write("products.xlsx")
When to Use Poi
Poi is built for spreadsheets that aren't just "dump a dataframe":
- Dashboard reports with KPI cards stacked above detailed tables
- Multi-section sheets where the layout shifts as data changes
- CJK-heavy layouts where column widths actually matter
- Multi-sheet books with mixed structures across tabs
If df.to_excel() is good enough for you, you don't need Poi. If you've ever counted rows by hand or rewritten merge_range("A3:B5", ...) because you inserted a row — Poi is for you.
Smart CJK Column Auto-fit
A real differentiator. Most Excel libraries measure column width by character count, which silently breaks for Chinese, Japanese, and Korean text — each CJK character renders at roughly twice the width of an ASCII character.
Table(data=data, columns=[...], col_width="auto")
Poi counts double-width characters correctly and recognizes common date patterns. Your 产品名称 column fits on the first try.
Full Example: Images & Conditional Formatting
from typing import NamedTuple
from datetime import datetime
from poi import Sheet, Table
class Product(NamedTuple):
name: str
price: int
created_at: datetime
img: str
data = [
Product(f"prod {i}", i * 17, datetime.now(), "./product.jpg")
for i in range(5)
]
Sheet(root=Table(
data=data,
columns=[
{"type": "image", "attr": "img", "title": "Image",
"options": {"x_scale": 0.27, "y_scale": 0.25}},
("name", "Name"),
("price", "Price"),
("created_at", "Created"),
],
col_width="auto",
row_height=80,
cell_style={
# Bold red when price > 50
"font_color: red; bold: true":
lambda record, col: col.attr == "price" and record.price > 50,
},
date_format="yyyy-mm-dd",
align="center",
border=1,
)).write("table.xlsx")
Comparison
| Poi | xlsxwriter |
openpyxl |
pandas.to_excel |
xltpl |
|
|---|---|---|---|---|---|
| Style | Declarative tree | Imperative coordinates | Imperative coordinates | One-shot export | Excel as template |
| Nested layouts (KPI cards above tables) | Native (Row/Col) |
Manual offset math | Manual offset math | Not supported | Constrained by template |
| Coordinate / span math | Automatic | You compute | You compute | N/A | Template-bound |
| CJK-aware column auto-fit | Built in (col_width="auto") |
Manual set_column |
Manual | Not supported | Template-bound |
| Conditional cell style | Lambda per row | Manual per cell | Manual per cell | Verbose Styler API |
Pre-baked in template |
| Multi-sheet books | Book API |
Supported | Supported | ExcelWriter |
Supported |
| Best when | Designed reports & dashboards | Maximum control, very large writes | Reading + editing existing files | Pure dataframe dump | Fixed layouts filled with data |
Features
- 🎨 Declarative DSL — describe spreadsheets as a tree of
Row,Col,Cell,Image,Table - 📐 Automatic layout — rowspans, colspans, offsets, and indexes are computed for you
- 📏 Smart column auto-fit —
col_width="auto", native CJK & date-pattern support - 🎯 CSS-like styling —
style="font_size: 14; bold: true; bg_color: #EEE" - 🎛️ Lambda-based conditional formatting — style cells by predicate
- 💬 Interactive comments — pop-up tooltips on cells and headers
- 📚 Multi-sheet workbooks via the
BookAPI - ⚡ Fast — built on
xlsxwriter;fast=Trueskips empty-cell styling for very large outputs
Documentation
- Introduction & philosophy
- Components reference & auto-fit guide
- Styling, borders & comments
- Multi-sheet workbooks
Contributing
Poi is a small, focused library and feedback is very welcome — especially from teams using it on real Chinese / multilingual reports. Open an issue, share a tricky layout, or send a PR.
Why "Poi"? A short, easy-to-type name — like a Point on a grid. That's it.
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 poi-1.2.8.tar.gz.
File metadata
- Download URL: poi-1.2.8.tar.gz
- Upload date:
- Size: 15.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0326a8629d0684347b0e578bcf41e3c87e1cdd18b911249a41fa532f54a7f7d7
|
|
| MD5 |
ac7e919a1728545b2842aad2b44e5fd9
|
|
| BLAKE2b-256 |
26b4bb0c55f8b67d2c30e8cffd7c4c9b4b14b48ffad434eadbf13bb570e86841
|
Provenance
The following attestation bundles were made for poi-1.2.8.tar.gz:
Publisher:
release.yml on ryanwang520/poi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
poi-1.2.8.tar.gz -
Subject digest:
0326a8629d0684347b0e578bcf41e3c87e1cdd18b911249a41fa532f54a7f7d7 - Sigstore transparency entry: 1893514427
- Sigstore integration time:
-
Permalink:
ryanwang520/poi@17d527a83b11c44af0f736f0bbd2146132ca01a8 -
Branch / Tag:
refs/tags/v1.2.8 - Owner: https://github.com/ryanwang520
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@17d527a83b11c44af0f736f0bbd2146132ca01a8 -
Trigger Event:
push
-
Statement type:
File details
Details for the file poi-1.2.8-py3-none-any.whl.
File metadata
- Download URL: poi-1.2.8-py3-none-any.whl
- Upload date:
- Size: 17.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a3b9d397a16e2aa045d12838c47db60c8e855d8a8f59e8557d25e94f9c7bb89
|
|
| MD5 |
06f93c7a98b4fe2ae1ee080fed13147b
|
|
| BLAKE2b-256 |
e19f5e30af6ab3c34932d292b00814e8e98e94f21ed648c3120fdc713ac74712
|
Provenance
The following attestation bundles were made for poi-1.2.8-py3-none-any.whl:
Publisher:
release.yml on ryanwang520/poi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
poi-1.2.8-py3-none-any.whl -
Subject digest:
5a3b9d397a16e2aa045d12838c47db60c8e855d8a8f59e8557d25e94f9c7bb89 - Sigstore transparency entry: 1893514509
- Sigstore integration time:
-
Permalink:
ryanwang520/poi@17d527a83b11c44af0f736f0bbd2146132ca01a8 -
Branch / Tag:
refs/tags/v1.2.8 - Owner: https://github.com/ryanwang520
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@17d527a83b11c44af0f736f0bbd2146132ca01a8 -
Trigger Event:
push
-
Statement type: