Unpivot wide records (id + attribute columns) into long (id, variable, value) triples — one row per attribute per original record. With pandas, pd.melt does this in one call. Melt is the inverse of pivot.

By hand

Loop over each wide record, then over each attribute name. Each combination emits one (id, attr, value) tuple into long. The result list grows one entry per attribute per record.

naive.py
# trace: ignore records
records = [
    {'id': 'p', 'a': 10, 'b': 20},
    {'id': 'q', 'a': 30, 'b': 40},
]
attrs = ['a', 'b']
long = []
for r in records:
    for attr in attrs:
        long.append((r['id'], attr, r[attr]))
print('RESULT:', long)

With pandas

pd.melt(df, id_vars='id', value_vars=['a', 'b']) unpivots both attribute columns into variable and value rows. Pandas emits all rows for 'a' before all rows for 'b' (column-major order); the naive loop is row-major. Sorting makes both sides comparable.

library.py
import pandas as pd
from dalib.display import set_display
set_display()

records = [
    {'id': 'p', 'a': 10, 'b': 20},
    {'id': 'q', 'a': 30, 'b': 40},
]
df = pd.DataFrame(records)
melted = pd.melt(df, id_vars='id', value_vars=['a', 'b'])
result = list(zip(
    melted['id'].tolist(),
    melted['variable'].tolist(),
    melted['value'].tolist(),
))
print('columns:', melted.columns.tolist())
print('id:', melted['id'].tolist())
print('variable:', melted['variable'].tolist())
print('value:', melted['value'].tolist())
print('RESULT:', sorted(result))
columns: ['id', 'variable', 'value']
id: ['p', 'q', 'p', 'q']
variable: ['a', 'a', 'b', 'b']
value: [10, 30, 20, 40]
RESULT: [('p', 'a', 10), ('p', 'b', 20), ('q', 'a', 30), ('q', 'b', 40)]

Implementation notes

  • pd.melt stacks value_vars in column-major order: all rows for 'a' come before all rows for 'b'. The naive loop is row-major (all attributes of 'p' before all attributes of 'q'). Both represent the same data; sort to compare.
  • Omit value_vars to melt every column not in id_vars.
  • The generated column names default to 'variable' and 'value'; rename them with var_name='attr' and value_name='score'.
  • Cross-reference: pivot-table-simple (this chapter) for the inverse long-to-wide direction; flatten-nested (python-data-basics) for pure-Python unpivoting.