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!

Christoph Schiessl

Hi, I'm Christoph Schiessl.

I help you build robust and fast Web Applications.


I'm available for hire as a freelance web developer, so you can take advantage of my more than a decade of experience working on many projects across several industries. Most of my clients are building web-based SaaS applications in a B2B context and depend on my expertise in various capacities.

More often than not, my involvement includes hands-on development work using technologies like Python, JavaScript, and PostgreSQL. Furthermore, if you already have an established team, I can support you as a technical product manager with a passion for simplifying complex processes. Lastly, I'm an avid writer and educator who takes pride in breaking technical concepts down into the simplest possible terms.

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

Boolean Operators and Short-Circuiting

Fully understand Python's boolean operators for negation, conjunction, and disjunction. Master operator precedence and short-circuiting.

By Christoph Schiessl on Python

Web App Reverse Checklist

Ready to Build Your Next Web App?

Get my Web App Reverse Checklist first ...


Software Engineering is often driven by fashion, but swimming with the current is rarely the best choice. In addition to knowing what to do, it's equally important to know what not to do. And this is precisely what my free Web App Reverse Checklist will help you with.

Subscribe below to get your free copy of my Reverse Checklist delivered to your inbox. Afterward, you can expect one weekly email on building resilient Web Applications using Python, JavaScript, and PostgreSQL.

By the way, it goes without saying that I'm not sharing your email address with anyone, and you're free to unsubscribe at any time. No spam. No commitments. No questions asked.