Skip to content

Emptying a nested array of tables before an out-of-order super-table renders invalid TOML (0.15.2, since #567) #690

Open

Description

@drewjst

Versions tested: tomlkit 0.15.2 (tag a0b15eb), and master at d7def7d (2026-10-29); Python 3.12.

Reproduce:

import tomlkit

doc = tomlkit.parse("[[a.b.c]]\nx = 1\n\n[a]\ny = 1\n")
doc["a"]["b"]["c"].pop()
out = tomlkit.dumps(doc)
print(repr(out))
tomlkit.parse(out)

The input is valid TOML, and tomlkit parses it to {'a': {'b': {'c': [{'x': 1}]}, 'y': 1}}. After the pop, doc.unwrap() is {'a': {'b': {'c': []}, 'y': 1}}, as expected.

Observed: dumps returns 'a.b.c = []\n[a]\ny = 1\n', which declares a twice:

tomlkit.exceptions.ParseError: Redefinition of an existing table at line 3 col 0

tomllib.loads rejects it too: Cannot declare ('a',) twice (at line 2, column 3).

Expected: output that parses back to {'a': {'b': {'c': []}, 'y': 1}}, for example what tomlkit.dumps(doc.unwrap()) returns:

[a]
y = 1

[a.b]
c = []

Where it comes from: #567 renders an emptied array of tables as key = []. When the array sits under a super-table that is defined later in the document, the dotted key is emitted where the [[a.b.c]] header was, before [a]. On the parent commit of #567, the same steps produce output that parses. The same happens when [a] extends a.b with a dotted key, for example [[a.b.c]]\nx = 1\n[a]\nb.z = 1\n.

Related: #553, which #567 addressed in part, and #675, another failure after removing an element from an out-of-order table.

This came up while running generated tests on pairs of tomlkit pull requests that were open at the same time (#565 and #567). The reproduction above gives the same output on 0.15.2 and on master.

Activity

  1. ryanchou1994 commented on Nov 2, 2026

    @ryanchou1994

    This reproduces on master (d7def7d). The key = [] fallback from #567 is written at the top of the nearest header scope, so when a later fragment declares a table on its path with a header, the hoisted dotted key declares that table a second time.

    A fix that writes the fallback under the header that declares it instead ([a] / b.c = [] / y = 1 for the first example) is coming as a PR shortly. It only touches rendering in container.py, but it is about 80 lines, so happy to rework it if you would prefer a different approach.

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