Dates and times
A date read from a file is a string, and strings sort in the wrong order.
pd.to_datetime.dt accessorDatetimeIndexpartial string indexingTimedeltaWatch it happen
Play it through, or step back and forth yourself.
dfA date read from a CSV is a str until you say otherwise. It looks like a date and prints like a date, and nothing warns you.
The idea
orders["date"] looks like a date, prints like a date, and is a str. Nothing warns you, and everything works — badly.
Why it matters
Strings sort lexicographically, character by character. So "2026-03-3" sorts after "2026-03-21", because "3" comes after "2". One missing zero and your chronology is silently wrong.
You also can't subtract them, can't ask which weekday they fall on, and can't resample by month. All of that needs real timestamps.
Parsing
orders["date"] = pd.to_datetime(orders["date"])
orders["date"].dtype # datetime64[ns]Better still, do it on the way in:
pd.read_csv("orders.csv", parse_dates=["date"])For input that fights back:
pd.to_datetime(s, format="%d/%m/%Y") # you know the layout — faster, fails loudly
pd.to_datetime(s, dayfirst=True) # 05/03 is 5 March, not 3 May
pd.to_datetime(s, errors="coerce") # unparseable values become NaTPassing format= is worth the keystrokes on a large file — it's much faster than letting pandas guess per value, and a value that doesn't match raises instead of being quietly reinterpreted.
NaT — "not a time" — is the datetime equivalent of NaN, and .isna() finds it just the same.
The .dt accessor
Date parts live behind .dt, exactly as string methods live behind .str:
orders["date"].dt.year
orders["date"].dt.month
orders["date"].dt.day_name() # "Monday"
orders["date"].dt.dayofweek # 0 = Monday
orders["date"].dt.quarter
orders["date"].dt.to_period("M") # 2026-03Each is an ordinary column, so they group like anything else — orders.groupby(orders["date"].dt.day_name())["cups"].mean() gives you the weekly pattern in one line.

Dates as the index
This is the move that makes time series work pleasant:
ts = orders.set_index("date").sort_index()
ts.loc["2026-03"] # the whole of March
ts.loc["2026"] # the whole year
ts.loc["2026-03-02":"2026-03-05"] # a range — end INCLUDEDThat's partial string indexing: with a DatetimeIndex, pandas understands "2026-03" as a period rather than a label, and gives you everything inside it. It's why nearly every time series recipe starts by putting the date in the index.
Sort it. Unsorted date indexes give wrong slices and warnings, and .sort_index() is one call.

Durations
orders["date"].max() - orders["date"].min() # Timedelta("5 days")
(pd.Timestamp("2026-04-01") - orders["date"]).dt.days
orders["date"] + pd.Timedelta(days=7)
orders["date"] + pd.DateOffset(months=1) # calendar-awareTimedelta is a fixed span — seven days is always 168 hours. DateOffset is calendar-aware: "one month" from 31 January lands on 28 February. Use the second whenever a human would say "next month".
Practice
Write it yourself. The answer is there when you want it.
Putting the kettle on…
Starting up…
Write it yourself
not gradedPrint the dtype of date as it arrives, parse it, and print the dtype again. Print the first few day names from .dt. Then put the parsed dates on the index, sort it, and print everything that happened on 2026-03-04 — one label, several rows. Finish by printing the span between the first date and the last.
Your turn
4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Convert orders["date"] to real timestamps and return the resulting dtype.
Return the weekday name for every row of orders.
Return the total cups per weekday name — group by the parsed date's day name.
How long does orders span? Return max - min of the parsed dates.
