Skip to main content

listtocell

Convert nested Python dicts into Excel-style merged-cell coordinates (A1, A1:C1, A1:B2).

Useful for building multi-level table headers with merged cells in spreadsheet libraries like openpyxl or xlsxwriter — without manually calculating ranges.

Installation

pip install listtocell
# or
uv add listtocell

Quick start

from listtocell import get_cells

result = get_cells({
    "Person": {"Name": "John", "Age": 25},
    "Score": 99,
})
[
  {"cell": "A1", "range": "A1:B1", "value": "Person", "column": "Person"},
  {"cell": "A2", "range": "A2:A2", "value": "John", "column": "Name"},
  {"cell": "B2", "range": "B2:B2", "value": 25, "column": "Age"},
  {"cell": "C1", "range": "C1:C2", "value": 99, "column": "Score"}
]

Each entry contains:

Key Description
cell Top-left cell of the merge (e.g. A1)
range Full merge range (e.g. A1:B1)
value Cell content
column Original dict key

How it works

Flat dict — single header row

get_cells({"Name": "John", "Age": 25})
[
  {"cell": "A1", "range": "A1:A1", "value": "John", "column": "Name"},
  {"cell": "B1", "range": "B1:B1", "value": 25, "column": "Age"}
]

Nested dict — merged parent + child row

get_cells({"Person": {"Name": "John", "Age": 25}})
[
  {"cell": "A1", "range": "A1:B1", "value": "Person", "column": "Person"},
  {"cell": "A2", "range": "A2:A2", "value": "John", "column": "Name"},
  {"cell": "B2", "range": "B2:B2", "value": 25, "column": "Age"}
]

Mixed depth — flat value spans remaining rows

When a flat value sits alongside nested values, it automatically rowspans to fill the depth:

get_cells({"Group": {"X": 1, "Y": 2}, "Total": 99})
[
  {"cell": "A1", "range": "A1:B1", "value": "Group", "column": "Group"},
  {"cell": "A2", "range": "A2:A2", "value": 1, "column": "X"},
  {"cell": "B2", "range": "B2:B2", "value": 2, "column": "Y"},
  {"cell": "C1", "range": "C1:C2", "value": 99, "column": "Total"}
]

Multiple nesting levels

get_cells({"Report": {"Summary": {"Total": 100, "Avg": 50}}})
[
  {"cell": "A1", "range": "A1:B1", "value": "Report", "column": "Report"},
  {"cell": "A2", "range": "A2:B2", "value": "Summary", "column": "Summary"},
  {"cell": "A3", "range": "A3:A3", "value": 100, "column": "Total"},
  {"cell": "B3", "range": "B3:B3", "value": 50, "column": "Avg"}
]

List as value — expanded like a dict

Lists are treated as nested structures with integer keys:

get_cells({"Tags": ["python", "excel"]})
[
  {"cell": "A1", "range": "A1:B1", "value": "Tags", "column": "Tags"},
  {"cell": "A2", "range": "A2:A2", "value": "python", "column": 0},
  {"cell": "B2", "range": "B2:B2", "value": "excel", "column": 1}
]

Use cases

openpyxl — grouped headers

from openpyxl import Workbook
from openpyxl.styles import Alignment
from listtocell import get_cells

wb = Workbook()
ws = wb.active

headers = {
    "Personal": {"Name": "Alice", "Age": 30},
    "Financial": {"Salary": 5000, "Bonus": 500, "Total": 5500},
}

for entry in get_cells(headers):
    ws[entry["cell"]] = entry["value"]
    if entry["cell"] != entry["range"].split(":")[1]:  # is a merge
        ws.merge_cells(entry["range"])
        ws[entry["cell"]].alignment = Alignment(horizontal="center")

wb.save("report.xlsx")

xlsxwriter — grouped headers

import xlsxwriter
from listtocell import get_cells

workbook = xlsxwriter.Workbook("report.xlsx")
ws = workbook.add_worksheet()
merge_fmt = workbook.add_format({"align": "center", "bold": True})

