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

by Christoph Schiessl on Python

Last week, I published an article explaining how to use Python to find the first/last day of a given year. One natural follow-up question is how to find the first/last day of a given month, and this is the topic of the article you are reading now ...

The first day of the year is always the 1st of January, and the last is always the 31st of December — there are no exceptions. However, months are more interesting because they are not always the same length: Some are 31 days, and others are 30 days. February is the weirdest month because it is either 28 or 29 days in leap years. That means we cannot simply hardcode the last day of the month but have to calculate it depending on the number of the month (1-12) and the year (i.e., 2024).

We will again use the built-in datetime module from the standard library and compose our functions from this module's functionality. We will name our functions beginning_of_month and end_of_month, the first which is easy because every month has a day number 1, and we can use this to our advantage:

from datetime import date

def begnning_of_month(today: date | None = None) -> date:
    today = today or date.today()
    return date(today.year, today.month, 1)

But, end_of_month is more challenging for the reasons that we have already discussed. We first need to explore a few more features of the date class to implement this function.

Introducing date.resolution and the timedelta class

The smallest increment of time the date class can represent is one full day. For example, given a date object like the one you get when you call date.today(), you can move forward by one full day to get tomorrow's date, or you can move backward by one full day to get yesterday's date. However, these are the smallest increments/decrements that you can make — the date class cannot represent anything between those dates. Inside the datetime module, this concept is called resolution, and for the date class, you can get it programmatically with the class property date.resolution. This property is an instance of class timedelta, which represents, as the name suggests, a specific duration of time. In the case of the date class, it's exactly one full day.

Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from datetime import date
>>> today = date.today()
>>> res = date.resolution
>>> (res, type(res))
(datetime.timedelta(days=1), <class 'datetime.timedelta'>)
>>> yesterday, tomorrow = today - res, today + res
>>> print(yesterday, today, tomorrow)
2024-02-18 2024-02-19 2024-02-20

You can add/subtract instances of timedelta from date objects to move forward/backward by the number of days that the timedelta represents. Apart from that, you can also multiply timedelta objects with ints:

Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from datetime import date
>>> one_week = date.resolution * 7
datetime.timedelta(days=7)
>>> today = date.today()
>>> next_week = today + one_week
>>> (today, next_week)
(datetime.date(2024, 2, 19), datetime.date(2024, 2, 26))

How does that help us to implement end_of_month? Given that we can subtract timedeltas from dates, we can already calculate the end of the current month by subtracting one day from the beginning of the next month.

from datetime import date
begnning_of_next_month = date(2024, 3, 1)
end_of_current_month = beginning_of_next_month - date.resolution
assert end_of_current_month == date(2024, 2, 29) # 29 because 2024 is a leap year!

So, now we have to answer a different question: How do we calculate the beginning of the next month? Simple with a three-step process:

  1. We get the beginning of the current month,
  2. then we add 31 days (duration of the longest possible month),
  3. finally, we get the beginning of the month of the date that we just got as a result.

The idea is easier to explain with code:

# This file is saved as firstlast.py so that it can be imported ...
from datetime import date

def beginning_of_month(today: date | None = None) -> date:
    today = today or date.today()
    return date(today.year, today.month, 1)

def end_of_month(today: date | None = None) -> date:
    today = today or date.today()
    beginning_of_current_month = beginning_of_month(today) # Step (1)
    beginning_of_next_month = beginning_of_month(          # Step (3)
        beginning_of_current_month + date.resolution * 31  # Step (2)
    )
    return beginning_of_next_month - date.resolution

And with that, we are done. Except ... there is a bug, but it's pretty hard to spot. Here is a hint:

Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from firstlast import *
>>> assert end_of_month(date(9999, 12, 31)) == date(9999, 12, 31)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/cs/firstlast.py", line 12, in end_of_month
    beginning_of_current_month + date.resolution * 31
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~
OverflowError: date value out of range

Boundaries: date.min and date.max

As it turns out, the date class can only represent dates within a specific range. We can use the class properties date.min/date.max to get the lower/upper boundaries of this range:

Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from datetime import date
>>> date.min
datetime.date(1, 1, 1)
>>> date.max
datetime.date(9999, 12, 31)
>>> date.min - date.resolution
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OverflowError: date value out of range
>>> date.max + date.resolution
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OverflowError: date value out of range

As you can see, we get OverflowErrors if we try to step over these boundaries. And if you have been paying attention so far, you should already see why this is a problem for us. The calculation we implemented in end_of_month has intermediate states that may require date objects greater than date.max (we have no problem at the lower boundary). We are computing the beginning of the next month, but as far as the date class is concerned, there is no next month for December 9999. Long story short, we must find a way to avoid this calculation.

Introducing calendar.monthrange(year, month)

Instead of building our own solution, we can use monthrange() function from the built-in calendar module. This function returns a tuple[int, int], whose first element is the weekday of the first day of the month, and its second element is the number of days in the month given by the parameters.

Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from calendar import monthrange
>>> monthrange(2024, 2) # handles leap-years, and
(calendar.THURSDAY, 29)
>>> monthrange(2025, 2) # non leap-years correctly
(calendar.SATURDAY, 28)
>>> monthrange(9999, 12)
(calendar.WEDNESDAY, 31)

Armed with this new knowledge, we can rewrite our end_of_month() function as follows:

# This file is saved as firstlast.py so that it can be imported into a console session ...
from datetime import date
from calendar import monthrange

def beginning_of_month(today: date | None = None) -> date:
    today = today or date.today()
    return date(today.year, today.month, 1)

def end_of_month(today: date | None = None) -> date:
    today = today or date.today()
    (_, num_days_in_month) = monthrange(today.year, today.month)
    return date(today.year, today.month, num_days_in_month)

As expected, the bug is now fixed — no more OverflowErrors:

Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from firstlast import *
>>> end_of_month(date.min)
datetime.date(1, 1, 31)
>>> end_of_month(date.today())
datetime.date(2024, 2, 29)
>>> end_of_month(date.max)
datetime.date(9999, 12, 31)

This is everything for today. Thank you for reading, and see you again next time!

Ready to Learn More Web Development?

Join my Mailing List to receive one article per week.


I send one email per week on building performant and resilient Web Applications 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 Year from a given Date

Learn how to find the first and last day of a year with Python's datetime module. This article explains step by step what you need to know.

By Christoph Schiessl on Python

The Built-In chr() and ord() Functions

Discover Python's built-in functions chr() and ord() for handling Unicode characters and converting between integers and characters.

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

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 the more than a decade of experience I have collected 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 also an avid writer and educator who takes pride in breaking down technical concepts into the simplest possible terms.