Find the minimum and maximum value per category from parallel label and value lists. On the first visit to a category the value seeds both bounds; subsequent visits update lo or hi only when the new value falls outside the current range. The trace shows lo and hi narrowing to their final bounds before the result tuples are assembled.

By hand

Walk cats and vals together with zip. Keep two dicts, lo and hi. On the first visit to a category, seed both with the current value. On every visit run two comparisons: update lo[cat] if val is smaller, update hi[cat] if it is larger. Build result from sorted keys.

naive.py
Replay: real traced execution (multi-file project)
cats = ['a', 'b', 'a', 'c', 'b', 'a', 'c', 'b']
vals = [2, 9, 4, 6, 3, 6, 10, 6]
lo = {}
hi = {}
for cat, val in zip(cats, vals):
    if cat not in lo:
        lo[cat] = val
        hi[cat] = val
    if val < lo[cat]:
        lo[cat] = val
    if val > hi[cat]:
        hi[cat] = val
result = {k: (lo[k], hi[k]) for k in sorted(lo)}
print('RESULT:', result)
  1. cats ← ['a', 'b', 'a', 'c', 'b', 'a', 'c', 'b']

    1cats = ['a', 'b', 'a', 'c', 'b', 'a', 'c', 'b']2vals = [2, 9, 4, 6, 3, 6, 10, 6]
    values this step['a', 'b', 'a', 'c', 'b', 'a', 'c', 'b']cats
  2. vals ← [2, 9, 4, 6, 3, 6, 10, 6]

    1cats = ['a', 'b', 'a', 'c', 'b', 'a', 'c', 'b']2vals = [2, 9, 4, 6, 3, 6, 10, 6]3lo = {}
    values this step[2, 9, 4, 6, 3, 6, 10, 6]vals
  3. lo ← {}

    2vals = [2, 9, 4, 6, 3, 6, 10, 6]3lo = {}4hi = {}
    values this step{}lo
  4. hi ← {}

    3lo = {}4hi = {}5for cat, val in zip(cats, vals):
    values this step{}hi
  5. cat ← 'a', val ← 2, lo ← {'a': 2}, hi ← {'a': 2}

    pass 1 of 2
    4hi = {}5for cat, val in zip(cats, vals):6    if cat not in lo:7        lo[cat] = val8        hi[cat] = val9    if val < lo[cat]:10        lo[cat] = val11    if val > hi[cat]:12        hi[cat] = val
    values this step'a'cat2val{} {'a': 2}lo{} {'a': 2}hi
  6. cat ← 'b', val ← 9, lo ← {'a': 2, 'b': 9}, hi ← {'a': 2, 'b': 9}

    pass 2 of 2
    4hi = {}5for cat, val in zip(cats, vals):6    if cat not in lo:7        lo[cat] = val8        hi[cat] = val9    if val < lo[cat]:10        lo[cat] = val11    if val > hi[cat]:12        hi[cat] = val
    values this step'a' 'b'cat2 9val{'a': 2} {'a': 2, 'b': 9}lo{'a': 2} {'a': 2, 'b': 9}hi
  7. cat ← 'a', val ← 4

    4hi = {}5for cat, val in zip(cats, vals):6    if cat not in lo:
    values this step'b' 'a'cat9 4val
  8. if cat not in lo:

    5for cat, val in zip(cats, vals):6    if cat not in lo:7        lo[cat] = val
  9. if val < lo[cat]:

    8    hi[cat] = val9if val < lo[cat]:10    lo[cat] = val
  10. if val > hi[cat]:

    10    lo[cat] = val11if val > hi[cat]:12    hi[cat] = val
  11. hi ← {'a': 4, 'b': 9}

    11    if val > hi[cat]:12        hi[cat] = val13result = {k: (lo[k], hi[k]) for k in sorted(lo)}
    values this step{'a': 2, 'b': 9} {'a': 4, 'b': 9}hi
  12. cat ← 'c', val ← 6

    4hi = {}5for cat, val in zip(cats, vals):6    if cat not in lo:
    values this step'a' 'c'cat4 6val
  13. if cat not in lo:

    5for cat, val in zip(cats, vals):6    if cat not in lo:7        lo[cat] = val
  14. lo ← {'a': 2, 'b': 9, 'c': 6}

    6if cat not in lo:7    lo[cat] = val8    hi[cat] = val
    values this step{'a': 2, 'b': 9} {'a': 2, 'b': 9, 'c': 6}lo
  15. hi ← {'a': 4, 'b': 9, 'c': 6}

    7    lo[cat] = val8    hi[cat] = val9if val < lo[cat]:
    values this step{'a': 4, 'b': 9} {'a': 4, 'b': 9, 'c': 6}hi
  16. if val < lo[cat]:

    8    hi[cat] = val9if val < lo[cat]:10    lo[cat] = val
  17. if val > hi[cat]:

    10    lo[cat] = val11if val > hi[cat]:12    hi[cat] = val
  18. cat ← 'b', val ← 3

    4hi = {}5for cat, val in zip(cats, vals):6    if cat not in lo:
    values this step'c' 'b'cat6 3val
  19. if cat not in lo:

    5for cat, val in zip(cats, vals):6    if cat not in lo:7        lo[cat] = val
  20. if val < lo[cat]:

    8    hi[cat] = val9if val < lo[cat]:10    lo[cat] = val
  21. lo ← {'a': 2, 'b': 3, 'c': 6}

    9if val < lo[cat]:10    lo[cat] = val11if val > hi[cat]:
    values this step{'a': 2, 'b': 9, 'c': 6} {'a': 2, 'b': 3, 'c': 6}lo
  22. if val > hi[cat]:

    10    lo[cat] = val11if val > hi[cat]:12    hi[cat] = val
  23. cat ← 'a', val ← 6, hi ← {'a': 6, 'b': 9, 'c': 6}

    pass 1 of 2
    4hi = {}5for cat, val in zip(cats, vals):6    if cat not in lo:7        lo[cat] = val8        hi[cat] = val9    if val < lo[cat]:10        lo[cat] = val11    if val > hi[cat]:12        hi[cat] = val13result = {k: (lo[k], hi[k]) for k in sorted(lo)}
    values this step'b' 'a'cat3 6val{'a': 4, 'b': 9, 'c': 6} {'a': 6, 'b': 9, 'c': 6}hi
  24. cat ← 'c', val ← 10, hi ← {'a': 6, 'b': 9, 'c': 10}

    pass 2 of 2
    4hi = {}5for cat, val in zip(cats, vals):6    if cat not in lo:7        lo[cat] = val8        hi[cat] = val9    if val < lo[cat]:10        lo[cat] = val11    if val > hi[cat]:12        hi[cat] = val13result = {k: (lo[k], hi[k]) for k in sorted(lo)}
    values this step'a' 'c'cat6 10val{'a': 6, 'b': 9, 'c': 6} {'a': 6, 'b': 9, 'c': 10}hi
  25. cat ← 'b', val ← 6

    4hi = {}5for cat, val in zip(cats, vals):6    if cat not in lo:
    values this step'c' 'b'cat10 6val
  26. if cat not in lo:

    5for cat, val in zip(cats, vals):6    if cat not in lo:7        lo[cat] = val
  27. if val < lo[cat]:

    8    hi[cat] = val9if val < lo[cat]:10    lo[cat] = val
  28. if val > hi[cat]:

    10    lo[cat] = val11if val > hi[cat]:12    hi[cat] = val
  29. for cat, val in zip(cats, vals):

    4hi = {}5for cat, val in zip(cats, vals):6    if cat not in lo:
  30. result ← {'a': (2, 6), 'b': (3, 9), 'c': (6, 10)}

    12        hi[cat] = val13result = {k: (lo[k], hi[k]) for k in sorted(lo)}14print('RESULT:', result)
    values this step{'a': (2, 6), 'b': (3, 9), 'c': (6, 10)}result
  31. stdout ← RESULT: {'a': (2, 6), 'b': (3, 9), 'c': (6, 10)}

    13result = {k: (lo[k], hi[k]) for k in sorted(lo)}14print('RESULT:', result)
    values this stepRESULT: {'a': (2, 6), 'b': (3, 9), 'c': (6, 10)}stdout

The Pythonic way

Collect all values per group into a defaultdict(list), then compute min and max over each group list in a single dict comprehension.

library.py
from collections import defaultdict
cats = ['a', 'b', 'a', 'c', 'b', 'a', 'c', 'b']
vals = [2, 9, 4, 6, 3, 6, 10, 6]
groups = defaultdict(list)
for cat, val in zip(cats, vals):
    groups[cat].append(val)
result = {k: (min(groups[k]), max(groups[k])) for k in sorted(groups)}
print('RESULT:', result)
RESULT: {'a': (2, 6), 'b': (3, 9), 'c': (6, 10)}

Implementation notes

  • The seed if cat not in lo: block fires only on the first occurrence of each key (three times for three categories); the comparison ifs fire on every iteration including the first, but are always false on the seed pass because val == lo[cat] == hi[cat] at that point.
  • lo and hi stay as plain int-valued dicts throughout the loop so the trace shows concrete numbers at every step; the tuple is formed only in the final comprehension.
  • result is a dict (trackable), so the comprehension event shows the completed {'a': (2, 6), 'b': (3, 9), 'c': (6, 10)} in the trace even though the values are tuples.