It removes the time. It also quietly removes the datetime.
What it does
Have you ever done the obvious thing — df['when'].dt.date, because you wanted days rather than timestamps — and then found that the next .dt call in the chain no longer worked? That is not a bug you introduced two lines later. It is what .dt.date does.
.dt.date returns the calendar date of every value in a datetime column. What it hands back is not a datetime column with the time zeroed out. It is a column of plain Python datetime.date objects, which means the dtype is object, which means Pandas is now storing a pointer to a Python object per row instead of one integer per row. Everything datetime-shaped goes with it: the .dt accessor, string-based slicing, resample, and the timezone if the column had one.
Nine times out of ten what people actually wanted is .dt.normalize(), which sets the time to midnight and leaves you with a datetime column.
Official documentation: Series.dt.date and Series.dt.normalize.
A worked example, on real data
The USGS serves every earthquake it has ever recorded through an open query API. Two months of magnitude 4.5 and up is 1,462 rows, and the timestamps are UTC:
import pandas as pd
url = ('https://earthquake.usgs.gov/fdsnws/event/1/query?format=csv'
'&starttime=2026-06-01&endtime=2026-08-01&minmagnitude=4.5')
df = pd.read_csv(url, usecols=['time', 'mag', 'place'], parse_dates=['time'])
df['time'].dt.date.head(3)
0 2026-07-31
1 2026-07-31
2 2026-07-31
Name: time, dtype: object
The values print like dates. The dtype line is the tell: object, where the original column was datetime64[us, UTC]. Ask that column for anything datetime-ish and Pandas no longer recognizes it:
df['time'].dt.date.dt.year
AttributeError: Can only use .dt accessor with datetimelike values
.dt.normalize() answers the same question and stays a datetime:
df['time'].dt.normalize().head(3)
0 2026-07-31 00:00:00+00:00
1 2026-07-31 00:00:00+00:00
2 2026-07-31 00:00:00+00:00
Name: time, dtype: datetime64[us, UTC]
Midnight is showing, which is the one cosmetic price. In exchange, .dt.year still works, the UTC marker survived, and the column is a fifth of the size — 11,696 bytes against 58,480 for the same 1,462 dates, by memory_usage(deep=True).
The difference stops being cosmetic the moment you group. Both work:
df.groupby(df['time'].dt.date)['mag'].size().head(3)
df.groupby(df['time'].dt.normalize())['mag'].size().head(3)
time
2026-06-01 14
2026-06-02 14
2026-06-03 21
Name: mag, dtype: int64
time
2026-06-01 00:00:00+00:00 14
2026-06-02 00:00:00+00:00 14
2026-06-03 00:00:00+00:00 21
Name: mag, dtype: int64
Same counts. But the first index is object and the second is a DatetimeIndex, and only one of them can be asked a follow-up question:
df.groupby(df['time'].dt.date)['mag'].size().loc['2026-07-04']
KeyError: '2026-07-04'
The keys are date objects, not strings, so reaching that row would take datetime.date(2026, 7, 4). The normalized version takes the string — it returns 28 — and it will also resample('W'), where the object index raises TypeError: Only valid with DatetimeIndex, TimedeltaIndex or PeriodIndex.
Mistakes people make
Assuming the dtype is still a datetime because the values look like dates. Printing a Series does not tell you. df.dtypes does, and object in a column of dates is almost always this.
Losing the timezone without noticing. .dt.date on the UTC column above gives the UTC date. If the interesting question is which local day something happened on, convert first — that is dt.hour's territory, and the same tz_convert fixes both.
Using it as a merge key on one side only. I expected this to fail quietly. It does not — Pandas 3 stops you:
left.merge(right, on='day')
ValueError: You are trying to merge on object and datetime64[us] columns
for key 'day'. If you wish to proceed you should use pd.concat
Worth knowing, because the plain comparison left['day'] == right['day'] does return True for the same day: a date and a midnight Timestamp compare equal element by element. So a filter built on == works and a merge on the same two columns raises. Convert one side and stop thinking about it.
Reaching for it when what you want is a string. If the destination is a label, a filename or a concatenation, .dt.strftime('%Y-%m-%d') says so directly, and I build those format codes at strfti.me rather than from memory.
Where it shows up in Bamboo Weekly
Three Bamboo Weekly solutions use .dt.date, and all three use it for the same reason: they need a day-level key, and nothing downstream needs a datetime.
Bamboo Weekly #101: Los Angeles fires creates a date_only column with .dt.date and feeds it to pivot_table as the columns — one column per day rather than one per timestamp. As a grouping key that is exactly what .dt.date is for.
Bamboo Weekly #113: US airport traffic is the other legitimate use: .dt.date.astype(str) glued to a time string and handed back to to_datetime, because the file stores the date and the hour in two different columns.
Bamboo Weekly #163: Daylight saving time is the one where dates are the subject rather than the machinery — which countries change their clocks, and on what date — and it is worth reading alongside the timezone note above.
Practice it
Work through a dt.date exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/dt-date/
Go deeper
If you want a day-level key that is still a first-class datetime, the choices are .dt.normalize(), .dt.to_period('D'), or resample on a datetime index. The numeric parts of a date are on dt.year and dt.month, the clock half on dt.hour, and none of it starts until to_datetime has run. The Pandas user guide's time series chapter covers normalizing, flooring and rounding together.
More Pandas videos on Python and Pandas with Reuven Lerner.
Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.
Related methods
.dt.year()— when you want one calendar part rather than the whole datepd.to_datetime()— which has to run first, and whose dtype dt.date quietly throws away