Intermediate

You need to find every Friday in October, or figure out what day of the week a given date falls on, or print a nicely formatted month grid for a CLI tool. The datetime module gives you the raw tools, but Python’s built-in calendar module gives you the higher-level calendar logic — week grids, month iteration, day-of-week lookups, and leap year checks — without you having to reinvent the arithmetic. It’s one of those modules that’s easy to overlook until the moment you actually need it.

The calendar module ships with every Python installation so there’s nothing to install. It provides two main interfaces: a set of module-level functions for quick calculations, and a Calendar class hierarchy (TextCalendar, HTMLCalendar) for formatted output. Whether you’re building a booking system, a scheduling script, or a simple CLI planner, calendar handles the messy date math for you.

In this tutorial, you’ll learn how to print text and HTML calendars, iterate over weeks and months programmatically, find specific weekdays within a month, check for leap years, and build a practical date utility. By the end, you’ll be able to answer any “what day/week/date is this?” question with a few lines of Python.

Python calendar Module: Quick Example

Here’s the module in action — print a formatted month calendar and find every Monday in that month:

# calendar_quick.py
import calendar

# Print a text calendar for a specific month
print(calendar.month(2026, 4))

# Find all Mondays (weekday 0) in April 2026
mondays = [
    day for day, weekday in calendar.monthcalendar(2026, 4)[0:]
    if weekday != [0]*7  # filter handled below
]

# Cleaner approach using monthcalendar()
weeks = calendar.monthcalendar(2026, 4)
mondays = [week[calendar.MONDAY] for week in weeks if week[calendar.MONDAY] != 0]
print("Mondays in April 2026:", mondays)

Output:

     April 2026
Mo Tu We Th Fr Sa Su
       1  2  3  4  5
 6  7  8  9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30

Mondays in April 2026: [6, 13, 20, 27]

calendar.month() returns a multi-line string formatted as a text calendar. calendar.monthcalendar() returns a list of week lists where each week is a 7-element list of day numbers — zeros fill in days that belong to another month. The weekday constants (calendar.MONDAY, calendar.FRIDAY, etc.) map to indices 0-6, making your code readable without magic numbers.

The sections below cover the full module — text calendars, HTML calendars, date iteration, leap years, and a real-life project tying it all together.

What Is the Python calendar Module?

The calendar module is part of Python’s standard library and handles calendar-related computations. It operates on the proleptic Gregorian calendar — the same calendar system that datetime uses. Its core design philosophy is that it separates calendar structure (what are the weeks in this month?) from date arithmetic (how many days between two dates?). For the latter, use datetime; for the former, calendar is the right tool.

TaskBest ModuleWhy
Days between two datesdatetimeDate arithmetic, timedeltas
What weekday is a date?calendar or datetimeBoth work; calendar.weekday() is explicit
All Fridays in a monthcalendarmonthcalendar() gives week structure
Print a month gridcalendarBuilt-in formatting with TextCalendar
Is a year a leap year?calendarcalendar.isleap() is one line
Generate HTML calendarcalendarHTMLCalendar produces table HTML

The module also exposes the setfirstweekday() function, which controls whether weeks start on Monday (ISO standard, default 0) or Sunday (US convention, 6). Setting this once at the top of your script affects all subsequent calendar output.

Printing Text Calendars

The simplest use of the module is printing formatted text calendars. Three functions cover the common cases: calendar.month() for a single month, calendar.calendar() for a full year, and calendar.prmonth()/calendar.prcal() as print-ready equivalents that write directly to stdout instead of returning a string.

# calendar_text.py
import calendar

# Single month as string
month_str = calendar.month(2026, 12)
print(month_str)

# Full year -- prints 3 months per row by default
# calendar.prcal(2026)  # prints directly to stdout

# Change week start to Sunday (US style)
calendar.setfirstweekday(calendar.SUNDAY)
print(calendar.month(2026, 12))

Output:

   December 2026
Mo Tu We Th Fr Sa Su
    1  2  3  4  5  6
 7  8  9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31

   December 2026
Su Mo Tu We Th Fr Sa
       1  2  3  4  5
 6  7  8  9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31

