2025, Oct 17 18:00

How to Restrict a Tkinter tkcalendar Date Picker to an Allowlist of Specific Dates

Learn how to extend tkcalendar's Calendar and DateEntry to enforce an allowed_dates allowlist, disable disallowed days, and keep mindate/maxdate in sync.

Restricting a date picker to a strict allowlist sounds simple, yet in many GUI stacks the default widget only supports a start and end boundary. If you are using tk and tkcalendar, mindate and maxdate give you a useful range guard, but they don’t prevent selecting the disallowed days inside that range. Below is a concise guide to implement a true allowlist that makes only specific dates selectable in a calendar widget.

Problem

You want the user to pick exactly one date from a predefined set. The default DateEntry can restrict a range, but it still lets the user click any intermediate day that happens to fall within mindate and maxdate.

import datetime as dt
import tkinter as tk
from tkinter import ttk
from tkcalendar import DateEntry
# fmt: off
allowed_str_days = [
    "2024-04-08", "2024-04-10", "2024-04-11", "2024-04-12",
    "2024-04-15", "2024-04-16", "2024-04-17", "2024-04-18", "2024-04-19",
    "2024-04-22",
    "2024-05-21", "2024-05-22", "2024-05-23", "2024-05-24",
    "2024-05-27", "2024-05-28", "2024-05-29", "2024-05-30", "2024-05-31",
    "2024-06-03", "2024-06-04", "2024-06-05", "2024-06-07",
    "2024-06-10", "2024-06-11", "2024-06-12", "2024-06-13", "2024-06-14",
]
# fmt: on
app = tk.Tk()
picked_var = tk.StringVar()
selector = DateEntry(
    app,
    textvariable=picked_var,
    date_pattern="yyyy-mm-dd",
    mindate=dt.date.fromisoformat(allowed_str_days[0]),
    maxdate=dt.date.fromisoformat(allowed_str_days[-1]),
)
selector.pack()
app.mainloop()

This code restricts the earliest and latest selectable dates, but it does not exclude the disallowed days within those boundaries.

What’s really going on

Calendar and DateEntry in tkcalendar provide range constraints via mindate and maxdate. There is no built-in mechanism to allow only a sparse subset of dates inside that range. To achieve a true allowlist, the calendar grid must actively disable every day that is not in the list. That requires changing how the calendar cells are rendered and how the date selection is validated.

Solution

The approach is to extend the underlying Calendar with a new option allowed_dates. This option accepts a list of datetime.date objects and is used during rendering to disable every date not present in that list, within the range from allowed_dates[0] to allowed_dates[-1]. The core idea is implemented where the calendar is displayed: iterate over the relevant dates and disable the cells not in the allowlist.

# inside a custom Calendar class
# --- render grid
def _render_grid(self):
    # ... existing rendering code for the month view ...
    allowlist = self['allowed_dates']
    if allowlist is not None:
        import datetime as dt
        day_step = dt.timedelta(days=1)
        cur_day = allowlist[0]
        last_day = allowlist[-1]
        while cur_day <= last_day:
            if cur_day not in allowlist:
                row_idx, col_idx = self._get_day_coords(cur_day)
                if row_idx is not None:
                    print(cur_day, row_idx, col_idx, '!disabled')
                    self._calendar[row_idx][col_idx].state(['disabled'])
            cur_day += day_step

With this in place, you can pass allowed_dates and make only those cells clickable. The list of strings must be converted to datetime.date first, and you can use either Calendar or DateEntry from your custom module. Below is a runnable example that also demonstrates dynamically adding and removing dates to the allowlist while keeping mindate and maxdate aligned with the new bounds.

import datetime as dt
import tkinter as tk
# from tkinter import ttk
# from tkcalendar import DateEntry
from mycalendar import Calendar
from mycalendar import DateEntry
# fmt: off
raw_days = [
    "2024-04-08", "2024-04-10", "2024-04-11", "2024-04-12",
    "2024-04-15", "2024-04-16", "2024-04-17", "2024-04-18", "2024-04-19",
    "2024-04-22",
    "2024-05-21", "2024-05-22", "2024-05-23", "2024-05-24",
    "2024-05-27", "2024-05-28", "2024-05-29", "2024-05-30", "2024-05-31",
    "2024-06-03", "2024-06-04", "2024-06-05", "2024-06-07",
    "2024-06-10", "2024-06-11", "2024-06-12", "2024-06-13", "2024-06-14",
]
# fmt: on
ui = tk.Tk()
whitelist_dt = [dt.date.fromisoformat(d) for d in raw_days]
# Calendar example
tk.Label(ui, text="Calendar").pack()
cal_widget = Calendar(
    ui,
    date_pattern="yyyy-mm-dd",
    mindate=whitelist_dt[0],
    maxdate=whitelist_dt[-1],
    allowed_dates=whitelist_dt,
    locale="en_GB.utf-8",
)
cal_widget.pack()
entry_var = tk.StringVar()
# DateEntry example
tk.Label(ui, text="DateEntry").pack()
entry_widget = DateEntry(
    ui,
    textvariable=entry_var,
    date_pattern="yyyy-mm-dd",
    mindate=whitelist_dt[0],
    maxdate=whitelist_dt[-1],
    allowed_dates=whitelist_dt,
    locale="en_GB.utf-8",
)
entry_widget.pack()
# Target can be either Calendar or DateEntry
# target = cal_widget
target = entry_widget
def show_whitelist():
    for d in target['allowed_dates']:
        print('allowed:', d)
btn_show = tk.Button(ui, text="Show Allowed Dates", command=show_whitelist)
btn_show.pack(fill='x')
def add_whitelisted(day_str):
    day_obj = dt.date.fromisoformat(day_str)
    if day_obj not in target['allowed_dates']:
        print('add allowed:', day_str)
        target['allowed_dates'].append(day_obj)
        target['allowed_dates'] = sorted(target['allowed_dates'])
        if target['allowed_dates'][0] < target['mindate']:
            target['mindate'] = target['allowed_dates'][0]
        if target['allowed_dates'][-1] > target['maxdate']:
            target['maxdate'] = target['allowed_dates'][-1]
        if target == cal_widget:
            target._render_grid()
for s in ('2024-06-06', '2024-06-26', '2024-07-10'):
    tk.Button(ui, text=f"Add Allowed Date: {s}", command=lambda x=s: add_whitelisted(x)).pack(fill='x')
def remove_whitelisted(day_str):
    day_obj = dt.date.fromisoformat(day_str)
    if day_obj in target['allowed_dates']:
        print('remove allowed:', day_str)
        target['allowed_dates'].remove(day_obj)
        if target['mindate'] < target['allowed_dates'][0]:
            target['mindate'] = target['allowed_dates'][0]
        if target['maxdate'] > target['allowed_dates'][-1]:
            target['maxdate'] = target['allowed_dates'][-1]
        if target == cal_widget:
            target._render_grid()
for s in ('2024-06-06', '2024-06-26', '2024-07-10'):
    tk.Button(ui, text=f"Remove Allowed Date: {s}", command=lambda x=s: remove_whitelisted(x)).pack(fill='x')
ui.mainloop()

When using this approach, there are noteworthy observations. Adding 2024-06-06 to Calendar also reflected in DateEntry, but removing it did not propagate in the same way. Another observed behavior is that DateEntry did not add a date if it was outside the current mindate or maxdate. Keeping allowlist edits consistent with range bounds is therefore essential for predictable interaction.

Why this matters

Real-world scheduling often needs a sparse set of selectable dates. Disabling arbitrary days inside a range isn’t a built-in feature of the default widgets, so extending Calendar to honor an allowlist is a pragmatic way to deliver that user experience without abandoning the familiar DateEntry workflow. It is also important to understand that multiple functions cooperate to validate and render dates. Besides the display routine that paints the grid, selection checks like _check_sel_date and the DateEntry integration can require changes to fully enforce allowed_dates across the entire interaction cycle.

Takeaways

If you need a true allowlist, pass allowed_dates as datetime.date objects and disable every non-allowed cell during rendering of the calendar. Convert your string inputs with date.fromisoformat, keep the allowlist sorted, and adjust mindate and maxdate whenever you add or remove items so your range and allowlist stay consistent. If you integrate with DateEntry, expect that some additional functions may need alignment to reflect the allowlist rules everywhere, and consult the tkcalendar sources in tkcalendar.calendar_ and tkcalendar.dateentry to locate the relevant hooks. If modifying the calendar isn’t feasible for your timeline, selecting from a dropdown tied to the same allowlist is a workable alternative.

The article is based on a question from StackOverflow by Joooeey and an answer by furas.