'03/04/2026' — March 4th or April 3rd? Your answer depends on which country's export you're reading, and guessing wrong silently shifts every event in the dataset by weeks. Dates are the most treacherous data type in the profession — and datetime is how Python tames them.
What you'll learn
What
The datetime module: date and datetime objects that make time computable, strptime to PARSE strings into them (you declare the format), strftime to FORMAT them back out, and timedelta for arithmetic — differences, offsets, and comparisons.
Why
Almost every real dataset has a time column: order timestamps, log lines, sensor readings, sign-up dates. As strings they can only be compared alphabetically (and '9/1' > '10/1' alphabetically!); as datetime objects they subtract, sort, bucket by month, and answer 'how long between?' correctly. Time handling is also where silent corruption loves to hide — format guesses and timezone mixups don't crash, they just shift your data.
Where it's used
Cohort analysis, session durations, SLA calculations, log forensics, churn windows, feature engineering (day-of-week, days-since), and every pandas to_datetime call — which wraps exactly these concepts.
Where this runs in production
Every fare starts with dropoff_time - pickup_time — a timedelta. Surge pricing buckets those trips by hour-of-day and weekday, both single attribute reads once the timestamp is parsed.
Incident timelines splice logs from servers worldwide; that only works because everything is stored in UTC and converted for display — the aware-datetime discipline this lesson introduces.
'Customers acquired in March who reordered within 30 days' is strptime on signup dates, a timedelta(days=30) window, and comparisons — this lesson's toolkit verbatim.
from datetime import datetime launch = datetime(2026, 7, 16, 14, 30) print(launch.year, launch.month, launch.day) # 2026 7 16 print(launch.hour, launch.minute) # 14 30 print(launch.weekday()) # 3 (Mon=0 ... Thu=3) print(launch.date()) # 2026-07-16
Constructor order is big-to-small: year, month, day, hour, minute, second. .weekday() returns Monday=0 through Sunday=6 (its sibling .isoweekday() is Monday=1). .date() drops the time part — handy for grouping events by day. datetime.now() gives the current moment.
datetime.strptime('2026-07-16 14:30', '%Y-%m-%d %H:%M') parses by matching the string against YOUR format declaration: %Y four-digit year, %m month, %d day, %H hour (24h), %M minute, %S second. Mnemonic: strPtime Parses. The format string is a feature, not a chore — it makes the '03/04/2026' ambiguity impossible because YOU state whether it's %m/%d/%Y or %d/%m/%Y. A mismatch raises ValueError — which the error-handling module taught you to catch per-row.
from datetime import datetime
us = datetime.strptime('03/04/2026', '%m/%d/%Y')
eu = datetime.strptime('03/04/2026', '%d/%m/%Y')
print(us) # 2026-03-04 00:00:00 (March 4)
print(eu) # 2026-04-03 00:00:00 (April 3)
iso = datetime.strptime('2026-07-16T14:30:00', '%Y-%m-%dT%H:%M:%S')
print(iso) # 2026-07-16 14:30:00The same string, two formats, two different days — the ambiguity made explicit and therefore safe. The third parse is ISO 8601 (year-month-day, T separator), the international standard: unambiguous, and it even sorts correctly AS A STRING. datetime.fromisoformat('2026-07-16T14:30:00') is the shortcut for exactly that shape.
dt.strftime('%Y-%m-%d') renders a datetime using the identical format codes: strFtime Formats. Beyond the numeric codes: %A full weekday name, %a short, %B full month name, %b short. Two habits: human-facing output can be pretty ('%B %d, %Y' → July 16, 2026), but filenames and logs should be ISO-ish ('%Y-%m-%d') — because ISO strings sort chronologically even when sorted alphabetically, which is why data lakes are full of report_2026-07-16.csv and never report_16-7-26.csv.
from datetime import datetime, timedelta start = datetime(2026, 7, 1, 9, 0) end = datetime(2026, 7, 16, 14, 30) gap = end - start # timedelta print(gap) # 15 days, 5:30:00 print(gap.days) # 15 print(gap.total_seconds()) # 1315800.0 deadline = start + timedelta(days=30) print(deadline) # 2026-07-31 09:00:00
Subtracting datetimes yields a timedelta; adding a timedelta to a datetime yields a new datetime. Mind the two accessors: .days is just the day COMPONENT (truncated), while .total_seconds() is the full duration — for 'how many hours?' use total_seconds()/3600, not .days*24. timedelta accepts days, hours, minutes, seconds, weeks.
A datetime object is a GPS coordinate for time — exact, computable, comparable; you can measure the distance between two of them (timedelta) or move from one by an offset. A date STRING is a postcard describing a place: lovely for humans, but written in a local dialect ('03/04' means different things in different countries) and useless for measurement until geocoded. strptime is the geocoder (declare the dialect, get coordinates); strftime writes a new postcard in whatever dialect the reader needs. Analytics happens in coordinates; presentation happens in postcards — never do math on the postcard.
1) Parsing with a guessed format: %d/%m vs %m/%d succeeds on ambiguous strings and shifts events — confirm the format from the data source, and test with a day > 12 which makes the wrong guess ERROR instead of lie. 2) Sorting date strings: '9/1/2026' > '10/1/2026' alphabetically — parse first, or use ISO strings. 3) gap.days on durations under a day is 0 — use total_seconds(). 4) Mixing naive and aware datetimes — pick one regime per pipeline (UTC-aware for anything multi-source). 5) Doing month arithmetic with timedelta(days=30) — months vary in length; that's an approximation you must own (or reach for dateutil/pandas offsets).
Strings become objects, objects compute, results become strings again. Click each stage.
Two order timestamps — which came first, and when?
Objects compare chronologically — no string tricks. strftime('%A') names the weekday; .hour reads a part directly. July 16, 2026 is a Thursday.
True Thursday - 9 h
Your task
Orders arrive as (order_id, 'DD/MM/YYYY') tuples with a 7-day delivery promise. Parse each date (%d/%m/%Y), compute the promised delivery date (+7 days), and against today = 2026-07-16 print '<id>: LATE by <n> days' or '<id>: on track (<n> days left)'. Finish with 'late orders: <count>'.
ORD-1: LATE by 4 days ORD-2: on track (3 days left) ORD-3: on track (5 days left) late orders: 1
Write your solution in the editor on the right, then hit Run.
Which direction does each function go?
What does datetime(2026, 7, 16) - datetime(2026, 7, 1) return?
A session lasted 2 hours 30 minutes. gap = end - start; gap.days is 0. Why, and what's the right accessor for hours?
What happened?
Parse stamps = ['2026-07-14', '2026-07-02', '2026-07-16'] (%Y-%m-%d), then print the earliest and latest as '<weekday name> YYYY-MM-DD'. Expected: earliest: Thursday 2026-07-02 latest: Thursday 2026-07-16
A pipeline merges event logs from servers in three countries and computes event ordering from naive local timestamps. What's the professional fix?
events = [('deploy', '16/07/2026 09:15'), ('alert', '16/07/2026 11:45'), ('bad', 'oops'), ('fix', '16/07/2026 12:30')]. Parse with '%d/%m/%Y %H:%M', skipping unparseable rows with a per-row try/except (count them). Print each good event as '<name> at <HH:MM>', then minutes between first and last good event, then the skip count. Expected: deploy at 09:15 alert at 11:45 fix at 12:30 span: 195.0 minutes skipped: 1
A CSV's date column contains values like '03/04/2026'. Walk through how you'd handle it safely.
First, establish the format from AUTHORITY, not inspection: the source system's docs, the exporting team, or the locale of the producing application — '03/04/2026' is March 4 in a US export and April 3 in a European one, and eyeballing samples can't settle it. Second, validate the claim against the data: scan for any value with the first segment > 12 (e.g. '25/04/2026') — its existence PROVES day-first; if the full column stays ≤ 12 in both positions, the data alone is genuinely ambiguous and the source must answer. Third, parse with the explicit format — datetime.strptime(value, '%m/%d/%Y') or '%d/%m/%Y' — never a permissive guesser, because a wrong guess doesn't error, it silently swaps months and days for a subset of rows, which is the worst possible failure mode: plausible, partial, and weeks-shifted. Fourth, wrap the parse in per-row try/except ValueError with skip-and-count so genuinely corrupt values surface as a reported number instead of killing the job. Finally, once parsed, write anything you re-emit as ISO 8601 (%Y-%m-%d) so the ambiguity dies at your stage instead of propagating. In pandas the same reasoning appears as pd.to_datetime(col, format='...') — always pass format for ambiguous data.
Explain naive vs aware datetimes and the rule that keeps multi-source time data correct.
A naive datetime has no timezone attached — datetime(2026, 7, 16, 9, 0) is 'nine o'clock' with the zone implied by context. An aware datetime carries tzinfo — an explicit UTC offset (via timezone.utc or zoneinfo.ZoneInfo('America/New_York')) — making it an unambiguous instant. Python enforces the distinction: subtracting or comparing naive against aware raises TypeError, which is a feature — it's the language refusing to compute an answer that would be meaningless. Naive is acceptable when a dataset genuinely lives in one implicit zone (a single store's POS logs). The moment sources multiply — servers in different regions, mobile clients, third-party APIs — naive local times become incomparable: 09:00 Tokyo is hours before 09:00 London, but naive objects compare them equal-ish. The rule: STORE AND COMPUTE IN UTC, CONVERT AT THE EDGES — ingest by converting everything to aware UTC, do all arithmetic and ordering there, and call .astimezone(user_zone) only for display. DST is the sharpest reason: local wall-clock time repeats an hour every fall (01:30 happens twice) and skips one every spring, so durations computed in local time are wrong twice a year; UTC has no DST, so the math is always valid. zoneinfo handles the conversion rules, including DST, from the IANA database.
How do you compute 'the same day next month', and why is timedelta the wrong tool?
timedelta measures FIXED durations — exact counts of days/seconds — and months aren't fixed: 28 to 31 days. date + timedelta(days=30) from January 31 gives March 2 (in a non-leap year), not 'end of February', and from July 15 gives August 14, not August 15. Calendar arithmetic needs calendar-aware logic. Options in order of preference: in pure Python, dateutil's relativedelta(months=1) handles it, clamping January 31 + 1 month to February 28/29; in pandas, pd.DateOffset(months=1) and period logic do the same; hand-rolled, you increment the month field yourself and clamp the day to that month's length (calendar.monthrange gives it) — doable but exactly the kind of edge-case code libraries exist to own. The interview-worthy insight is the distinction itself: durations (timedelta — physics time, always valid) versus calendar offsets (relativedelta/DateOffset — human time, needs rules). Billing cycles, 'monthly' cohorts, and anniversaries are calendar problems; SLAs measured in hours and session lengths are duration problems. Choosing the wrong category is why some subscriptions renew on the 2nd of March instead of the last day of February.
Common Mistakes to Avoid
1) Guessing %m/%d vs %d/%m — confirm from the source; a wrong guess shifts data silently. 2) Sorting or comparing date STRINGS — parse first (ISO strings are the lone exception). 3) gap.days for sub-day durations (it's 0) and gap.seconds for multi-day ones (it wraps) — total_seconds() is the duration. 4) timedelta(days=30) as 'one month' — months vary; use calendar-aware offsets. 5) Mixing naive and aware datetimes — one regime per pipeline, UTC-aware when sources vary. 6) datetime.now() inside analysis logic — pin 'today' for reproducibility, inject the clock. 7) Pretty formats in filenames — ISO %Y-%m-%d sorts; 'July 16' doesn't.
Ask the AI Tutor
Try these prompts in the AI Tutor panel: • 'Flash-card me on format codes: show a timestamp, I write the strptime format.' • 'Give me five duration problems and check whether I reach for .days, .seconds, or total_seconds() correctly.' • 'Explain the DST fallback bug with a concrete log example.' • 'Design the date handling for a multi-region order pipeline with me.' • 'Interview mode: ask me the ambiguous-date-column question and grade my answer.'
Glossary
datetime / date — computable timestamp / calendar day objects. strptime — Parse string → datetime against a declared format. strftime — Format datetime → string with the same codes. format codes — %Y year, %m month, %d day, %H hour (24h), %M minute, %S second, %A weekday name, %B month name. timedelta — a duration; supports + - and comparisons. total_seconds() — the full duration in seconds (vs the .days/.seconds COMPONENTS). ISO 8601 — YYYY-MM-DD(THH:MM:SS), the unambiguous standard that sorts as text. naive / aware — without / with timezone info. UTC — the zero-offset reference zone; store and compute here. zoneinfo — Python's IANA timezone database access. DST — daylight saving time, the reason local-time math fails twice a year.
Recommended Resources
• Docs: the datetime module page — bookmark the strftime/strptime format-code table; you'll consult it for years. • Read: the zoneinfo module intro for when timezones become real in your work. • Practice: parse the timestamps out of any log file on your machine and compute the gap between first and last line. • Next in DSM: dates conquered, the other messy string type awaits — Regex for Data: patterns, groups, findall, and extracting structure from free text.
Recap
✓ Parse once at ingestion (strptime + explicit format), compute as objects, format once at the exit (strftime). ✓ strPtime Parses, strFtime Formats — same codes, opposite directions. ✓ datetime - datetime = timedelta; use total_seconds() for durations, .days only for whole days. ✓ Never guess %m/%d vs %d/%m — confirm from the source; never sort date strings (except ISO). ✓ timedelta is fixed duration; months need calendar-aware offsets. ✓ Multi-source time = aware UTC in storage and math, local zones only at display. Next up: Regex for Data. Timestamps were structured strings; now the unstructured ones — regular expressions for finding, extracting, and cleaning patterns buried in free text.
Run your code to see the output here.