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.
Release files for poi 1.2.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| poi-1.2.8.tar.gz | 15.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| poi-1.2.8-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.9 kB
Release files / poi-1.2.8.tar.gz
| Download URL | poi-1.2.8.tar.gz |
|---|---|
| Size | 15.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0326a8629d0684347b0e578bcf41e3c87e1cdd18b911249a41fa532f54a7f7d7
|
|
BLAKE2b-256 checksum How to use checksums |
26b4bb0c55f8b67d2c30e8cffd7c4c9b4b14b48ffad434eadbf13bb570e86841
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 21, 2026.
Transparency logRelease files / poi-1.2.8-py3-none-any.whl
| Download URL | poi-1.2.8-py3-none-any.whl |
|---|---|
| Size | 17.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5a3b9d397a16e2aa045d12838c47db60c8e855d8a8f59e8557d25e94f9c7bb89
|
|
BLAKE2b-256 checksum How to use checksums |
e19f5e30af6ab3c34932d292b00814e8e98e94f21ed648c3120fdc713ac74712
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 21, 2026.
Transparency log