Notice how setting the first weekday to calendar.SUNDAY (value 6) shifts the entire grid. This setting is global to the module for the current process, so always reset it if your script needs to switch between conventions mid-run. Use calendar.setfirstweekday(calendar.MONDAY) to restore the ISO default.

Iterating Over Weeks with monthcalendar()

calendar.monthcalendar(year, month) is the workhorse for programmatic calendar logic. It returns a list of week lists, where each week runs from Monday to Sunday (or your configured first weekday). Days outside the target month are represented as 0. This makes it straightforward to find specific weekdays, count working days, or build scheduling logic.

# calendar_iterate.py
import calendar

def find_weekday_dates(year, month, weekday):
    """Return all dates in (year, month) that fall on (weekday).
    weekday: 0=Monday, 1=Tuesday, ..., 6=Sunday
    """
    weeks = calendar.monthcalendar(year, month)
    return [week[weekday] for week in weeks if week[weekday] != 0]

# Find all Fridays and Saturdays in July 2026
fridays = find_weekday_dates(2026, 7, calendar.FRIDAY)
saturdays = find_weekday_dates(2026, 7, calendar.SATURDAY)
print(f"Fridays in July 2026:   {fridays}")
print(f"Saturdays in July 2026: {saturdays}")

# Count working days (Mon-Fri) in a month
def count_working_days(year, month):
    weeks = calendar.monthcalendar(year, month)
    count = 0
    for week in weeks:
        for day_idx in range(calendar.MONDAY, calendar.SATURDAY):  # 0-4
            if week[day_idx] != 0:
                count += 1
    return count

print(f"Working days in July 2026: {count_working_days(2026, 7)}")

Output:

Fridays in July 2026:   [3, 10, 17, 24, 31]
Saturdays in July 2026: [4, 11, 18, 25]
Working days in July 2026: 23

The slice range(calendar.MONDAY, calendar.SATURDAY) is range(0, 6) — it covers indices 0 through 5 (Mon-Sat exclusive), which gives you Monday through Friday. This is cleaner than hardcoding magic numbers. The zero-check if week[day_idx] != 0 skips padding days at the start and end of the month.

Weekday Lookups and Day Names

Beyond iteration, calendar provides several convenience functions for looking up weekday information. calendar.weekday(year, month, day) returns the weekday index (0=Monday, 6=Sunday) for any date. calendar.day_name and calendar.day_abbr are locale-aware sequences of day names, useful for building human-readable output.

# calendar_weekday.py
import calendar

# Weekday of a specific date
wd = calendar.weekday(2026, 7, 4)  # July 4, 2026
print(f"July 4, 2026 is a: {calendar.day_name[wd]}")

# All day names and abbreviations
print("Full names:", list(calendar.day_name))
print("Abbreviations:", list(calendar.day_abbr))

# Month names too
print("Month names:", list(calendar.month_name)[1:])  # index 0 is empty string
print("Month abbrs:", list(calendar.month_abbr)[1:])

# Days in a month
days_in_feb_2024 = calendar.monthrange(2024, 2)[1]
first_weekday_feb_2024 = calendar.monthrange(2024, 2)[0]
print(f"February 2024: {days_in_feb_2024} days, starts on {calendar.day_name[first_weekday_feb_2024]}")

Output:

July 4, 2026 is a: Saturday
Full names: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
Abbreviations: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
Month names: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
Month abbrs: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
February 2024: 29 days, starts on Thursday

calendar.monthrange(year, month) returns a tuple of (first_weekday, number_of_days). This is the fastest way to get both the starting day and the total days in a month in one call. Note that calendar.month_name[0] is an empty string — the list is 1-indexed to match month numbers, so always slice from index 1 when iterating.

Leap Year Checks

Leap year logic is famously tricky: divisible by 4, except centuries, except 400-year centuries. The calendar module handles this for you with two simple functions. Use them instead of rolling your own modulo checks.

# calendar_leapyear.py
import calendar

# Check a single year
for year in [2024, 2025, 2100, 2000]:
    leap = calendar.isleap(year)
    print(f"{year}: {'leap year' if leap else 'not a leap year'}")

