Method chaining
One expression, no half-modified frames, and the same answer every time you run it.
chaining.assign().pipe().query().loc[lambda]inplace is a trapWatch it happen
Play it through, or step back and forth yourself.
df = orders.copy()
df = df.drop_duplicates()
df["item"] = df["item"].str.strip().str.lower()
df = df.dropna(subset=["rating"])
df["price"] = df["revenue"] / df["cups"]
df = df[df["cups"] > 90]
df = df.sort_values("cups", ascending=False)
df = df.reset_index(drop=True)Eight steps, each reassigning df. It works, and it has three real problems: you can't tell what df holds at any given line, re-running a cell out of order silently gives a different answer, and the name stops describing the thing.
The idea
Nearly every pandas method returns a new frame. That's not an accident — it's what makes them composable, and it's the whole basis of this lesson.
The problem with the usual style
df = orders.copy()
df = df.drop_duplicates()
df["item"] = df["item"].str.strip().str.lower()
df = df.dropna(subset=["rating"])
df = df[df["cups"] > 90]It works, and it has three real problems.
You can't tell what df holds at any given line without reading every line above it.
Re-running one cell changes the answer. In a notebook, running the drop_duplicates line twice is harmless — running the filter twice isn't obviously so, and running them out of order gives a different result with no error.
The name stops describing the thing. By line five, df is a deduplicated, cleaned, filtered subset — and it's still called df.
The same thing as a chain
clean = (
orders
.drop_duplicates()
.assign(item=lambda d: d["item"].str.strip().str.lower())
.dropna(subset=["rating"])
.assign(price=lambda d: d["revenue"] / d["cups"])
.query("cups > 90")
.sort_values("cups", ascending=False)
.reset_index(drop=True)
)One expression, read top to bottom as a list of steps. There is no intermediate state to get wrong, nothing half-modified, and re-running it always gives the same answer. The wrapping parentheses are what let each step sit on its own line.

The three tools that make it work
assign adds a column and returns a new frame, so it chains where df["x"] = ... can't. The lambda matters: d is the frame as it is at that point in the chain, so a step can use a column an earlier step just created.
query — or .loc[lambda d: ...] — filters without needing a name. Mid-chain there's nothing to write df[df["cups"] > 90] against.
pipe drops any function into a chain. It calls fn(df) and passes the result on, so your own helpers join in rather than breaking the chain in half:
def add_price(df):
return df.assign(price=df["revenue"] / df["cups"])
orders.pipe(add_price).pipe(tidy_names).query("cups > 90")
Debugging a chain
A chain is one expression, so you can't drop a print in the middle. Two ways round it:
.pipe(lambda d: (print(d.shape), d)[1]) # peek and pass through
orders.head(20).pipe(...) # develop on a sample firstAnd it's fine to break a chain up while you're working on it and reassemble it afterwards. The chain is the finished form, not the working one.
When not to chain
Past about eight steps a chain stops being clearer than what it replaced. Split it into named stages — each a function you can name, test and reuse:
clean = raw.pipe(deduplicate).pipe(normalise_text).pipe(add_derived)Also: if you need an intermediate result twice, name it. Computing it inside two separate chains does the work twice and invites them to drift apart.
A word on inplace
You'll see inplace=True in older tutorials. Avoid it. It returns None, so it breaks every chain and turns df = df.drop(..., inplace=True) into a silent way of destroying your frame. It doesn't reliably save memory either — pandas usually copies internally regardless. The pandas team has discussed deprecating it for years, and the ecosystem has already moved on.
Practice
Write it yourself. The answer is there when you want it.
Putting the kettle on…
Starting up…
Write it yourself
not gradedWrite one chain that drops the duplicates, lowercases and strips item, drops the rows missing a rating, adds a rounded price, keeps the orders over 90 cups, sorts by cups and resets the index. Print four columns of it, then print the length of orders to show the original is untouched. Finish with a second chain that uses pipe to print the shape at two points along the way.
Your turn
4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
In a single chain, add a price column (revenue / cups) to orders and return the frame — without assigning to a temporary.
Chain it: drop duplicates, then keep only rows with more than 100 cups, then reset the index.
One chain: add price, then keep only rows where price is above 20. The second step has to see the column the first one made.
Use .pipe() to apply len to orders after dropping duplicates — returning the row count.
