Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions benedict/serializers/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,16 @@ def encode(self, d: list[dict[str, Any]], **kwargs: Any) -> str:
kwargs.setdefault("lineterminator", "\n")
columns = kwargs.pop("columns", None)
columns_row = kwargs.pop("columns_row", True)
# 2026-10-11: Supplied headers keep their existing writer behavior.
headers = columns
if not columns and len(ls) and type_util.is_dict(ls[0]):
keys = [str(key) for key in ls[0].keys()]
columns = sorted(keys)
# 2026-10-11: Keep original keys for lookup while sorting their labels.
columns = sorted(ls[0].keys(), key=str)
headers = [str(key) for key in columns]
f = StringIO()
w = csv.writer(f, **kwargs)
if columns_row and columns:
w.writerow(columns)
w.writerow(headers)
for item in ls:
if type_util.is_dict(item):
row = [item.get(key, "") for key in columns]
Expand Down
21 changes: 21 additions & 0 deletions tests/dicts/io/test_io_dict_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,27 @@ def test_to_csv(self) -> None:
"""
self.assertEqual(s, r)

# 2026-10-11: Column labels must not replace the keys used for row lookup.
def test_to_csv_with_non_string_keys(self) -> None:
for row, expected in (
({2: "second", 1: "first"}, "1,2\nfirst,second\n"),
({2: "second", "a": "first"}, "2,a\nsecond,first\n"),
({2: "second", "10": "first"}, "10,2\nfirst,second\n"),
):
with self.subTest(row=row):
d = IODict({"values": [row]})
self.assertEqual(d.to_csv(), expected)
self.assertEqual(
d.to_csv(columns_row=False), expected.split("\n", 1)[1]
)

# 2026-10-11: Inferred labels stringify None while explicit columns keep it.
def test_to_csv_with_none_key(self) -> None:
d = IODict({"values": [{None: "first", "a": "second"}]})
self.assertEqual(d.to_csv(), "None,a\nfirst,second\n")
self.assertEqual(d.to_csv(columns_row=False), "first,second\n")
self.assertEqual(d.to_csv(columns=[None, "a"]), ",a\nfirst,second\n")

def test_to_csv_with_custom_columns(self) -> None:
d = IODict(
{
Expand Down
Loading