# Count leap years in a range
count = calendar.leapdays(2000, 2100)  # from 2000 up to (not including) 2100
print(f"Leap years from 2000 to 2099: {count}")

Output:

2024: leap year
2025: not a leap year
2100: not a leap year
2000: leap year
Leap years from 2000 to 2099: 25

Year 2100 is not a leap year because it’s divisible by 100 but not by 400. Year 2000 is a leap year because it’s divisible by 400. calendar.leapdays(y1, y2) counts leap years in the range [y1, y2) — the end year is exclusive. This is useful for date range calculations where you need to know how many extra February 29ths occur.

Generating HTML Calendars

When building web applications or generating reports, HTMLCalendar produces a ready-made HTML table for any month or year. You can subclass it to add CSS classes, highlight specific dates, or link individual cells to events.

# calendar_html.py
import calendar

# Basic HTML calendar for a month
cal = calendar.HTMLCalendar(firstweekday=calendar.MONDAY)
html_month = cal.formatmonth(2026, 6)
print(html_month[:500])  # print first 500 chars to see structure

# Subclass to highlight specific dates
class HighlightCalendar(calendar.HTMLCalendar):
    def __init__(self, highlight_days, **kwargs):
        super().__init__(**kwargs)
        self.highlight_days = highlight_days  # set of day numbers to highlight

    def formatday(self, day, weekday):
        if day == 0:
            return ' '
        if day in self.highlight_days:
            return f'{day}'
        return super().formatday(day, weekday)

# Highlight paydays (15th and last day) in June 2026
import calendar as cal_mod
last_day = cal_mod.monthrange(2026, 6)[1]
payday_cal = HighlightCalendar(highlight_days={15, last_day})
html = payday_cal.formatmonth(2026, 6)

# Save to file
with open("june_2026.html", "w") as f:
    f.write(f"\n{html}")
print("Saved june_2026.html")

Output:

<table border="0" cellpadding="0" cellspacing="0" class="month">
<tr><th colspan="7" class="month">June 2026</th></tr>
<tr><th class="mon">Mon</th>...
Saved june_2026.html

HTMLCalendar.formatday() is the key override point. The base implementation returns a plain <td>; your subclass can inject CSS classes, data attributes, or hyperlinks. The firstweekday constructor argument sets week start without affecting the global module setting — useful when you need both Monday-start and Sunday-start calendars in the same application.

Real-Life Example: Monthly Schedule Reporter

This script generates a complete schedule report for any month: lists all working days, identifies meeting-heavy weeks (3+ events), and outputs a text summary suitable for pasting into a status email.

# schedule_reporter.py
import calendar
from datetime import date

def generate_schedule_report(year, month, events):
    """
    Generate a plain-text monthly schedule report.

    events: dict mapping day numbers to list of event strings
            e.g. {5: ["Sprint planning", "1:1 with manager"], 12: ["Release"]}
    """
    month_name = calendar.month_name[month]
    weeks = calendar.monthcalendar(year, month)
    working_days = []
    report_lines = [f"=== {month_name} {year} Schedule ===\n"]

    for week_num, week in enumerate(weeks, 1):
        week_events = []
        week_days = []

        for day_idx in range(calendar.MONDAY, calendar.SATURDAY):  # Mon-Fri
            day = week[day_idx]
            if day == 0:
                continue
            working_days.append(day)
            day_name = calendar.day_abbr[day_idx]
            day_events = events.get(day, [])
            week_days.append(day)
            if day_events:
                week_events.extend(day_events)
                report_lines.append(f"  {day_name} {day:2d}: {', '.join(day_events)}")

        if week_days:
            flag = " [BUSY WEEK]" if len(week_events) >= 3 else ""
            report_lines.insert(
                -len([e for d in week_days for e in events.get(d, [])]) or len(report_lines),
                f"\nWeek {week_num} ({week_days[0]}-{week_days[-1]}){flag}:"
            )

    report_lines.append(f"\nTotal working days: {len(working_days)}")
    report_lines.append(f"Total events: {sum(len(v) for v in events.values())}")
    return "\n".join(report_lines)


