double = lambda x: x * 2
def double_named(x):
return x * 2
print(double(21), double_named(21))42 42
Ricky Macharm
September 25, 2026
A few of the posts on this blog lean on the same small trick: a function written inside a pandas chain, so that one step can see what the step before it just built. It works, it is short, and reading it does not tell you why it is there.
pandas 3 has a shorter way to write it. This post takes three snippets I published here, runs them again exactly as they were written, and then does the same jobs with pd.col() so the two can be put side by side. Every code block below runs when this page is built, and the output under it is what the code printed.
A lambda is a function written where it is used, without a name:
42 42
Same job, two spellings. The only reason to reach for the first is that it fits inside another call, which is somewhere a def statement cannot go. That is the whole idea.
In pandas it turns up for a less obvious reason. When pandas runs a method, it works out the arguments first and then runs the method. Inside a chain, the frame that a step is about to produce does not exist yet at that moment. A lambda puts the calculation off until the step is actually running, when the frame does exist.
This is the one I wrote about in Lambda Functions: How I tend to use them, using the same CSV that post used. Here it is again, unchanged:
Date Close Month
2023-01-02 3773.956049 January
2023-01-03 3438.878440 January
2023-01-04 3858.597920 January
Date datetime64[us]
Close float64
Month str
Look at the second line. Month reads the Date column, and that column only becomes a date on the line above — inside the same assign call. Both lines are the same call, so nothing is committed until the call runs. Writing it without the lambda, as most people first try, does not work:
AttributeError - Can only use .dt accessor with datetimelike values
The same post used a second pattern, a lambda handed to agg so that each group is reduced by my own calculation:
Month
April 3245.80
August 3268.60
December 3247.84
February 3226.91
January 3165.53
July 3331.57
June 3183.17
March 3323.71
May 3269.06
November 3290.07
October 3183.56
September 3240.97
Lambdas show up in the Karachi property prices post too, where they clean column names and split a price into a number and a currency. That post read a CSV of its own, so here is the same code on three rows in the same shape:
karachi = pd.DataFrame({
"property type": ["Flat", "House", "Plot"],
"price": ["2.5 Crore PKR", "4 Crore PKR", "75 Lakh PKR"],
})
print(karachi
.rename(columns=lambda x: x.replace(" ", "_"))
.assign(price_=lambda x: x["price"].str.split(" ", expand=True)[0],
currency_name=lambda x: x["price"].str.split(" ", expand=True)[1])
.astype({"price_": float})
.to_string(index=False))property_type price price_ currency_name
Flat 2.5 Crore PKR 2.5 Crore
House 4 Crore PKR 4.0 Crore
Plot 75 Lakh PKR 75.0 Lakh
And in the weather data wrangling post, the same assign shape appears one more time — a date column converted, then a month number taken from the column that conversion just made:
dteday hum mnth
2011-01-01 0.81 1
2011-02-14 0.80 2
2011-03-09 0.63 3
Three posts, one pattern: convert something, then use the result of the conversion in the same call. The lambda is what makes the second half possible.
The lambda is not a loop, and that matters. It is called once, with the whole frame as its argument, and inside that call x['Date'] is a full column — the arithmetic runs on the column, at pandas speed. A lambda handed to .apply() row by row is a different animal entirely: that one is Python running per row and it is slow. The chain lambdas in these posts are the fast kind.
The reason they are needed at all is ordering. pandas evaluates the arguments of a method before it runs that method. In a chain, the value being built is not available to its own arguments yet. So the lambda is a way of saying calculate this later, when the frame is ready.
pandas 3 adds a second way to say exactly that. pd.col("name") is not the column; it is a stand-in for a column — the documentation calls it “a deferred object representing a column of a DataFrame”, and anywhere that accepts lambda df: df[col_name] will also accept pd.col(col_name).
Here are the three snippets again, written that way:
Date Close Month
2023-01-02 3773.956049 January
2023-01-03 3438.878440 January
2023-01-04 3858.597920 January
same result as the lambda version: True
The column is named once — pd.col("Date") — instead of being named once for the frame and again inside the function body.
The second snippet needs a correction, and it is a useful one. That agg(lambda ...) was never doing the deferral job at all. agg hands each group to the function one at a time, so a lambda there is just a small function, and np.percentile with its own keyword does the same work. What the two lines below check is that the numbers are identical either way:
quarterly percentiles identical: True
Month
April 3245.80
August 3268.60
December 3247.84
February 3226.91
January 3165.53
July 3331.57
June 3183.17
March 3323.71
May 3269.06
November 3290.07
October 3183.56
September 3240.97
v3_bikes = (bikes
.assign(dteday=pd.to_datetime(bikes['dteday']))
.assign(mnth=pd.col("dteday").dt.month))
with_lambda = (bikes
.assign(dteday=pd.to_datetime(bikes['dteday']),
mnth=lambda x: x['dteday'].dt.month))
print("month column identical:", v3_bikes["mnth"].tolist() == with_lambda["mnth"].tolist())
print("both month columns:", with_lambda["mnth"].tolist())month column identical: True
both month columns: [1, 2, 3]
The third snippet is where pd.col() earns its place, because there the lambda really was postponing the calculation. Nothing is delayed with pd.col("dteday"), so the line reads in the order it runs, and the conversion is separated from the step that uses its result — which is what makes the failure in the earlier section impossible to write by accident.
It is not a replacement for every lambda in those posts. The rename(columns=...) lambda has no equivalent, because rename is not working on a frame’s contents — it is working on the labels:
TypeError - boolean value of an expression is ambiguous
The rule of thumb: if the argument is about a column of the frame, pd.col() can probably write it. If it is about the column labels, or about rows one at a time, it cannot, and the lambda stays. The release notes for 3.0 call this “initial support” and name three places it works — assign, loc, and setting by column — so it is early.
Two things worth knowing before the next run of an old notebook, in case the reader hits them at the same time:
a string column now reports: str
the old default was: object
import warnings
df = pd.DataFrame({"a": [1, 2, 3], "b": [10, 20, 30]})
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
df["a"][df["b"] > 15] = 99
print("warning:", caught[0].category.__name__ if caught else "none")
print("the write did not land:", df.to_dict("records"))warning: ChainedAssignmentError
the write did not land: [{'a': 1, 'b': 10}, {'a': 2, 'b': 20}, {'a': 3, 'b': 30}]
Text columns no longer report as object — they report as str. And a two-step write, df["a"][df["b"] > 15] = 99, no longer writes anything: pandas warns and the frame is left alone. The single-step df.loc[df["b"] > 15, "a"] = 99 is what works now.
pd.col() entry is under Other enhancementspandas.col in the API reference