Six (key, attr, val) triples in long format folded into a nested dict-of-dicts in wide format. The trace shows table growing one inner key at a time as each attribute is set under its parent key.

By hand

For each triple, seed an empty inner dict for k if it has not been seen yet, then set table[k][a] = v. The if k not in table guard prevents overwriting an inner dict that already holds earlier attributes.

naive.py
Replay: real traced execution (multi-file project)
keys = ['p', 'p', 'q', 'q', 'r', 'r']
attrs = ['x', 'y', 'x', 'y', 'x', 'y']
vals = [1, 2, 3, 4, 5, 6]
table = {}
for k, a, v in zip(keys, attrs, vals):
    if k not in table:
        table[k] = {}
    table[k][a] = v
print('RESULT:', {k: table[k] for k in sorted(table)})
  1. keys ← ['p', 'p', 'q', 'q', 'r', 'r']

    1keys = ['p', 'p', 'q', 'q', 'r', 'r']2attrs = ['x', 'y', 'x', 'y', 'x', 'y']
    values this step['p', 'p', 'q', 'q', 'r', 'r']keys
  2. attrs ← ['x', 'y', 'x', 'y', 'x', 'y']

    1keys = ['p', 'p', 'q', 'q', 'r', 'r']2attrs = ['x', 'y', 'x', 'y', 'x', 'y']3vals = [1, 2, 3, 4, 5, 6]
    values this step['x', 'y', 'x', 'y', 'x', 'y']attrs
  3. vals ← [1, 2, 3, 4, 5, 6]

    2attrs = ['x', 'y', 'x', 'y', 'x', 'y']3vals = [1, 2, 3, 4, 5, 6]4table = {}
    values this step[1, 2, 3, 4, 5, 6]vals
  4. table ← {}

    3vals = [1, 2, 3, 4, 5, 6]4table = {}5for k, a, v in zip(keys, attrs, vals):
    values this step{}table
  5. a ← 'x', k ← 'p', v ← 1

    4table = {}5for k, a, v in zip(keys, attrs, vals):6    if k not in table:
    values this step'x'a'p'k1v
  6. if k not in table:

    5for k, a, v in zip(keys, attrs, vals):6    if k not in table:7        table[k] = {}
  7. table ← {'p': {}}

    6if k not in table:7    table[k] = {}8table[k][a] = v
    values this step{} {'p': {}}table
  8. table ← {'p': {'x': 1}}

    7        table[k] = {}8    table[k][a] = v9print('RESULT:', {k: table[k] for k in sorted(table)})
    values this step{'p': {}} {'p': {'x': 1}}table
  9. a ← 'y', v ← 2

    4table = {}5for k, a, v in zip(keys, attrs, vals):6    if k not in table:
    values this step'x' 'y'a1 2v
  10. if k not in table:

    5for k, a, v in zip(keys, attrs, vals):6    if k not in table:7        table[k] = {}
  11. table ← {'p': {'x': 1, 'y': 2}}

    7        table[k] = {}8    table[k][a] = v9print('RESULT:', {k: table[k] for k in sorted(table)})
    values this step{'p': {'x': 1}} {'p': {'x': 1, 'y': 2}}table
  12. a ← 'x', k ← 'q', v ← 3

    4table = {}5for k, a, v in zip(keys, attrs, vals):6    if k not in table:
    values this step'y' 'x'a'p' 'q'k2 3v
  13. if k not in table:

    5for k, a, v in zip(keys, attrs, vals):6    if k not in table:7        table[k] = {}
  14. table ← {'p': {'x': 1, 'y': 2}, 'q': {}}

    6if k not in table:7    table[k] = {}8table[k][a] = v
    values this step{'p': {'x': 1, 'y': 2}} {'p': {'x': 1, 'y': 2}, 'q': {}}table
  15. table ← {'p': {'x': 1, 'y': 2}, 'q': {'x': 3}}

    7        table[k] = {}8    table[k][a] = v9print('RESULT:', {k: table[k] for k in sorted(table)})
    values this step{'p': {'x': 1, 'y': 2}, 'q': {}} {'p': {'x': 1, 'y': 2}, 'q': {'x': 3}}table
  16. a ← 'y', v ← 4

    4table = {}5for k, a, v in zip(keys, attrs, vals):6    if k not in table:
    values this step'x' 'y'a3 4v
  17. if k not in table:

    5for k, a, v in zip(keys, attrs, vals):6    if k not in table:7        table[k] = {}
  18. table ← {'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}}

    7        table[k] = {}8    table[k][a] = v9print('RESULT:', {k: table[k] for k in sorted(table)})
    values this step{'p': {'x': 1, 'y': 2}, 'q': {'x': 3}} {'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}}table
  19. a ← 'x', k ← 'r', v ← 5

    4table = {}5for k, a, v in zip(keys, attrs, vals):6    if k not in table:
    values this step'y' 'x'a'q' 'r'k4 5v
  20. if k not in table:

    5for k, a, v in zip(keys, attrs, vals):6    if k not in table:7        table[k] = {}
  21. table ← {'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}, 'r': {}}

    6if k not in table:7    table[k] = {}8table[k][a] = v
    values this step{'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}} {'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}, 'r': {}}table
  22. table ← {'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}, 'r': {'x': 5}}

    7        table[k] = {}8    table[k][a] = v9print('RESULT:', {k: table[k] for k in sorted(table)})
    values this step{'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}, 'r': {}} {'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}, 'r': {'x': 5}}table
  23. a ← 'y', v ← 6

    4table = {}5for k, a, v in zip(keys, attrs, vals):6    if k not in table:
    values this step'x' 'y'a5 6v
  24. if k not in table:

    5for k, a, v in zip(keys, attrs, vals):6    if k not in table:7        table[k] = {}
  25. table ← {'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}, 'r': {'x': 5, 'y': 6}}

    7        table[k] = {}8    table[k][a] = v9print('RESULT:', {k: table[k] for k in sorted(table)})
    values this step{'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}, 'r': {'x': 5}} {'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}, 'r': {'x': 5, 'y': 6}}table
  26. for k, a, v in zip(keys, attrs, vals):

    4table = {}5for k, a, v in zip(keys, attrs, vals):6    if k not in table:
  27. stdout ← RESULT: {'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}, 'r': {'x': 5, 'y': 6}}

    8    table[k][a] = v9print('RESULT:', {k: table[k] for k in sorted(table)})
    values this stepRESULT: {'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}, 'r': {'x': 5, 'y': 6}}stdout

The Pythonic way

dict.setdefault(k, {}) returns the existing inner dict for k when present, or inserts an empty dict and returns it. Chaining [a] = v onto the return value collapses the seed-or-get guard into a single line per iteration.

library.py
keys = ['p', 'p', 'q', 'q', 'r', 'r']
attrs = ['x', 'y', 'x', 'y', 'x', 'y']
vals = [1, 2, 3, 4, 5, 6]
table = {}
for k, a, v in zip(keys, attrs, vals):
    table.setdefault(k, {})[a] = v
print('RESULT:', {k: table[k] for k in sorted(table)})
RESULT: {'p': {'x': 1, 'y': 2}, 'q': {'x': 3, 'y': 4}, 'r': {'x': 5, 'y': 6}}

Implementation notes

  • Long format stores one observation per row (three columns: key, attribute, value). Wide format stores all attributes for a key on one row — one column per attribute. Pivoting trades row count for column count.
  • The loop may interleave rows for different keys freely. The seed-or-get pattern handles this because setdefault is idempotent: calling it twice for the same key leaves the existing inner dict unchanged.
  • In pandas, df.pivot(index='key', columns='attr', values='val') performs this transformation in one call. The pandas pivot-table lesson (roadmap) extends this to aggregated values when multiple rows share the same (key, attr) pair.