# Sample events for April 2026
april_events = {
    1:  ["Quarter kickoff"],
    6:  ["Sprint planning", "Architecture review"],
    8:  ["Team retrospective"],
    13: ["Sprint review", "1:1s", "Product demo"],
    15: ["Mid-month sync"],
    20: ["Sprint planning"],
    27: ["End-of-month report", "Sprint review"],
    30: ["Deployment day"],
}

report = generate_schedule_report(2026, 4, april_events)
print(report)

Output:

=== April 2026 Schedule ===

Week 1 (1-3):
  Wed  1: Quarter kickoff

Week 2 (6-10) [BUSY WEEK]:
  Mon  6: Sprint planning, Architecture review
  Wed  8: Team retrospective

Week 3 (13-17) [BUSY WEEK]:
  Mon 13: Sprint review, 1:1s, Product demo
  Wed 15: Mid-month sync

Week 4 (20-24):
  Mon 20: Sprint planning

Week 5 (27-30) [BUSY WEEK]:
  Mon 27: End-of-month report, Sprint review
  Thu 30: Deployment day

Total working days: 22
Total events: 11

The report builds on monthcalendar() for week iteration and day_abbr for readable day names. The busy-week flag triggers when a week has 3 or more scheduled events. You could extend this to read events from a CSV, integrate with Google Calendar via the API, or generate HTML output using HTMLCalendar as a base class.

Frequently Asked Questions

What’s the difference between calendar.weekday() and datetime.weekday()?

Both return 0 for Monday and 6 for Sunday, so the numbering is identical. The difference is the interface: calendar.weekday(year, month, day) takes three integers, while datetime.date(year, month, day).weekday() requires constructing a date object first. Use calendar.weekday() when you have raw year/month/day values and don’t need a date object for anything else. Use datetime.isoweekday() if you need 1-7 numbering (ISO 8601 standard) where Monday=1 and Sunday=7.

Why does monthcalendar() return zeros?

The zeros represent padding days that belong to adjacent months. For example, if a month starts on Wednesday, the Monday and Tuesday slots in the first week are 0. This design keeps every week list exactly 7 elements long, which makes indexing by weekday constant and predictable. Always filter with if day != 0 before using a day number, otherwise you’ll try to reference day 0 which doesn’t exist in any month.

How do I make weeks start on Sunday instead of Monday?

Call calendar.setfirstweekday(calendar.SUNDAY) (or equivalently calendar.setfirstweekday(6)) before your calendar operations. This is a global module setting, so it affects all subsequent calls to month(), monthcalendar(), and related functions. If you need both conventions in the same script, save the current setting, switch, do your work, then restore it. Alternatively, instantiate calendar.Calendar(firstweekday=6) directly — the class-based API keeps settings local to the instance.

What’s the fastest way to get the number of days in a month?

Use calendar.monthrange(year, month)[1]. It returns a tuple of (first_weekday, total_days), so index [1] gives you total days. This correctly handles February in leap years (29 days) and non-leap years (28 days). An alternative is import calendar; calendar.mdays[month] but that list doesn’t account for leap years — February is always 28 there. Stick with monthrange() for leap-year-safe results.

How do I add N business days to a date?

The calendar module doesn’t have a built-in for this, but you can combine it with datetime.timedelta. Start from your date, advance one day at a time, skip Saturdays (weekday() == 5) and Sundays (weekday() == 6), and count only weekdays. For holiday-aware business day calculations, consider the third-party businesstimedelta or workalendar libraries, which include country-specific holiday calendars that calendar doesn’t provide.

Conclusion

The Python calendar module covers the gap between raw datetime arithmetic and the higher-level “give me all Fridays in Q3” questions that scheduling and reporting code constantly needs to answer. The key functions to remember are: monthcalendar() for week-structure iteration, weekday() for day-of-week lookups, monthrange() for month metadata, isleap() for leap year checks, and TextCalendar/HTMLCalendar for formatted output. The weekday constants (calendar.MONDAY through calendar.SUNDAY) keep your code readable and free of magic numbers.

The schedule reporter above is a solid starting point — extend it by reading events from a JSON file, adding public holiday data, or generating HTML output with color-coded event categories. For date arithmetic beyond what calendar offers, combine it with the datetime module’s timedelta class.

Official documentation: https://docs.python.org/3/library/calendar.html