DSM
70 XP
30 minsIntermediate70 XP

Dates & Times with datetime

'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

  • Build datetime objects and read their parts (.year, .month, .weekday())
  • Parse timestamp strings with strptime and an explicit format
  • Format datetimes for humans and filenames with strftime
  • Compute durations and offsets with timedelta
  • Recognize the ISO 8601 standard and the naive-vs-aware timezone split

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

UberTrip duration is a subtraction

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.

CloudflareLog forensics across timezones

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.

ShopifyCohorts and retention windows

'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.

Theory

The core ideas, in plain language.

from datetime import date, datetime, timedelta. A date is a calendar day (2026, 7, 16); a datetime adds a time of day. Both are real objects with named parts — .year, .month, .day, .hour — plus derived answers like .weekday() (Monday=0). They compare correctly (dt1 < dt2), sort correctly, and subtract into timedeltas. The moment a timestamp becomes a datetime object, the whole standard library of time reasoning opens up.
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.

Key Concept
strptime: string → datetime (you declare the format)

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:00

The 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.

Key Concept
strftime: datetime → string (same codes, reversed direction)

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.

Analogy: Timestamps are coordinates, strings are postcards

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.

Watch out
Where time data silently corrupts

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).

Visual Learning

See the concept, then explore it.

The time-data round trip

Strings become objects, objects compute, results become strings again. Click each stage.

Worked Examples

Watch it built up, one line at a time.

Very EasyBuild, inspect, compare

Two order timestamps — which came first, and when?

Step 1 of 1

Objects compare chronologically — no string tricks. strftime('%A') names the weekday; .hour reads a part directly. July 16, 2026 is a Thursday.

Code
01from datetime import datetime
02 
03a = datetime(2026, 7, 16, 9, 15)
04b = datetime(2026, 7, 16, 14, 30)
05print(a < b)
06print(a.strftime('%A'), '-', a.hour, 'h')
Output
True
Thursday - 9 h
Practice Coding

Your turn — write the code.

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>'.

Expected output
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.

Exercises

Prove it. Reach 80% to complete the lesson.

Mastery Gate0% / 80% required
Easy0/2 solved

Which direction does each function go?

What does datetime(2026, 7, 16) - datetime(2026, 7, 1) return?

Medium0/3 solved

A session lasted 2 hours 30 minutes. gap = end - start; gap.days is 0. Why, and what's the right accessor for hours?

ScenarioAn analyst sorts a report by its string date column ('M/D/YYYY' values like '9/1/2026', '10/1/2026', '11/1/2026') and presents 'the last event of the year'. October and November appear BEFORE September in their sorted output.

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

Parse before compare:Strings are parsed to datetimes; min/max run on objects
strftime output:Output combines %A with the ISO date
Hard0/2 solved

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

Guarded parsing:try/except ValueError per row with a skip counter
Duration math:Span uses total_seconds()/60 on a datetime difference
Complete 80% more exercises to unlock.
Interview Prep

How this shows up in real interviews.

A CSV's date column contains values like '03/04/2026'. Walk through how you'd handle it safely.

Show model answer

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.

Show model answer

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?

Show model answer

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.

scratchpad — preview this lesson's challenge anytime
delivery_tracker.pyPython
Ready
Output

Run your code to see the output here.