How to Find the First/Last Day of a Year from a given Date
by Christoph Schiessl on Python
Recently, one of my followers, who is still a Python beginner, approached me with the following question:
How can I calculate the boundary dates, meaning a year's first and last days? As input, I have a string with the following format: "YYYY-MM-DD". The dates in the output should have the same format.
Ok, that's an excellent question. Let's break it down ...
We will use the built-in datetime
module from the standard library that already implements all the necessary functionality. Our job is to compose the built-in functionality to achieve the desired result. So, first off, we have to talk about the relevant functionality in this module, and then we will write separate functions to compute the beginning_of_year
and the end_of_year
.
Introducing the date
class
So, if we import
the datetime
module, we can access the date
class that it provides. This class has three attributes for the year, month, and day ...
Python 3.12.1 (main, Jan 1 2024, 16:22:47) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import datetime
>>> d = datetime.date(1991, 2, 20) # construct new instance of date
>>> print(d.year, d.month, d.day) # year/month/day attributes can be accessed individually
1991 2 20
>>> datetime.date.today() # there is a class method to get today's date
datetime.date(2024, 2, 12)
As you can see, the date
class also provides a class method named today()
that we will use to provide default values for the parameters of our functions. Note that this function returns a local date, so you get the current date in your time zone.
ISO8601 Formatting
The format, YYYY-MM-DD
, that my follower requested is known as ISO8601. This is a widespread standardized format; therefore, we are lucky because the date
class ships with support to parse from and serialize into this format ...
Python 3.12.1 (main, Jan 1 2024, 16:22:47) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from datetime import date
>>> bday = date.fromisoformat("1991-02-20") # parse ISO8601 formatted string
>>> print(bday.year, bday.month, bday.day) # proof that the parsing worked correctly
1991 2 20
>>> print(bday.isoformat()) # serialize back to ISO8601 formatted string
1991-02-20
By the way, do you recognize this date? No? Well, it's Python's birthday! On this day, the very first version of Python was released to the public!
First/Last Day of the Year
Now, we can implement the desired functions to calculate the boundary dates of a year:
from datetime import date
def begnning_of_year(today: date = date.today()) -> date:
return date(today.year, 1, 1)
def end_of_year(today: date = date.today()) -> date:
return date(today.year, 12, 31)
Easy, right? But not so fast. Both of these functions have a subtle bug ... can you spot it? Here is the answer: The default values for the parameters are incorrect or at least misleading. You see, these default values are evaluated only once when the functions are defined. They are not evaluated (again) when the functions are called! In other words, today
inside these functions is not today, as the name implies, but the date on which functions were defined. A long-running process where this behavior becomes a problem isn't hard to imagine ...
The correct way to define these defaults would be something like this:
from datetime import date
def begnning_of_year(today: date | None = None) -> date:
today = today or date.today()
return date(today.year, 1, 1)
def end_of_year(today: date | None = None) -> date:
today = today or date.today()
return date(today.year, 12, 31)
This ensures that date.today()
is called on each invocation of our functions.
Test Drive
Now, if we put everything together, we can fulfill all of the original requirements:
from datetime import date
def beginning_of_year(today: date | None = None) -> date:
today = today or date.today()
return date(today.year, 1, 1)
def end_of_year(today: date | None = None) -> date:
today = today or date.today()
return date(today.year, 12, 31)
if __name__ == '__main__':
bday = date.fromisoformat("1991-02-20")
print(f"Python's birthday is on {bday.isoformat()} ...")
start, end = beginning_of_year(bday), end_of_year(bday)
print(f"that was the year starting on {start.isoformat()}")
print(f" and ending on {end.isoformat()}")
If you run this program, you see the expected output:
$ python birthday.py
Python's birthday is on 1991-02-20 ...
that was the year starting on 1991-01-01
and ending on 1991-12-31
Now you know the basics of working with the date
class in Python. Thank you for reading, and see you next time!