Today's pattern: a sequence of pure-Python transforms, each consuming the previous's output. The naive form is one nested expression:
result = sorted([w.upper() for w in raw.split()] , reverse=True)Readable enough at 3 transforms. Past that, intermediate names make the chain debuggable:
raw = "the quick brown fox"
words = raw.split() # step 1: split
upper = [w.upper() for w in words] # step 2: upper-case
sorted_desc = sorted(upper, reverse=True) # step 3: sort
print(sorted_desc)
# ['THE', 'QUICK', 'FOX', 'BROWN']Four lines, three transforms, every intermediate has a name. If step 2 is wrong you print(upper) to see exactly that step's output.
Why bother — the one-liner works.
Two reasons. (1) Debuggability — print(upper) between steps shows you the data at each boundary. With the nested expression you'd dissect the one-liner mentally. (2) Modifiability — adding a 4th step ("only words longer than 3") is a single new line, not a re-nest of the whole thing.
When IS the one-liner right?
When the chain is so trivial you wouldn't unit-test the steps separately. [w.upper() for w in raw.split()] is fine inline. sorted([... complex predicate involving 3 facts ...], key=lambda x: ...) is the time to break out.
step_1 = transform_1(input)
step_2 = transform_2(step_1)
step_3 = transform_3(step_2)
result = step_3Each line is one transform. Each intermediate has a meaningful name (words, unique_ids, summary_rows) — not step_1, step_2. Let the names tell the story.
Debugging:
words = raw.split()
uppered = [w.upper() for w in words]
filtered = [w for w in uppered if len(w) > 3]
sorted_desc = sorted(filtered, reverse=True)
# something's off — print after each step
print(words) # ['the', 'quick', 'brown', 'fox']
print(uppered) # ['THE', 'QUICK', 'BROWN', 'FOX']
print(filtered) # ['QUICK', 'BROWN'] — oh, 'THE' was dropped, fox was dropped
print(sorted_desc) # ['QUICK', 'BROWN']The names give you free instrumentation. Compare with the one-liner equivalent — you'd have to rewrite the expression to see anything intermediate.
If a transform has logic worth naming and reusing:
def long_words_only(words):
return [w for w in words if len(w) > 3]
filtered = long_words_only(uppered)A function is a named transform. Use it when:
long_words_only is clearer than the predicateReal chains often mix transforms with side effects:
rows = read_sheet(...) # tool I/O
filtered = [r for r in rows if r["active"]] # transform
sorted_rows = sorted(filtered, key=lambda r: r["priority"]) # transform
for r in sorted_rows:
process(r) # tool I/OThe pattern: pure transforms in the middle, I/O at the boundaries. Same shape as the 3-step chain (week 1 day 1) — just more steps in the middle.
# excessive
stripped = raw.strip()
lowered = stripped.lower()
result = lowered
# better
normalized = raw.strip().lower()Method chaining on builtins is fine — that's not 3 transforms, that's normalize. Reach for intermediates when each step is conceptually distinct, not for syntactic chaining.
Today's pattern: a sequence of pure-Python transforms, each consuming the previous's output. The naive form is one nested expression:
result = sorted([w.upper() for w in raw.split()] , reverse=True)Readable enough at 3 transforms. Past that, intermediate names make the chain debuggable:
raw = "the quick brown fox"
words = raw.split() # step 1: split
upper = [w.upper() for w in words] # step 2: upper-case
sorted_desc = sorted(upper, reverse=True) # step 3: sort
print(sorted_desc)
# ['THE', 'QUICK', 'FOX', 'BROWN']Four lines, three transforms, every intermediate has a name. If step 2 is wrong you print(upper) to see exactly that step's output.
Why bother — the one-liner works.
Two reasons. (1) Debuggability — print(upper) between steps shows you the data at each boundary. With the nested expression you'd dissect the one-liner mentally. (2) Modifiability — adding a 4th step ("only words longer than 3") is a single new line, not a re-nest of the whole thing.
When IS the one-liner right?
When the chain is so trivial you wouldn't unit-test the steps separately. [w.upper() for w in raw.split()] is fine inline. sorted([... complex predicate involving 3 facts ...], key=lambda x: ...) is the time to break out.
step_1 = transform_1(input)
step_2 = transform_2(step_1)
step_3 = transform_3(step_2)
result = step_3Each line is one transform. Each intermediate has a meaningful name (words, unique_ids, summary_rows) — not step_1, step_2. Let the names tell the story.
Debugging:
words = raw.split()
uppered = [w.upper() for w in words]
filtered = [w for w in uppered if len(w) > 3]
sorted_desc = sorted(filtered, reverse=True)
# something's off — print after each step
print(words) # ['the', 'quick', 'brown', 'fox']
print(uppered) # ['THE', 'QUICK', 'BROWN', 'FOX']
print(filtered) # ['QUICK', 'BROWN'] — oh, 'THE' was dropped, fox was dropped
print(sorted_desc) # ['QUICK', 'BROWN']The names give you free instrumentation. Compare with the one-liner equivalent — you'd have to rewrite the expression to see anything intermediate.
If a transform has logic worth naming and reusing:
def long_words_only(words):
return [w for w in words if len(w) > 3]
filtered = long_words_only(uppered)A function is a named transform. Use it when:
long_words_only is clearer than the predicateReal chains often mix transforms with side effects:
rows = read_sheet(...) # tool I/O
filtered = [r for r in rows if r["active"]] # transform
sorted_rows = sorted(filtered, key=lambda r: r["priority"]) # transform
for r in sorted_rows:
process(r) # tool I/OThe pattern: pure transforms in the middle, I/O at the boundaries. Same shape as the 3-step chain (week 1 day 1) — just more steps in the middle.
# excessive
stripped = raw.strip()
lowered = stripped.lower()
result = lowered
# better
normalized = raw.strip().lower()Method chaining on builtins is fine — that's not 3 transforms, that's normalize. Reach for intermediates when each step is conceptually distinct, not for syntactic chaining.
Create a free account to get started. Paid plans unlock all tracks.