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 = 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!

Ready to Learn More Web Development?

Join my Mailing List to receive 1-2 useful Articles per week.


I send up to two weekly emails on building performant and resilient Web Apps with Python, JavaScript and PostgreSQL. No spam. Unscubscribe at any time.

Continue Reading?

Here are a few more Articles for you ...


How to Find the First/Last Day of a Month from a given Date

Learn how to calculate a month's first and last days using Python's built-in datetime module, with step-by-step explanations and code examples.

By Christoph Schiessl on Python

Date/Time Formatting According to RFC 5322

Learn about the custom date/time formatting rules defined in legacy standards like RFC 5322 and how to work with them using Python's datetime module.

By Christoph Schiessl on Python

How to Format all JSON Files in a Git Repository

Do you struggle with inconsistent JSON formatting across your Git repository? Learn how to reformat everything using Python with this step-by-step guide.

By Christoph Schiessl on DevOps, Git, and Python

Christoph Schiessl

Christoph Schiessl

Independent Consultant + Full Stack Developer


If you hire me, you can rely on more than a decade of experience, which I have collected working on web applications for many clients across multiple industries. My involvement usually focuses on hands-on development work using various technologies like Python, JavaScript, PostgreSQL, or whichever technology we determine to be the best tool for the job. Furthermore, you can also depend on me in an advisory capacity to make educated technological choices for your backend and frontend teams. Lastly, I can help you transition to or improve your agile development processes.