headers = {
    "Q1": {"Jan": 100, "Feb": 120, "Mar": 110},
    "Q2": {"Apr": 130, "May": 140, "Jun": 150},
}

for entry in get_cells(headers):
    cell_start, cell_end = entry["range"].split(":")
    if cell_start == cell_end:
        ws.write(entry["cell"], entry["value"])
    else:
        ws.merge_range(entry["range"], entry["value"], merge_fmt)

workbook.close()

Dynamic headers from config

Headers can be built programmatically from any data source — database schema, API response, config file:

from listtocell import get_cells

# Config driven
schema = {
    "user": {"id": None, "email": None, "role": None},
    "audit": {"created_at": None, "updated_at": None},
}

cells = get_cells(schema)
# Use cells to render a table header in any output format

Row offset (range_start)

When writing below existing content (e.g. a title row), shift all row numbers:

result = get_cells({"Group": {"X": 1, "Y": 2}}, range_start=3)
[
  {"cell": "A3", "range": "A3:B3", "value": "Group", "column": "Group"},
  {"cell": "A4", "range": "A4:A4", "value": 1, "column": "X"},
  {"cell": "B4", "range": "B4:B4", "value": 2, "column": "Y"}
]

Column span (colspan / Span)

Two ways to make a leaf cell span multiple columns:

External dict:

from listtocell import get_cells

result = get_cells(
    {"Notes": "see below", "Group": {"X": 1, "Y": 2}},
    colspan={"Notes": 2},
)
[
  {"cell": "A1", "range": "A1:B2", "value": "see below", "column": "Notes"},
  {"cell": "C1", "range": "C1:D1", "value": "Group", "column": "Group"},
  {"cell": "C2", "range": "C2:C2", "value": 1, "column": "X"},
  {"cell": "D2", "range": "D2:D2", "value": 2, "column": "Y"}
]

Span inline — keeps the colspan next to the value:

from listtocell import get_cells, Span

result = get_cells({"Notes": Span("see below", 2), "Group": {"X": 1, "Y": 2}})
[
  {"cell": "A1", "range": "A1:B2", "value": "see below", "column": "Notes"},
  {"cell": "C1", "range": "C1:D1", "value": "Group", "column": "Group"},
  {"cell": "C2", "range": "C2:C2", "value": 1, "column": "X"},
  {"cell": "D2", "range": "D2:D2", "value": 2, "column": "Y"}
]

API reference

get_cells(arr, *, range_start=1, colspan=None)

Parameter Type Description
arr dict | list Header structure to traverse
range_start int First row number (default 1)
colspan dict Mapping of key → column span for leaf values

Span(value, span)

Wraps a leaf value with an explicit column span. Takes priority over the colspan dict when both are provided for the same key.

Listtocell (class)

Lower-level class for cases where you need to reuse an instance or set range_start separately:

from listtocell import Listtocell

ltc = Listtocell()
ltc.range_start = 2
result = ltc.get_cells({"A": {"X": 1, "Y": 2}})

License

MIT

Metadata

Release files for listtocell 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for listtocell 0.1.0
File Size Uploaded
listtocell-0.1.0.tar.gz 5.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for listtocell 0.1.0
File Interpreter ABI Platform
listtocell-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 10.8 kB

Release files / listtocell-0.1.0.tar.gz

Download URL listtocell-0.1.0.tar.gz
Size 5.8 kB
Tags Source
SHA-256 checksum
How to use checksums
0e26162e7e4778949e6d562ff5ce9f178f0690810a71d273a49d25ab41d93cfa
BLAKE2b-256 checksum
How to use checksums
c602007d5b3c3b5e56721eca15cc2577041628e42571c132e42c07c9bdeea62a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.12

Release files / listtocell-0.1.0-py3-none-any.whl

Download URL listtocell-0.1.0-py3-none-any.whl
Size 5.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d7fd03482f3b0a875d0e969460f73e9930033b7fe1a14d1b4b857f6d62e40735
BLAKE2b-256 checksum
How to use checksums
4a7e7e75ab6b339372f5ef7220098747b541c5437a3081c4cc007ff1a2f6c43a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.12

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page