Method Chaining & .pipe()
Reviewed & published by Brayan K
Method chaining is the practice of linking several pandas operations into one flowing expression, where each method returns a new DataFrame for the next to work on, so a whole cleaning pipeline reads as a single top-to-bottom story.
Part of the free Pandas course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
You will chain dropna, assign, query, and sort_values, add columns with .assign(), drop in custom steps with .pipe(), and wrap chains in parentheses for clean formatting.
What You'll Learn in This Lesson
- Why chaining beats throwaway variables
- Chain dropna, assign, query, sort_values
- Add computed columns with .assign()
- Filter inside a chain with .query()
- Insert custom steps with .pipe(func)
- Wrap a chain in parentheses for readability
1 The Idea — One Expression, Many Steps
Most pandas methods return a new DataFrame rather than changing the original in place. That means the output of one method can flow straight into the next. Instead of a pile of df1, df2, df3 variables, you write a single chain that reads like a recipe.
# Without chaining: many throwaway variables
clean = df.dropna()
tagged = clean.assign(total=clean["qty"] * clean["price"])
big = tagged.query("total > 20")
result = big.sort_values("total")
# With chaining: one readable pipeline
result = (
df
.dropna()
.assign(total=lambda d: d["qty"] * d["price"])
.query("total > 20")
.sort_values("total")
)import pandas as pd
df = pd.DataFrame({
"item": ["a", "b", "c", "d"],
"qty": [2, 5, 1, 4],
"price": [10, 3, 50, 8],
})
result = (
df
.assign(total=lambda d: d["qty"] * d["price"])
.query("total > 20")
.sort_values("total")
)
print(result)
# ✅ Expected output:
# item qty price total
# 3 d 4 8 32
# 2 c 1 50 502 .assign() — Add Columns Without Breaking the Flow
.assign() returns a copy of the DataFrame with new columns added. You can add several at once, and a later column can build on one defined earlier in the same call by using a lambda. Because it never mutates in place, the original frame stays safe.
df.assign(
total = lambda d: d["qty"] * d["price"],
pricey = lambda d: d["total"] > 20, # uses the column above
)import pandas as pd
df = pd.DataFrame({"qty": [2, 5, 1], "price": [10, 3, 50]})
out = df.assign(
total=lambda d: d["qty"] * d["price"],
pricey=lambda d: d["total"] > 20,
)
print(out)
# ✅ Expected output:
# qty price total pricey
# 0 2 10 20 False
# 1 5 3 15 False
# 2 1 50 50 True3 .pipe() — Drop Your Own Function Into a Chain
Sometimes a step is too custom for a built-in method. .pipe(func) passes the current DataFrame to func and returns whatever it gives back, so your own function reads like a native chain link. Without pipe you would have to break the chain to write func(df).
def add_tax(d, rate):
return d.assign(with_tax=d["total"] * (1 + rate))
result = (
df
.assign(total=lambda d: d["qty"] * d["price"])
.pipe(add_tax, rate=0.1) # custom step, stays in the flow
)import pandas as pd
def add_tax(d, rate):
return d.assign(with_tax=(d["total"] * (1 + rate)).round(2))
df = pd.DataFrame({"qty": [2, 3], "price": [10, 20]})
result = (
df
.assign(total=lambda d: d["qty"] * d["price"])
.pipe(add_tax, rate=0.1)
)
print(result)
# ✅ Expected output:
# qty price total with_tax
# 0 2 10 20 22.0
# 1 3 20 60 66.0import pandas as pd
df = pd.DataFrame({
"name": ["Ann", "Bob", "Cy", "Dee"],
"score": [88, 55, 91, None],
})
# 🎯 YOUR TURN:
# Drop rows with missing scores, then keep only scores above 80,
# then sort highest first. Replace ___ with the method that
# removes rows containing NaN.
result = (
df
.___()
.query("score > 80")
.sort_values("score", ascending=False)
)
print(result)
# Expected output:
# name score
# 2 Cy 91.0
# 0 Ann 88.0! Common Errors (And How to Fix Them)
❌ Line breaks without parentheses
Python ends the statement at the first newline:
result = df
.dropna()
.query("score > 80") # ❌ SyntaxError / only df is assigned✅ Fix: wrap the whole chain in parentheses:
result = (
df
.dropna()
.query("score > 80")
) # ✅❌ assign referencing the wrong frame
df.dropna().assign(total=df["qty"] * df["price"]) # ❌ uses pre-dropna df✅ Fix: use a lambda so it sees the chained frame:
df.dropna().assign(total=lambda d: d["qty"] * d["price"]) # ✅🎯 Mini Challenge: Clean and Rank in One Chain
Turn a messy orders table into a tidy ranked result with a single chain.
- Drop rows with any missing values
- Add a revenue column = qty times price
- Keep revenue over 30, sort it descending
import pandas as pd
df = pd.DataFrame({
"item": ["a", "b", "c", "d"],
"qty": [4, 2, None, 5],
"price": [10, 25, 5, 8],
})
result = (
df
.dropna()
.assign(revenue=lambda d: d["qty"] * d["price"])
.query("revenue > 30")
.sort_values("revenue", ascending=False)
.reset_index(drop=True)
)
print(result)
# ✅ Expected output:
# item qty price revenue
# 0 b 2.0 25 50.0
# 1 a 4.0 10 40.0
# 2 d 5.0 8 40.0❓ Frequently Asked Questions
Lesson complete — your pipelines read like prose!
You can chain dropna, assign, query, and sort_values, add columns with .assign(), slot custom steps in with .pipe(), and wrap it all in parentheses for clean formatting.
Practice quiz
What makes method chaining possible in pandas?
- Methods print as they run
- Most methods return a new DataFrame for the next to act on
- Methods always mutate in place
- Pandas caches every step
Answer: Most methods return a new DataFrame for the next to act on. Each method returns a new DataFrame, so its output flows into the next call.
Why wrap a long chain in a pair of parentheses?
- It runs faster
- It is required by .pipe
- So each method can sit on its own line without backslashes
- It mutates the original
Answer: So each method can sit on its own line without backslashes. Python ignores line breaks inside parentheses, keeping the chain readable.
What does .assign() return?
- None (mutates in place)
- Only the new column
- A new DataFrame with the column(s) added
- A boolean mask
Answer: A new DataFrame with the column(s) added. .assign() returns a copy with the new columns, leaving the original untouched.
Inside a chain, why use assign(total=lambda d: ...) instead of assign(total=df[...])?
- The lambda sees the current chained frame, not the original df
- Lambdas are faster
- df is undefined
- It avoids parentheses
Answer: The lambda sees the current chained frame, not the original df. A lambda receives the current state of the chain; referencing df uses the original.
What does .query('total > 20') do in a chain?
- Adds a column
- Sorts the rows
- Renames total
- Filters rows where the condition is True
Answer: Filters rows where the condition is True. .query() keeps only rows matching the string expression.
When should you reach for .pipe(func)?
- To insert your own function as a chain step
- To sort values
- To drop NaN
- To rename columns
Answer: To insert your own function as a chain step. .pipe(func) passes the current frame to your function so a custom step reads like a native one.
Which method removes rows containing NaN inside a chain?
- query()
- dropna()
- assign()
- pipe()
Answer: dropna(). dropna() drops rows with missing values and returns a new frame.
What does df.pipe(my_func) pass to my_func?
- Only the index
- A copy of one column
- The current DataFrame
- Nothing
Answer: The current DataFrame. pipe hands the whole current DataFrame to your function as its first argument.
Why does line breaks WITHOUT parentheses break a chain?
- Pandas disallows newlines
- Python ends the statement at the first newline
- It deletes the DataFrame
- It raises a KeyError
Answer: Python ends the statement at the first newline. Without enclosing parentheses, Python treats the first line as the whole statement.
Which reads more clearly for three custom steps?
- summarise(enrich(clean(df)))
- Three separate scripts
- df.pipe(clean).pipe(enrich).pipe(summarise)
- A while loop
Answer: df.pipe(clean).pipe(enrich).pipe(summarise). Chained .pipe calls read top-to-bottom in order, unlike nested inside-out calls.
Continue this course
- Previous: Plotting with DataFrame.plot()
- Next: Styling & Formatting Output — Control display options and format tables with round, to_markdown, and .style
- Quick reference: Pandas cheat sheet
Frequently asked questions
What is method chaining and why is it useful?
Method chaining strings several pandas operations together in one expression, where each method returns a new DataFrame that the next method acts on — like df.dropna().assign(...).query(...).sort_values(...). It avoids dozens of throwaway intermediate variables and reads top-to-bottom as a clear pipeline of steps.
Why wrap a chain in parentheses?
Wrapping the whole expression in a pair of parentheses lets you put each method on its own line without backslashes. Python ignores line breaks inside parentheses, so the chain stays readable and you can comment each step. It is the standard style for long pandas pipelines.
What does .assign() do?
.assign() returns a new DataFrame with one or more new (or replaced) columns, computed from the existing ones. Because it returns a copy rather than mutating in place, it fits perfectly inside a chain and keeps your original DataFrame untouched.
When should I reach for .pipe()?
Use .pipe(func) when you want to insert your own function into a chain. Instead of breaking the flow to call my_func(df), you write df.pipe(my_func), so a custom step reads exactly like the built-in ones. It is the escape hatch that keeps long pipelines unbroken.