Find Month Boundaries with calendar.monthrange
Return the first and last date of a month, including leap-year February.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Find Month Boundaries with calendar.monthrange
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Monthly reports need clear inclusive boundaries. Hard-coding February as 28 days or assuming every month has thirty days breaks on leap years and at year changes. Python's calendar.monthrange(year, month) supplies the number of days for that month. The first run asks calendar.monthrange about February 2024 and February 2025. Its second value is the number of days, so the answers are 29 and 28. This library call avoids embedding a partial leap-year rule in a report generator. A calendar month is not a fixed number of days. The precise last date matters for reports, billing windows, and schedule labels.
- 00:44
February makes mistakes obvious, so we will begin there. Before running, decide whether February 2024 ends on the twenty-eighth or twenty- ninth. Then compare your prediction with the starter's fixed day. February 2025 gives a useful second case because the same code must handle an ordinary year too. Construct the first date with datetime.date(year, month, 1), then ask monthrange for the final day number and construct the last date. This avoids subtracting one second from a next-month timestamp or making assumptions about months. The pair represents dates, not instants or local time-zone boundaries.
- 01:26
A year divisible by four is not the complete Gregorian rule: century years require further conditions. A known library routine is preferable to a hand-written shortcut here. The lesson still checks actual dates with datetime.date, so an invalid year or month does not silently produce a plausible- looking result. The starter assumes the twenty-eighth is always the last day of February. It passes a common year but fails 2024. This is a data rule failure that a single happy-path test would miss. The starter returns February 28 for 2024, one day too early. That is a valid date object but the wrong boundary.
- 02:09
Write the expected leap-day result beside it so the contract failure is visible without needing a traceback. Validate that year and month are true integers rather than booleans. datetime.date enforces its supported year and month range, while monthrange supplies the day count. Do not silently roll an invalid month into the next year; reject it with ValueError so the caller can correct an input. The second example takes November's thirty days and constructs November 30 as a date object. The result can be compared with other dates without parsing a display string again. The first day is simply date(year, month, 1), while the last uses monthrange's day
- 02:49
count. This is an inclusive calendar-date range, suitable for a monthly label or report. A timestamp query often benefits from a half-open interval ending at the start of next month instead. That choice depends on time-zone policy and should not be smuggled into a function that promises only date boundaries. The diagnostic run asks the calendar library for February 2024 and 2025. It shows two different lengths. Using the standard library avoids an incomplete hand-written leap-year condition. The diagnostic call uses monthrange for November and constructs its thirtieth day
- 03:30
as a date. The same function returns 29 for February 2024 and 28 for February 2025; it encodes the calendar rule instead of a hand- written guess. Implement month_dates(year, month) returning a tuple of first and last datetime.date values. Use calendar.monthrange for the final day. Reject bool, non-integer, or out-of-range inputs with ValueError. The prepared run must show February 1 and 29, 2024. Validate type(year) is int and type(month) is int; create date(year, month, 1); use calendar.monthrange(year, month)[1] for the last day.
- 04:13
The last run proves month zero is invalid. The checked task also rejects month thirteen, year zero, booleans, and non- integers. Because bool is a subclass of int, an isinstance check alone is too permissive for this input contract; use exact integer type checks before calling date. The corrected function builds actual date objects for first and last. It validates integer inputs first, because bool behaves like int in Python but is not a meaningful year or month here. The repaired function creates first and last date objects only after checking exact integer types. A boolean is an int subclass but is not a meaningful month or year. monthrange
- 04:57
provides the final day, while date rejects month zero and invalid years. The checked task verifies February 2024 ends on the 29th, February 2025 on the 28th, and December 2026 on the 31st. For database queries over timestamps, a half-open interval from the first instant of this month to the first instant of next month is often easier, but that requires an explicit time zone and is a separate lesson. Test February of a leap year, February of an ordinary year, and December to cover distinct calendar edges. Do not invent a default final day for invalid input.
- 05:38
Returning an error makes the calling program ask for a corrected month instead of silently mislabeling a report. Ask the calendar for the actual number of days. This input contract excludes boolean values. Test leap February, ordinary February, December, and an invalid month. For timestamp queries, a half-open interval and an explicit time zone may be better; this function promises calendar dates only. A month-end date is inclusive for a report, while a database timestamp range often ends at the first instant of the following month. Those are different contracts. Verify both February cases, December, and an invalid month.
- 06:22
The result is an inclusive pair of dates. A database timestamp interval would need a separate time-zone policy and often ends at the first instant of the following month.
Understand the concept
Monthly reports need clear inclusive boundaries. Hard-coding February as 28 days or assuming every month has thirty days breaks on leap years and at year changes. Python's calendar.monthrange(year, month) supplies the number of days for that month.
Construct the first date with datetime.date(year, month, 1), then ask monthrange for the final day number and construct the last date. This avoids subtracting one second from a next-month timestamp or making assumptions about months. The pair represents dates, not instants or local time-zone boundaries.
Validate that year and month are true integers rather than booleans. datetime.date enforces its supported year and month range, while monthrange supplies the day count. Do not silently roll an invalid month into the next year; reject it with ValueError so the caller can correct an input.
The checked task verifies February 2024 ends on the 29th, February 2025 on the 28th, and December 2026 on the 31st. For database queries over timestamps, a half-open interval from the first instant of this month to the first instant of next month is often easier, but that requires an explicit time zone and is a separate lesson.
- monthrange returns the number of days
- Leap-year February varies
- Use date objects for calendar dates
- Reject invalid month inputs
See it step by step
Read the code, predict the output, then compare it with the result.
01. Compare leap and common years
import calendar
print(calendar.monthrange(2024,2)[1])
print(calendar.monthrange(2025,2)[1])29
28The library applies the calendar rules.
02. Build a date from the last day
from datetime import date
import calendar
last=calendar.monthrange(2026,11)[1]
print(date(2026,11,last))2026-11-30The day count becomes a real date value.
03. Reject month zero
from datetime import date
try:
date(2026,0,1)
except ValueError:
print('invalid month')invalid monthInvalid months should not silently wrap.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Compare two February lengths
The first run asks calendar.monthrange about February 2024 and February 2025. Its second value is the number of days, so the answers are 29 and 28. This library call avoids embedding a partial leap-year rule in a report generator.
A year divisible by four is not the complete Gregorian rule: century years require further conditions. A known library routine is preferable to a hand-written shortcut here. The lesson still checks actual dates with datetime.date, so an invalid year or month does not silently produce a plausible-looking result.
import calendar
print(calendar.monthrange(2024,2)[1])
print(calendar.monthrange(2025,2)[1])29
28- Run monthrange for a leap and a common year.
- Read the second element of its result.
- Use that day number when constructing the last date.
Construct a real last-day date
The second example takes November's thirty days and constructs November 30 as a date object. The result can be compared with other dates without parsing a display string again. The first day is simply date(year, month, 1), while the last uses monthrange's day count.
This is an inclusive calendar-date range, suitable for a monthly label or report. A timestamp query often benefits from a half-open interval ending at the start of next month instead. That choice depends on time-zone policy and should not be smuggled into a function that promises only date boundaries.
from datetime import date
import calendar
last=calendar.monthrange(2026,11)[1]
print(date(2026,11,last))2026-11-30- Construct the first date.
- Get the month length.
- Construct the last date and compare them.
Reject invalid months and bool inputs
The last run proves month zero is invalid. The checked task also rejects month thirteen, year zero, booleans, and non-integers. Because bool is a subclass of int, an isinstance check alone is too permissive for this input contract; use exact integer type checks before calling date.
Test February of a leap year, February of an ordinary year, and December to cover distinct calendar edges. Do not invent a default final day for invalid input. Returning an error makes the calling program ask for a corrected month instead of silently mislabeling a report.
from datetime import date
try:
date(2026,0,1)
except ValueError:
print('invalid month')invalid month- Check input types before date construction.
- Let date reject out-of-range values.
- Verify leap and December boundaries.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Validate type(year) is int and type(month) is int; create date(year, month, 1); use calendar.monthrange(year, month)[1] for the last day.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Hard-coded February
date(year, 2, 28)calendar.monthrange(year, month)[1]What happens: Leap-year reports omit February 29.
Ask the calendar for the actual number of days.
Accepting bool as an int
isinstance(month, int)type(month) is intWhat happens: True can masquerade as month one.
This input contract excludes boolean values.