Skip to content

dumps(sort_keys=True) only sorts the top level of a parsed document (nested tables / inline tables / AoT keep original order) #614

Description

@Master-Norna

Summary

For a document produced by tomlkit.parse() / loads(), dumps(doc, sort_keys=True) sorts only the top-level keys. Nested tables, inline tables, and arrays-of-tables keep their original (insertion) order. The same option applied to a plain dict sorts recursively. So the output depends on whether the input was parsed or built from scratch, which is surprising for a documented, explicit option.

Reproduction (copy-paste)

import tomlkit

doc = tomlkit.parse("""
[zeta]
b = 2
a = 1

[alpha]
b = 2
a = 1

top = 0
""")

print(tomlkit.dumps(doc, sort_keys=True))
# [alpha]
# b = 2
# a = 1
#
# top = 0
#
# [zeta]
# b = 2
# a = 1
#     -> top level IS sorted (alpha before zeta), but inside each table
#        `b` still comes before `a`.

print(tomlkit.dumps({"zeta": {"b": 2, "a": 1}, "alpha": {"b": 2, "a": 1}, "top": 0}, sort_keys=True))
# top = 0
#
# [alpha]
# a = 1
# b = 2
#
# [zeta]
# a = 1
# b = 2
#     -> fully sorted, including nested keys.

Same split for inline tables and arrays of tables:

print(tomlkit.dumps(tomlkit.parse("[t]\ninl = { z = 1, a = 2 }\n"), sort_keys=True))
# inl = { z = 1, a = 2 }      <- NOT sorted
print(tomlkit.dumps({"t": {"inl": {"z": 1, "a": 2}}}, sort_keys=True))
# inl = { a = 2, z = 1 }      <- sorted

print(tomlkit.dumps(tomlkit.parse("[[t]]\nz = 1\na = 2\n"), sort_keys=True))
# z = 1
# a = 2                        <- NOT sorted
print(tomlkit.dumps({"t": [{"z": 1, "a": 2}]}, sort_keys=True))
# a = 2
# z = 1                        <- sorted

Expected

sort_keys=True sorts recursively, consistent with the plain-dict path and with the documented "sort the keys in alphabetic order".

Actual

Only the top level of a parsed document is sorted; nested structures keep their original order.

Root cause

dumps() (api.py:60-64) routes parsed documents through item(data, _sort_keys=True). In item() (items.py:114), the top-level TOMLDocument/Container is a dict (not an Item), so it is rebuilt in the isinstance(value, dict) branch (items.py:139-150), which sorts. But every nested value in a parsed document is already a Table / InlineTable / AoT, and item() returns it unchanged at the early guard if isinstance(value, Item): return value (items.py:130-131). Those items are never rebuilt, so their keys keep their original order. The plain-dict path has no such guard (nested plain dicts are rebuilt recursively), which is why it sorts fully.

This is the residual of #466 ("sort_keys does not (always?) work"), which fixed the top level; the nested levels were not covered.

Scope note / open question

A fix would need to rebuild nested Table / InlineTable / AoT items when _sort_keys is set. Because tomlkit's whole purpose is lossless round-tripping (preserving comments, blank lines, key order, formatting), rebuilding nested tables would drop that formatting. So this may be a deliberate trade-off rather than a plain bug — filing here to get a decision:

  • (a) sort recursively and accept loss of nested formatting, or
  • (b) document that sort_keys applies only to the top level of parsed documents.

Happy to take either once there's a direction.

Environment

tomlkit master @ 8c959b5 (0.15.1+), Python 3.12.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions