月末の締め処理を自動化しようとして、ふと手が止まったことはありませんか。2月は28日なのか29日なのか、4月は30日だったか31日だったか。
人間なら指を折って数えれば済みます。けれどもプログラムに書くとなると、意外と面倒です。
こうした日付の計算は、Python標準ライブラリのcalendarに任せられます。pip installは要りません。
今回は月末日、うるう年、第何週の何曜日といった、実務でよく出てくる計算を中心に見ていきます。最後に、datetimeとの使い分けも整理します。
calendarは、カレンダーの形で日付を扱うための標準ライブラリ¶
calendarは、その名のとおりカレンダーを扱うモジュールです。import calendarと書けば、すぐに使えます。
日付そのものを表すのはdatetimeの役目です。calendarはその一段上で、月の並びや曜日の配置といった、カレンダーとしての形を扱います。
datetimeの基本がまだ不安な方は、先にこちらを読んでおくと話がつながりやすくなります。【関連記事】Pythonのdatetime入門
まずは、いちばん簡単な使い方から試してみましょう。
import calendar
print(calendar.month(2026, 10))
手元のPython 3.11で実行すると、次のように表示されました。
October 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
壁に掛けるカレンダーと同じ形ですね。ここで注目してほしいのは、週が月曜日から始まっている点です。
まず押さえたい、曜日は月曜日が0という約束¶
calendarでは、曜日を数字で表します。そして月曜日が0、日曜日が6です。
これはdatetimeのweekday()と同じ約束です。名前で書けるように、calendar.MONDAYからcalendar.SUNDAYまでの定数も用意されています。
主な曜日の数字と定数を、表にまとめておきます。
| 曜日 | 数字 | 定数 |
|---|---|---|
| 月曜日 | 0 | calendar.MONDAY |
| 火曜日 | 1 | calendar.TUESDAY |
| 金曜日 | 4 | calendar.FRIDAY |
| 土曜日 | 5 | calendar.SATURDAY |
| 日曜日 | 6 | calendar.SUNDAY |
コードの中に5や6といった数字を直接書くと、あとから読んだ人は意味がわかりません。定数を使うだけで、読みやすさがぐっと上がります。
Python 3.12からは、曜日と月を表すDayとMonthという列挙型も加わりました。列挙型の考え方は、こちらで詳しく紹介しています。【関連記事】Pythonのenumとは?
月末日は、monthrangeで一発で求める¶
ここからが本題です。ある月が何日まであるかは、monthrange()で求められます。
monthrange(年, 月)は、2つの値の組を返します。1つ目はその月の1日の曜日、2つ目はその月の日数です。
実際に、2026年2月と2028年2月で試してみます。
import calendar
print(calendar.monthrange(2026, 2)) # (6, 28)
print(calendar.monthrange(2028, 2)) # (1, 29)
2026年2月は1日が日曜日(6)で、28日まで。2028年2月は1日が火曜日(1)で、29日まであるとわかります。
月末の日付をdateで受け取る関数¶
日数がわかれば、月末の日付は簡単に作れます。datetimeのdateと組み合わせた関数にしておくと便利です。
import calendar
import datetime
def month_end(year, month):
"""その月の最終日をdateで返す"""
last_day = calendar.monthrange(year, month)[1]
return datetime.date(year, month, last_day)
for m in (1, 2, 4):
print(month_end(2026, m))
実行結果は次のとおりです。31日、28日、30日と、月ごとの違いを自分で覚えておく必要はありません。
2026-01-31
2026-02-28
2026-04-30
replaceで月だけ変えると落ちる理由¶
月末の扱いで、初心者がよくつまずく書き方があります。1月31日の月だけを2月に変えようとするコードです。
import datetime
d = datetime.date(2026, 1, 31)
d.replace(month=2)
# ValueError: day is out of range for month
2月31日という日付は存在しないので、エラーになります。翌月の同じ日を求めたいときは、先にmonthrange()で日数を確かめて、はみ出したら月末に丸めるのが安全です。
私は10年ほどエンジニアとして開発に関わってきましたが、この月末のずれで請求処理が2月だけ止まった現場を見たことがあります。テストを1月のデータでしか書いていなかったのが原因でした。
うるう年は、isleapとleapdaysに任せる¶
2月の日数を左右するのが、うるう年です。4で割り切れる年はうるう年、ただし100で割り切れる年は平年、さらに400で割り切れる年はうるう年、という少し複雑な決まりがあります。
自分で条件分岐を書くと、100と400の例外を忘れがちです。calendarならisleap()で判定できます。
import calendar
print(calendar.isleap(1900)) # False
print(calendar.isleap(2000)) # True
print(calendar.isleap(2028)) # True
1900年は100で割り切れるので平年、2000年は400で割り切れるのでうるう年です。こうした例外まで、正しく扱ってくれます。
期間の中にうるう年が何回あるかを数えるleapdays()もあります。ここで1つ注意があります。
leapdays(2000, 2028)は7を返しました。2000年から2027年までを数えていて、終わりの年は含まれません。
range()と同じで、終わりを含まない数え方です。2028年まで含めたいなら、leapdays(2000, 2029)と書きます。こちらは8になりました。
第2火曜日のような日付は、monthcalendarで求める¶
燃えないゴミの日は第2・第4火曜日、定例会議は毎月第1月曜日。こうした日付を求めたい場面は、意外と多いものです。
ここで役立つのがmonthcalendar()です。月のカレンダーを、週ごとのリストのリストとして返してくれます。
import calendar
for week in calendar.monthcalendar(2026, 10):
print(week)
実行すると、次のように表示されました。
[0, 0, 0, 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, 0]
1週間が月曜日から日曜日までの7つの数字で並んでいます。その月に含まれない日は0で埋められています。
第n何曜日を返す関数¶
この形がわかれば、第何週の何曜日かを求める関数はすぐに書けます。0を取り除いてから数えるのがポイントです。
import calendar
def nth_weekday(year, month, weekday, n):
"""year年month月の、第n weekday曜日の日を返す"""
days = [
week[weekday]
for week in calendar.monthcalendar(year, month)
if week[weekday] != 0
]
return days[n - 1]
print(nth_weekday(2026, 11, calendar.TUESDAY, 2)) # 10
2026年11月の第2火曜日は、10日と求められました。0のまま数えてしまうと、1週目が空振りして答えが1週ずれるので気をつけてください。
月の最終金曜日がほしいときは、days[-1]とすれば取り出せます。インデックスの使い方を変えるだけで、応用が利きますね。
週の始まりを日曜日にしたいときは¶
日本の壁掛けカレンダーは、日曜日始まりのものが多いですよね。calendarでも、週の始まりを変えられます。
方法は2つあります。モジュール全体の設定を変えるsetfirstweekday()と、カレンダーのオブジェクトごとに指定する方法です。
それぞれの特徴を比べてみます。
| 方法 | 影響する範囲 | 向いている場面 |
|---|---|---|
calendar.setfirstweekday(calendar.SUNDAY) |
モジュール全体 | 短いスクリプトで手早く変えたいとき |
calendar.TextCalendar(firstweekday=calendar.SUNDAY) |
そのオブジェクトだけ | ほかの処理に影響を出したくないとき |
おすすめは後者です。全体の設定を変えると、同じプログラム内でcalendarを使っている別の処理まで結果が変わってしまいます。
TextCalendarで日曜日始まりにしてみます。
import calendar
cal = calendar.TextCalendar(firstweekday=calendar.SUNDAY)
print(cal.formatmonth(2026, 10))
先頭がSuになり、見慣れた並びになりました。
October 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
なお、HTMLの表としてカレンダーを出力するHTMLCalendarもあります。簡単な社内ページにカレンダーを載せたいときに使えます。
実務でよく使う、月末の営業日を求める¶
ここまでの知識を組み合わせて、実務でよくある計算をしてみましょう。月末の最終営業日を求めるコードです。
月末日から1日ずつさかのぼり、土日でなくなったところで止めます。
import calendar
import datetime
def last_business_day(year, month):
"""月末の最終営業日(土日を除く)を返す"""
last = calendar.monthrange(year, month)[1]
day = datetime.date(year, month, last)
while day.weekday() >= calendar.SATURDAY:
day -= datetime.timedelta(days=1)
return day
for m in (1, 2, 5, 10):
print(last_business_day(2026, m))
実行結果は次のとおりです。2026年10月31日は土曜日なので、30日の金曜日が返ってきました。
2026-01-30
2026-02-27
2026-05-29
2026-10-30
ただし、このコードは祝日を考えていません。calendarは国ごとの祝日を知らないので、祝日まで除きたいなら、自社の休日リストを別に用意して照らし合わせる必要があります。
以前、月末バッチの実行日をこの方法で決めていたところ、年末年始だけ想定外の日に動いてしまったことがありました。祝日や会社の休業日は、最初から外部のリストで管理しておくのがおすすめです。
決まった日に処理を動かす仕組みそのものは、cronの記事で紹介しています。【関連記事】Pythonのcronとは?
calendarとdatetimeの使い分け¶
最後に、datetimeとの役割の違いを整理しておきます。どちらも日付を扱うので、迷いやすいところです。
よく使う機能を目的別に並べると、次のようになります。
| やりたいこと | 使うもの |
|---|---|
| 今日の日付や時刻を取得する | datetime |
| 日付の足し算・引き算をする | datetimeのtimedelta |
| その月が何日まであるか知る | calendarのmonthrange |
| うるう年かどうか判定する | calendarのisleap |
| 第何週の何曜日かを求める | calendarのmonthcalendar |
| タイムゾーンを扱う | zoneinfo |
ざっくり言えば、1つの日付を扱うならdatetime、月や週の並びを扱うならcalendarです。両方を組み合わせると、たいていの日付計算は標準ライブラリだけで済みます。
サーバーとの時差が絡む場合は、タイムゾーンの扱いも意識してください。【関連記事】Pythonのzoneinfoとは?
ちなみに、ターミナルでpython -m calendar 2026 10と打つと、その月のカレンダーがすぐに表示されます。Python 3.14では、このコマンドで今日の日付が色付きで強調されるようになりました。ちょっとカレンダーを確認したいときに便利ですよ。
まとめ¶
calendarは地味なモジュールですが、日付まわりの細かい計算をまとめて引き受けてくれます。月末日やうるう年を自分で数えるコードは、バグの温床になりやすいところです。
今回の内容を振り返ると、月末日はmonthrange、うるう年はisleap、第何週の何曜日はmonthcalendarでした。曜日は月曜日が0で、定数を使うと読みやすくなります。
まずは、いま書いている日付処理の中に、手で数えている部分がないか探してみてください。calendarに置き換えるだけで、コードが短く確実になるはずです。
ここまでお読みいただきありがとうございました。