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)
| File | Size | Uploaded | |
|---|---|---|---|
| listtocell-0.1.0.tar.gz | 5.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|