LESSON 135 · DEEPER PYTHON30 min

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

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 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.

  2. 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.

  3. 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.

  4. 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

  5. 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

  6. 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.

  7. 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

  8. 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.

  9. 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.

  10. 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

PYTHON
import calendar
print(calendar.monthrange(2024,2)[1])
print(calendar.monthrange(2025,2)[1])
EXPECTED OUTPUT
29
28

The library applies the calendar rules.

02. Build a date from the last day

PYTHON
from datetime import date
import calendar
last=calendar.monthrange(2026,11)[1]
print(date(2026,11,last))
EXPECTED OUTPUT
2026-11-30

The day count becomes a real date value.

03. Reject month zero

PYTHON
from datetime import date
try:
    date(2026,0,1)
except ValueError:
    print('invalid month')
EXPECTED OUTPUT
invalid month

Invalid months should not silently wrap.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

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.

PYTHON
import calendar
print(calendar.monthrange(2024,2)[1])
print(calendar.monthrange(2025,2)[1])
EXPECTED OUTPUT
29
28
Follow the reasoning
  1. Run monthrange for a leap and a common year.
  2. Read the second element of its result.
  3. Use that day number when constructing the last date.
02 / 03

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.

PYTHON
from datetime import date
import calendar
last=calendar.monthrange(2026,11)[1]
print(date(2026,11,last))
EXPECTED OUTPUT
2026-11-30
Follow the reasoning
  1. Construct the first date.
  2. Get the month length.
  3. Construct the last date and compare them.
03 / 03

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.

PYTHON
from datetime import date
try:
    date(2026,0,1)
except ValueError:
    print('invalid month')
EXPECTED OUTPUT
invalid month
Follow the reasoning
  1. Check input types before date construction.
  2. Let date reject out-of-range values.
  3. 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 run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnImplement 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.
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.

    01.How many days are in February 2024?

    02.What does calendar.monthrange return second?

    03.Why use date objects for this result?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Hard-coded February

    COMMON MISTAKE
    date(year, 2, 28)
    FIX
    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

    COMMON MISTAKE
    isinstance(month, int)
    FIX
    type(month) is int

    What happens: True can masquerade as month one.
    This input contract excludes boolean values.

    Browse this lesson’s problem fixes

    Sources and further reading