Adding and changing columns
Creating columns, and the chained assignment that silently does nothing.
df["new"] =.assign()np.wherepd.cut.loc[mask, col] =Copy-on-WriteWatch it happen
Play it through, or step back and forth yourself.
ordersdf["price"] = df["revenue"] / df["cups"]
df["price"] = df["revenue"] / df["cups"]. Assigning to a name that doesn't exist creates it; assigning to one that does replaces it. The right-hand side is an ordinary vectorised expression — no loop.
The idea
Assigning to a column name that doesn't exist creates it; assigning to one that does replaces it. The right-hand side is an ordinary vectorised expression — no loop:
orders["price"] = orders["revenue"] / orders["cups"]
orders["cups"] = orders["cups"] * 2Conditional columns
orders["band"] = np.where(orders["cups"] > 100, "busy", "quiet")
orders["band"] = pd.cut(
orders["cups"], bins=[0, 90, 120, 999], labels=["quiet", "steady", "busy"]
)np.where is the two-outcome case you already know. pd.cut bands a continuous column into labelled ranges — and returns a category, so it sorts in band order rather than alphabetically. pd.qcut does the same by quantile, giving equal-sized groups.
assign returns a new frame
orders.assign(
price=lambda d: d["revenue"] / d["cups"],
busy=lambda d: d["cups"] > 100,
)Same job, but it returns a copy rather than editing in place — which is what makes it chainable. Use a lambda when a new column depends on another one you're defining in the same call, since d refers to the frame as it is at that point.

The trap: chained assignment
This looks reasonable and does not work:
orders[orders["cups"] > 100]["price"] = 0 # writes nowhereThere are two bracket operations. The first produces a temporary frame; the second writes into that temporary; then it's discarded. Your original is untouched.
Historically pandas emitted a SettingWithCopyWarning here — a warning famous for being ignored, partly because it sometimes appeared when nothing was wrong, and the write sometimes worked anyway depending on memory layout.
The fix is to do it in one indexing operation:
orders.loc[orders["cups"] > 100, "price"] = 0 # one bracket — worksOne bracket, not two. That's the whole rule, and it's the same instinct as the NumPy view-versus-copy lesson.
What changed in pandas 3
Copy-on-Write is now the default, and it makes the behaviour consistent: an operation never modifies its parent. So chained assignment now reliably does nothing, rather than working by accident on some layouts and not others.
That's a real improvement — a silent failure that happens every time is far easier to find than one that happens sometimes. The habit doesn't change: select once, then assign.

Renaming and dropping
orders.rename(columns={"cups": "cups_sold"})
orders.drop(columns=["rating"])
orders.insert(1, "price", values) # at a specific positionThese all return new frames by default. You'll see inplace=True in older code; it's discouraged now — it doesn't save memory, it breaks chaining, and it makes code harder to reason about.
See it run
The lesson's code, ready to run and to fiddle with.
Putting the kettle on…
Starting up…
Worked example
not gradedAlready written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.
tryCompare the two "changed anything?" lines — that is the whole lesson.
Your turn
4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Add a price column to a copy of orders — revenue / cups — and return the frame.
Do the same with .assign() instead, returning a new frame with a price column.
Add a band column that is "busy" where cups is over 100 and "quiet" otherwise, and return the frame.
Set cups to 0 for every row over 100, and return the frame. The starter uses chained assignment and silently does nothing — fix it.
