assign() & Method Chaining
Create new columns with assign() without modifying the original, and build readable step-by-step pipelines with method chaining and pipe().
Analysis code often looks like this: make a copy, add a column, filter, sort, pick columns, reset the index — each step in its own line with a new variable name (df2, df3, df_final…). assign() plus method chaining lets you write the whole recipe as one readable pipeline, top to bottom, without touching the original data.
text
player team runs balls fours sixes
0 Virat RCB 741 480 62 38
1 Rohit MI 417 300 40 25
2 Dhoni CSK 161 110 11 14
3 Gill GT 890 560 85 33
4 Pant DC 446 290 41 22Definition
DataFrame.assign(**columns)returns a new DataFrame with the given columns added (or replaced). Each keyword is a column name; its value can be a Series/array/scalar, or a function (usually a lambda) that receives the DataFrame and returns the column.
Method chaining is the style of calling one method directly on the result of the previous one —
df.assign(...).query(...).sort_values(...)— so each line is one transformation step and no intermediate variables are needed.
- New DataFrame —
assignnever modifiesdf; you see the change only in the returned object. - Callable values —
lambda d: ...receives the DataFrame as it is at that point in the chain, including columns created earlier in the sameassign. - Keyword names — column names must be valid Python identifiers (
strike_rate, not"Strike Rate"). For other names, unpack a dict:assign(**{"Strike Rate": ...}).
Syntax
python
df.assign(new_col=value_or_function, other_col=..., ...)
result = (
df
.assign(...) # add columns
.query("...") # filter rows
.sort_values(...) # order
.loc[:, [...]] # choose columns
.reset_index(drop=True)
)
df.pipe(func, *args, **kwargs) # func(df, *args, **kwargs) inside a chain| Parameter | Meaning | Default |
|---|---|---|
**kwargs (assign) | column_name=value; value is a scalar, list/array/Series, or a callable f(df) | — |
func (pipe) | a function whose first argument is the DataFrame | required |
*args, **kwargs (pipe) | extra arguments forwarded to func | — |
Example — assign a new column
python
import pandas as pd
df = pd.DataFrame({
"player": ["Virat", "Rohit", "Dhoni", "Gill", "Pant"],
"team": ["RCB", "MI", "CSK", "GT", "DC"],
"runs": [741, 417, 161, 890, 446],
"balls": [480, 300, 110, 560, 290],
"fours": [62, 40, 11, 85, 41],
"sixes": [38, 25, 14, 33, 22],
})
out = df.assign(strike_rate=(df["runs"] / df["balls"] * 100).round(1))
print(out)
print("original columns:", list(df.columns))text
player team runs balls fours sixes strike_rate
0 Virat RCB 741 480 62 38 154.4
1 Rohit MI 417 300 40 25 139.0
2 Dhoni CSK 161 110 11 14 146.4
3 Gill GT 890 560 85 33 158.9
4 Pant DC 446 290 41 22 153.8
original columns: ['player', 'team', 'runs', 'balls', 'fours', 'sixes']How it works: assign returned a new DataFrame with strike_rate added at the end. df itself still has its original 6 columns.
Example — lambdas that use earlier new columns
python
out2 = df.assign(
boundary_runs=lambda d: d["fours"] * 4 + d["sixes"] * 6,
boundary_pct=lambda d: (d["boundary_runs"] / d["runs"] * 100).round(1),
)
print(out2[["player", "runs", "boundary_runs", "boundary_pct"]])text
player runs boundary_runs boundary_pct
0 Virat 741 476 64.2
1 Rohit 417 310 74.3
2 Dhoni 161 128 79.5
3 Gill 890 538 60.4
4 Pant 446 296 66.4How it works: keyword arguments are processed in order. When boundary_pct's lambda runs, d already contains boundary_runs. That is why lambdas are preferred inside assign: they always see the current DataFrame of the chain, not the old df variable.
Example — a full method chain
python
result = (
df
.assign(strike_rate=lambda d: (d["runs"] / d["balls"] * 100).round(1))
.query("runs > 400")
.sort_values("strike_rate", ascending=False)
.loc[:, ["player", "team", "runs", "strike_rate"]]
.reset_index(drop=True)
)
print(result)text
player team runs strike_rate
0 Gill GT 890 158.9
1 Virat RCB 741 154.4
2 Pant DC 446 153.8
3 Rohit MI 417 139.0How it works: wrap the chain in brackets ( ... ) so Python lets you break it across lines. Read it like a recipe: add strike rate → keep 400+ run scorers → sort by strike rate → keep 4 columns → renumber rows. Dhoni (161 runs) was filtered out.
Example — pipe() for your own functions
python
def add_sr(d):
return d.assign(strike_rate=(d["runs"] / d["balls"] * 100).round(1))
print(df.pipe(add_sr).head(2))text
player team runs balls fours sixes strike_rate
0 Virat RCB 741 480 62 38 154.4
1 Rohit MI 417 300 40 25 139.0df.pipe(add_sr) is the same as add_sr(df), but it fits inside a chain: df.pipe(clean).pipe(add_sr).query(...).
Note (pandas 3): pandas 3 adds
pd.col("name")expressions, sodf.assign(double=pd.col("runs") * 2)works without a lambda. Lambdas still work everywhere and are what you will see in most existing code.
assign vs other ways to add a column
| Way | Modifies df? | Position | Chainable? |
|---|---|---|---|
df["sr"] = ... | ✅ yes (in place) | end | ❌ (statement) |
df.assign(sr=...) | ❌ returns new DataFrame | end | ✅ |
df.insert(1, "country", "India") | ✅ yes, returns None | any position you choose | ❌ |
df.pipe(func) | depends on func (usually no) | — | ✅ |
Why chaining fits pandas 3
With Copy-on-Write always on in pandas 3, each step returns a new object that shares memory with the previous one until something is actually changed, so chains are cheap. Chaining also avoids the old SettingWithCopyWarning situations completely, because you never modify a slice — you always build a new result. inplace=True is discouraged; reassignment (df = df.assign(...)) is the modern style.
Common mistakes
- ❌
df.assign(sr=...)and expectingdfto change. ✅df = df.assign(sr=...). - ❌
df.assign("Strike Rate"=...)— SyntaxError. ✅df.assign(**{"Strike Rate": ...}). - ❌ Using
df["col"]inside a chain after a filter — it refers to the old DataFrame and the index may not line up. ✅ Uselambda d: d["col"]. - ❌ Forgetting the outer brackets on a multi-line chain. ✅ Wrap the whole chain in
( ... ). - ❌ Chains so long nobody can debug them. ✅ 4–8 steps is comfortable; split into named
pipefunctions if it grows.
Interview tip
Q: Why use lambda d: ... inside assign instead of df["x"]?
A: The lambda receives the DataFrame at that step of the chain, so it sees rows that survived earlier filters and columns created earlier in the same assign. Referencing the outer df would use stale data and could misalign indexes.
Key points to remember
- df.assign(new=...) returns a NEW DataFrame with the column added — the original is unchanged.
- Values in assign can be scalars, arrays/Series, or a callable that receives the current DataFrame.
- Keyword arguments are processed in order, so later lambdas can use columns created earlier in the same assign.
- Method chaining writes a pipeline one step per line; wrap it in brackets ( ... ) to break lines.
- df.pipe(func) calls func(df) and lets your own functions sit inside a chain.
- Names with spaces need dict unpacking: assign(**{'Strike Rate': ...}).
- In pandas 3 (Copy-on-Write) chaining is cheap and avoids chained-assignment problems; prefer reassignment over inplace=True.
Try it yourself
assign() with plain values and lambdas
A full method chain
Practice: chain with pipe()
Revision Flashcards
Tap a card to flip it and see the answer.
