Dates and Times in Python: 10 Code-Along Examples

Learn dates and times in Python by running them. Ten copy-and-run examples covering date and datetime objects, timedelta arithmetic, formatting and parsing, timezones with zoneinfo, daylight saving, and pandas resampling.

The dates and times article works through the two hardest things about temporal data: the arithmetic, which is fiddly, and the timezones, which are genuinely treacherous. This workbook runs both. It starts with plain calendar dates and their arithmetic, moves through datetime objects, formatting, and parsing, then into timedelta durations, and finally the two topics that trip up everyone: timezones with the modern zoneinfo module, and daylight saving, where an hour can vanish or happen twice. It ends in pandas, where the .dt accessor and resampling turn all of this into vectorised operations on whole columns. The rule that runs underneath the timezone half: do your arithmetic in UTC, always.

1. Date objects: calendar days

date represents a calendar day with no time attached. You build one from year, month, and day, read its parts as attributes, and ask which weekday it fell on.

from datetime import date
d = date(1992, 8, 24)
print(d.year, d.month, d.day) # 1992 8 24
print(d) # 1992-08-24 (ISO format by default)
# weekday(): Monday is 0, Sunday is 6
print("weekday number:", d.weekday())
days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
print("that was a", days[d.weekday()])

date is the right type whenever the time of day does not matter: a birthday, a deadline, a report period. Its attributes are read-only integers, so d.year is 1992, and printing a date gives the ISO YYYY-MM-DD form automatically. weekday() returns 0 for Monday through 6 for Sunday, which is the number you index into a list of names or use to detect weekends, and it is calculated correctly for any date in the calendar, so you never compute day-of-week by hand.

2. Date arithmetic and timedelta

Subtracting one date from another gives a timedelta, a span of time, whose .days attribute is the gap. This is how you count the days between two events.

from datetime import date
start = date(2007, 5, 9)
end = date(2007, 12, 13)
gap = end - start # a timedelta, not a number
print(type(gap).__name__) # timedelta
print("days between:", gap.days) # 218
# adding a timedelta moves a date
from datetime import timedelta
deadline = start + timedelta(days=30)
print("30 days after start:", deadline)

Subtraction between dates does not return an integer; it returns a timedelta object, and reading .days off it gives the 218-day gap. This matters because the object also handles leap years and month lengths correctly, so you never have to remember that February is short or that 2007 was not a leap year. Arithmetic works both ways: adding a timedelta to a date moves it forward, which is how you compute a deadline thirty days out without touching a calendar.

3. Datetime: a day plus a time

datetime extends a date with hours, minutes, and seconds, for when the moment matters. It has all the date attributes plus time ones.

from datetime import datetime
dt = datetime(2017, 10, 1, 15, 26, 26)
print(dt.year, dt.month, dt.day) # 2017 10 1
print(dt.hour, dt.minute, dt.second) # 15 26 26
print(dt) # 2017-10-01 15:26:26
# the current moment
now = datetime.now()
print("now:", now)
# build one from a Unix timestamp (seconds since 1970)
stamp = datetime.fromtimestamp(1506871586)
print("from timestamp:", stamp)

datetime is a date with a clock: it carries .hour.minute, and .second alongside the calendar attributes, so datetime(2017, 10, 1, 15, 26, 26) pins an exact moment. datetime.now() grabs the current one, and fromtimestamp converts a Unix timestamp, the seconds-since-1970 count that logs and APIs love, into a readable datetime. Everything the date examples did, arithmetic, weekday, works on datetimes too; the difference is only that the time of day now travels along.

4. Formatting with strftime

strftime turns a datetime into a string in any format you specify, using percent-codes as placeholders. This is how you produce human-readable or system-specific date strings.

from datetime import datetime
dt = datetime(2017, 10, 1, 15, 26, 26)
print(dt.strftime("%Y-%m-%d")) # 2017-10-01
print(dt.strftime("%d/%m/%Y")) # 01/10/2017
print(dt.strftime("%H:%M")) # 15:26
print(dt.strftime("%A, %d %B %Y")) # Sunday, 01 October 2017
print(dt.strftime("%I:%M %p")) # 03:26 PM

Each percent-code is a placeholder that strftime fills from the datetime: %Y is the four-digit year, %m the zero-padded month number, %d the day, %H the 24-hour hour, and %M the minute. The named forms are handy too, %A for the full weekday name, %B for the month name, %p for AM or PM with %I for the 12-hour clock. You compose these into whatever layout you need, which is why the same moment can print as 2017-10-01 for a database or Sunday, 01 October 2017for a reader. The codes are worth keeping on a cheatsheet, since nobody memorises all of them.

5. Parsing with strptime, and ISO format

strptime is the reverse of strftime: it reads a string into a datetime, given the format the string is in. And isoformat produces the one universal string format worth standardising on.

from datetime import datetime
# parse a string INTO a datetime: you must state its format
text = "2017-10-01 15:26:26"
dt = datetime.strptime(text, "%Y-%m-%d %H:%M:%S")
print(dt, "->", type(dt).__name__)
# a different layout needs a matching format string
uk = datetime.strptime("01/10/2017", "%d/%m/%Y")
print(uk)
# isoformat: the unambiguous international standard
print(dt.isoformat()) # 2017-10-01T15:26:26
# and datetime can read its own ISO output back
print(datetime.fromisoformat("2017-10-01T15:26:26"))

strptime needs you to describe the incoming string with the same percent-codes, so "%Y-%m-%d %H:%M:%S" tells it to expect year-month-day then time, and it raises an error if the string does not match, which is a feature since it catches malformed dates loudly. The format string is required because a date like 01/02/2017 is genuinely ambiguous between January and February without knowing the convention. isoformat sidesteps all of that: the YYYY-MM-DDTHH:MM:SS standard sorts correctly as text, parses everywhere, and is the format to store and exchange dates in, with fromisoformat reading it straight back.

6. Timedelta: building and measuring durations

timedelta is a span you can construct directly with keyword arguments, add to datetimes, and collapse to a single number of seconds. It is the arithmetic engine for time.

from datetime import datetime, timedelta
# construct a span from any mix of units
span = timedelta(days=1, hours=6, minutes=30)
print("total seconds:", span.total_seconds()) # 109800.0
start = datetime(2026, 1, 1, 9, 0, 0)
# add and subtract spans from a moment
print("plus the span: ", start + span)
print("a week earlier:", start - timedelta(weeks=1))
# the difference between two datetimes is itself a timedelta
end = datetime(2026, 1, 1, 17, 30, 0)
worked = end - start
print("hours worked: ", worked.total_seconds() / 3600)

timedelta accepts keyword arguments in any units, dayshoursminutesweeks, and normalises them, so a day and six and a half hours becomes one object. total_seconds() is the method to remember: it flattens a whole duration to a single float, which is what you need for computing rates, averages, or, as at the end here, hours worked from two timestamps. Adding and subtracting timedeltas from datetimes moves them around cleanly, handling every rollover across days and months so you never carry hours by hand.

7. Timezones: attaching and converting

A naive datetime has no timezone; an aware one does. The modern zoneinfo module attaches real timezones by IANA name, and astimezone converts a moment from one zone to another, showing the same instant on two clocks.

from datetime import datetime
from zoneinfo import ZoneInfo
# an AWARE datetime: the same instant, labelled with its zone
dt = datetime(2026, 6, 1, 12, 0, 0, tzinfo=ZoneInfo("America/New_York"))
print("New York: ", dt)
# convert to another zone: the clock changes, the instant does not
london = dt.astimezone(ZoneInfo("Europe/London"))
tokyo = dt.astimezone(ZoneInfo("Asia/Tokyo"))
print("London: ", london)
print("Tokyo: ", tokyo)
# UTC is the anchor everything converts through
utc = dt.astimezone(ZoneInfo("UTC"))
print("UTC: ", utc)

Attaching tzinfo=ZoneInfo("America/New_York") makes the datetime aware, meaning it knows which zone its clock reading belongs to, and zoneinfo uses the IANA database so the name carries the region’s full history of rules. astimezone then converts: noon in New York is 5pm in London and 1am the next day in Tokyo, the same instant expressed on three clocks. zoneinfo is the standard-library replacement for the old pytz library, so no extra install is needed on most systems, and it is the current recommended approach. Everything routes through UTC, which is why the next example insists on it.

8. Daylight saving: the hours that break arithmetic

Daylight saving is where naive date arithmetic goes wrong. In spring an hour never happens; in autumn an hour happens twice. The rule that saves you is to convert to UTC before doing any arithmetic across these boundaries.

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
ny = ZoneInfo("America/New_York")
# the night the clocks spring forward in 2026 (2am -> 3am)
before = datetime(2026, 3, 8, 1, 30, tzinfo=ny)
after = before + timedelta(hours=1)
# NAIVE-looking local arithmetic can mislead across the gap
print("local before:", before)
print("local +1h: ", after)
# the SAFE way: convert to UTC, do the maths there, convert back
utc = ZoneInfo("UTC")
before_utc = before.astimezone(utc)
after_utc = before_utc + timedelta(hours=1)
print("\nvia UTC: ", after_utc.astimezone(ny))
# the wall clock jumps 1:30 -> 3:30, because 2:00-3:00 does not exist

On the spring-forward night the local clock skips from 2:00 straight to 3:00, so the 2-to-3 hour simply does not exist, and adding an hour to 1:30 lands you at a wall-clock time that has to jump the gap. The reliable discipline is the one the article states flatly: convert aware datetimes to UTC, do the arithmetic there where every hour is real and equal, then convert back to local for display. UTC has no daylight saving and no gaps, so durations computed in it are always correct, and the only place local time should appear is at the very edges, input and output. The autumn fall-back, where an hour repeats, is disambiguated with the fold attribute, but the UTC rule spares you needing it for arithmetic.

9. Dates in pandas: parse_dates and the .dt accessor

Pandas handles dates at the column level. parse_dates converts date columns to datetimes on load, and the .dt accessor applies datetime operations to a whole column at once.

import pandas as pd
from io import StringIO
csv = StringIO(
"user,start,end\n"
"A,2026-01-01 09:00,2026-01-01 17:30\n"
"B,2026-01-02 10:15,2026-01-02 12:45\n"
"C,2026-01-02 14:00,2026-01-02 18:20\n"
)
# parse the two columns as datetimes on load
sessions = pd.read_csv(csv, parse_dates=["start", "end"])
print(sessions.dtypes)
# .dt exposes datetime parts on the whole column, vectorised
sessions["weekday"] = sessions["start"].dt.day_name()
sessions["hour"] = sessions["start"].dt.hour
# subtracting two datetime columns gives a timedelta column
sessions["duration_min"] = (sessions["end"] - sessions["start"]).dt.total_seconds() / 60
print(sessions[["user", "weekday", "hour", "duration_min"]])

parse_dates=["start", "end"] tells read_csv to convert those columns from text to real datetimes as it loads, so you never parse row by row. The .dt accessor is the pandas equivalent of the single-object attributes from earlier, applied across the whole column at once: .dt.day_name() and .dt.hour compute for every row in one vectorised operation, far faster than a loop. And subtracting two datetime columns yields a timedelta column, on which .dt.total_seconds() flattens each span to a number, exactly as the single-object version did, but for the entire table.

10. Resampling: grouping by time

Resampling groups rows into time buckets, daily, monthly, hourly, so you can count or average over periods. Combined with groupby, it produces per-category trends, the natural end point of temporal analysis.

import pandas as pd
import numpy as np
rng = np.random.default_rng(0)
n = 200
sessions = pd.DataFrame({
"start": pd.date_range("2026-01-01", periods=n, freq="6h"),
"plan": rng.choice(["free", "pro"], size=n),
"duration": rng.integers(5, 120, size=n),
})
# count sessions per DAY
daily = sessions.resample("D", on="start").size()
print("sessions per day (first 3):")
print(daily.head(3))
# median duration per MONTH, split by plan
monthly = (sessions
.groupby("plan")
.resample("ME", on="start")["duration"]
.median())
print("\nmedian duration per month by plan:")
print(monthly)

resample("D", on="start") buckets the rows by calendar day using the start column, and .size() counts how many fell in each, which is how you turn a log of events into a daily time series. The frequency string is the dial: "D" for day, "ME" for month-end, "h" for hour, "15min" for quarter-hours, and many more. The final query is the pattern most real analysis reaches for, chaining groupby("plan") with resample so each subscription plan gets its own monthly trend, which is how you compare how free and pro users behave over time. Resampling is groupby for the clock, and it is where the whole chapter pays off.

Work through these and you have the whole article in practice: date objects and weekday, date arithmetic through timedeltadatetime with time components, strftime formatting and strptime parsing, the ISO standard, timedelta construction and total_seconds, timezone attachment and conversion with zoneinfo, the daylight-saving traps and the UTC rule that avoids them, pandas parse_dates and the .dt accessor, and resampling for time-based grouping. The two habits to carry away are the ones that prevent the most bugs: store and exchange dates in ISO format, and do every calculation in UTC, converting to local time only when a human needs to read it.

See you soon.

View Comments (2)

Leave a Reply

Subscribe to My Newsletter

Subscribe to my email newsletter to get the latest posts delivered right to your email. Pure inspiration, zero spam.

Discover more from Discuss Data Science, Machine Learning and Analytics

Subscribe now to keep reading and get access to the full archive.

Continue reading