Working with dates and times¶
Goal: get today's date, do arithmetic with dates, and turn dates into text and back, using the datetime module.
Why this matters¶
In Module 6 you wrote a diary program that began by asking Today's date:. That was a little absurd. The computer
has a clock. It knows the date, and it can count the days until your holiday, tell you what day of the week you were
born, and stamp every diary entry for you. All of that lives in a standard library module called datetime, and it is
a good second module to learn, because it shows you something modules can give you that math did not: a whole new
kind of value.
Your output will differ
The examples in this lesson were run on Saturday 19 September 2026. Your computer will show its own dates.
Today's date¶
from datetime import date
today = date.today()
print(today)
print(today.year)
print(today.month)
print(today.day)
Take the first line slowly, because there are two new things in it. datetime is the module. Inside it is date,
which is not a function but a type, a new kind of value alongside the ones you know: str, int, float, list,
dict. from datetime import date brings that type into your program, which is the from import from last lesson
doing exactly the job it is best at.
date.today() is a function that lives inside date, and it returns a date value holding the current date. Print
one and you get it in the form year-month-day. A date also knows its own parts. today.year, today.month and
today.day are attributes, values that live inside the date, reached with a dot but without parentheses, just like
math.pi last lesson. They are ordinary integers, so you can do arithmetic with them.
Making a date¶
Any date can be made from its three numbers:
from datetime import date
christmas = date(2026, 12, 25)
course_start = date(2026, 9, 10)
print(christmas)
print(f"The course started on day {course_start.day} of month {course_start.month}.")
print(christmas > course_start)
date(2026, 12, 25) looks like a function call, and in effect it is one: give it a year, a month and a day and it hands
back a date. This is how a new value of any type is made, and you have seen it before without the name: int("12")
and str(5) work the same way. Python checks the numbers make sense, so date(2026, 2, 30) is a ValueError,
with the message day is out of range for month.
The last line shows that dates can be compared with < and >, and they compare the sensible way: a later date is
"bigger". That means if deadline < today: reads exactly as you would say it.
Counting the days between dates¶
Subtract one date from another and you get the gap between them:
from datetime import date
today = date.today()
christmas = date(2026, 12, 25)
gap = christmas - today
print(gap)
print(gap.days)
print(f"{gap.days} days until Christmas.")
The gap is yet another type, called a timedelta, which means a difference in time. Printed on its own it shows
days, then hours, minutes and seconds, which for whole dates are always zero. Its .days attribute is the number you
actually want, as an ordinary integer. Subtract in the other order, today - christmas, and .days is negative.
Adding days to a date¶
Going the other way, from a date and a number of days to a new date, needs a timedelta of your own:
from datetime import date, timedelta
today = date.today()
next_week = today + timedelta(days=7)
last_month = today - timedelta(days=30)
print(f"Today: {today}")
print(f"A week from now: {next_week}")
print(f"Thirty days ago: {last_month}")
You cannot write today + 7, because Python would have to guess whether you meant seven days, weeks or seconds.
timedelta(days=7) says which. That days=7 is a keyword argument: instead of relying on position, the call names
the parameter it is filling. In Module 5 you gave parameters default values in a def line. This is the other side of
that: timedelta has parameters called days, weeks, hours, minutes and seconds, all defaulting to zero, and
the caller names whichever ones it wants. You can write timedelta(weeks=2) or timedelta(days=1, hours=12) the
same way.
Once you have a timedelta, + and - do what you expect. Notice that the date arithmetic takes care of month ends
for you. Thirty days before 19 September is 20 August, and you did not have to know how long August is.
Showing a date nicely¶
2026-09-19 is fine for a file and poor for a person. The .strftime() method, short for string from time,
formats a date however you like:
from datetime import date
today = date.today()
print(today.strftime("%d/%m/%Y"))
print(today.strftime("%A %d %B %Y"))
print(today.strftime("%b %d"))
print(today.strftime("It is %A."))
The string you pass is a template. Each code beginning with % is replaced by a piece of the date, and everything else
is copied through unchanged. The codes you will use most:
| Code | Meaning | Example |
|---|---|---|
%d |
day of the month, two digits | 19 |
%m |
month as a number, two digits | 09 |
%Y |
year, four digits | 2026 |
%y |
year, two digits | 26 |
%B |
month name | September |
%b |
month name, short | Sep |
%A |
day of the week | Saturday |
%a |
day of the week, short | Sat |
%A is quietly remarkable: Python worked out that 19 September 2026 is a Saturday. It can do that for any date,
including the day you were born, which is one of the exercises.
Turning text into a date¶
Dates arrive as text: typed by a user, or read from a file. date.fromisoformat() turns the year-month-day form back
into a date. ISO format is just the international name for that year-month-day order, the same one print uses.
Anything else is a ValueError, so this is a job for the try/except loop from Module 7:
from datetime import date
def ask_for_date(prompt):
"""Keep asking until the user types a date like 2026-12-25, then return it."""
while True:
text = input(prompt)
try:
return date.fromisoformat(text)
except ValueError:
print("Please use the form YYYY-MM-DD, for example 2026-12-25.")
birthday = ask_for_date("When were you born? ")
print(f"You were born on a {birthday.strftime('%A')}.")
When were you born? 25/12/1990
Please use the form YYYY-MM-DD, for example 2026-12-25.
When were you born? 1990-02-30
Please use the form YYYY-MM-DD, for example 2026-12-25.
When were you born? 1990-12-25
You were born on a Tuesday.
ask_for_date() is ask_for_number() with date.fromisoformat in place of int. The pattern is the same every time:
let the conversion try, and ask again when it refuses. The second attempt, 30 February, has the right shape but is not
a real date, and it is refused too.
Because print(some_date) writes the ISO form and fromisoformat() reads it, the two make a matched pair for saving
dates in files. Write f"{some_date}" and read it back with fromisoformat(), and nothing is lost in between.
The time as well¶
date knows nothing about hours and minutes. For those, the module has a second type, datetime, which holds a date
and a time together:
from datetime import datetime
now = datetime.now()
print(now)
print(now.strftime("%H:%M"))
print(now.strftime("%Y-%m-%d %H:%M"))
print(now.date())
Yes, the type has the same name as the module, which confuses everybody once. from datetime import datetime reads
as from the module, import the type. datetime.now() gives the current date and time down to the microsecond,
which is usually more than you want, so .strftime() is used to trim it. Two more codes cover the time: %H for the
hour on the 24-hour clock and %M for the minute. .date() throws the time away and leaves a plain date.
Project: a countdown to everything¶
Here is a file of events, one per line, with the date in ISO form. One line has gone wrong on purpose:
2026-12-25,Christmas
2027-01-01,New Year's Day
2026-09-10,Started the Python course
soon,Holiday
2026-10-31,Halloween
The program reads it and says how far away each event is:
from datetime import date
FILENAME = "events.txt"
def read_lines(filename):
"""Return the lines of a file as a list, without the newline characters."""
lines = []
with open(filename) as file:
for line in file:
lines.append(line.strip())
return lines
def describe(event_date, name, today):
"""Return one line saying how far away the event is."""
days = (event_date - today).days
if days == 0:
return f"{name} is today!"
elif days > 0:
return f"{name} is in {days} days."
else:
return f"{name} was {-days} days ago."
today = date.today()
print(f"Today is {today.strftime('%A %d %B %Y')}.")
print()
for line in read_lines(FILENAME):
text, name = line.split(",")
try:
event_date = date.fromisoformat(text)
except ValueError:
print(f"Skipping '{name}': '{text}' is not a date.")
continue
print(describe(event_date, name, today))
Today is Saturday 19 September 2026.
Christmas is in 97 days.
New Year's Day is in 104 days.
Started the Python course was 9 days ago.
Skipping 'Holiday': 'soon' is not a date.
Halloween is in 42 days.
Almost all of it is old friends. read_lines() is from Module 6, .split(",") from Module 4, the try/except
with continue from Module 7. The new part is small: one subtraction, (event_date - today).days, and an if on
whether the result is zero, positive or negative. When it is negative, -days flips the sign so that the message can
say 9 days ago rather than -9 days.
Change the dates in events.txt to your own, add as many lines as you like, and you have a program worth keeping.
Try it¶
Print today's date in three different formats using strftime(), one of them in your own language's usual order.
Then work out what day of the week 1 January 2000 was, and how many days ago that is. Finally, add a line for today's
date to events.txt and run the countdown, to see the is today! message.
Common mistakes¶
AttributeError: module 'datetime' has no attribute 'today'
You wrote import datetime and then datetime.today(). With that import, the type is datetime.date, so you need
datetime.date.today(). Writing from datetime import date at the top avoids the double name.
TypeError: unsupported operand type(s) for +: 'datetime.date' and 'int'
You wrote today + 7. A date can only be added to a timedelta: today + timedelta(days=7). Make sure timedelta
is in your from datetime import line.
ValueError: day is out of range for month
The numbers do not make a real date, such as date(2026, 2, 30) or date(2026, 4, 31). If they came from a user,
catch the ValueError and ask again.
ValueError: Invalid isoformat string: '25/12/2026'
fromisoformat() accepts only the year-month-day form, with hyphens and leading zeros: 2026-12-25.
Tell the user the exact form to type, as ask_for_date() does.
The date prints, but the month and day are swapped
date(2026, 12, 25) is year, month, day, in that order, always. There is no setting for this. If your country
writes day first, use strftime("%d/%m/%Y") for showing the date, and keep year-month-day everywhere else.
AttributeError: attribute 'year' of 'datetime.date' objects is not writable
You tried some_date.year = 2027. Dates cannot be changed once made. Make a new one instead:
date(2027, some_date.month, some_date.day).
Exercises¶
- Stamped diary. Rewrite the Module 6 diary so it no longer asks for the date. Each entry should be saved as
2026-09-19 16:14: what happened, with the date and time filled in by the program. - Age in days. Ask for the user's date of birth with
ask_for_date(). Print what day of the week they were born on, how many days they have been alive, and roughly how many years that is. - Weekend or weekday. Ask for a date and print its day name and whether it falls on a weekend. Dates have a
method
.weekday()that returns a number: runhelp(date.weekday)to find out which number means which day. - Next birthday. Ask for a date of birth and print the date of the user's next birthday, how many days away it is, and how old they will be. Remember that this year's birthday may already have passed. If it is today, say happy birthday instead.
Solution 1
from datetime import datetime
entry = input("What happened today? ")
stamp = datetime.now().strftime("%Y-%m-%d %H:%M")
with open("diary.txt", "a") as file:
file.write(f"{stamp}: {entry}\n")
print("Saved. Your diary so far:")
with open("diary.txt") as file:
print(file.read())
Only two lines changed from the Module 6 version: the input() for the date is gone, and the stamp comes from
datetime.now().
Solution 2
from datetime import date
def ask_for_date(prompt):
"""Keep asking until the user types a date like 2026-12-25, then return it."""
while True:
text = input(prompt)
try:
return date.fromisoformat(text)
except ValueError:
print("Please use the form YYYY-MM-DD, for example 2026-12-25.")
birthday = ask_for_date("When were you born? ")
today = date.today()
age_in_days = (today - birthday).days
print(f"You were born on a {birthday.strftime('%A')}.")
print(f"You have been alive for {age_in_days} days.")
print(f"That is about {age_in_days // 365} years.")
Someone born on 25 December 1990 was born on a Tuesday and had been alive for 13,052 days on the day this was
written. Dividing by 365 with // gives whole years, ignoring leap days, which is what roughly allows.
Solution 3
from datetime import date
def ask_for_date(prompt):
"""Keep asking until the user types a date like 2026-12-25, then return it."""
while True:
text = input(prompt)
try:
return date.fromisoformat(text)
except ValueError:
print("Please use the form YYYY-MM-DD, for example 2026-12-25.")
chosen = ask_for_date("Which date? ")
day_name = chosen.strftime("%A")
if chosen.weekday() >= 5:
print(f"{chosen} is a {day_name}. That is the weekend!")
else:
print(f"{chosen} is a {day_name}. That is a working day.")
help(date.weekday) says Monday == 0 ... Sunday == 6, so the weekend is 5 and 6, and >= 5 catches both.
The day name still comes from strftime("%A"), which is simpler than a list of names.
Solution 4
from datetime import date
def ask_for_date(prompt):
"""Keep asking until the user types a date like 2026-12-25, then return it."""
while True:
text = input(prompt)
try:
return date.fromisoformat(text)
except ValueError:
print("Please use the form YYYY-MM-DD, for example 2026-12-25.")
birthday = ask_for_date("When were you born? ")
today = date.today()
next_birthday = date(today.year, birthday.month, birthday.day)
if next_birthday < today:
next_birthday = date(today.year + 1, birthday.month, birthday.day)
days = (next_birthday - today).days
age = next_birthday.year - birthday.year
if days == 0:
print(f"Happy birthday! You are {age} today.")
else:
print(f"Your next birthday is on {next_birthday.strftime('%A %d %B %Y')}.")
print(f"That is in {days} days. You will be {age}.")
The birthday is rebuilt in the current year from the birth date's month and day. If that date has already gone,
it is rebuilt in the next year instead. This solution has one known gap: a birthday on 29 February will cause a
ValueError in three years out of four. Handling that is a good stretch task.
Summary¶
from datetime import dategives you thedatetype.date.today()is today,date(2026, 12, 25)is any date..year,.monthand.dayare a date's attributes. Dates can be compared with<and>.- Subtracting two dates gives a
timedelta, and its.daysis the gap as an integer. - Add or subtract
timedelta(days=7)to move a date.days=7is a keyword argument: it names the parameter it fills. .strftime("%A %d %B %Y")formats a date for people.date.fromisoformat("2026-12-25")reads one from text, and raisesValueErrorfor anything else.datetime.now()gives the date and time together.%H:%Mformats the time.