Skip to content
ryanwang520Public

About

Make creating Excel XLSX files fun again

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Repository files navigation

Poi

Declarative Excel for Python. Build complex spreadsheets like you build React components — no coordinates, no merge_range math.

CI PyPI Python License Docs


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")

table


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 Book API
  • ⚡ Fast — built on xlsxwriter; fast=True skips empty-cell styling for very large outputs

Documentation

📖 Read the docs →


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.

About

Make creating Excel XLSX files fun again

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages