Part 5 - Barbershop Appointment Booking System

Had WillHad Will
29 min read

< Previous Post

Last week, I decided to visit my barber for a long-due haircut. My usual barbershop had closed not long ago, and I was searching on Google for 'barbershop near <my_town>.' This made me look at various booking systems. I thought it would be easy enough to code. So here we are... A tutorial on how to write a booking system in Python.

Our program will be barebones. It won't connect to the internet or do any fancy stuff. It will let a salon owner book reservations and not much more. This post is a great opportunity to show you Python's modules. To be precise, we'll use the built-in datetime module.

We'll also expand on the last post's topic and use more sets and set operations.

Modules

Our program will deal with appointments, meaning it needs to manipulate time.

Dates and times look simple at first glance. But that's a trap. Getting dates and times exactly right is a notorious headache for coders. That's why most programming languages provide some way to do it for you out of the box. For Python, that's the datetime module.

A module is a collection of ready-to-use Python code. It contains functions, classes, and variables. You can import that code into your programs for your own use. You can use the import statement to import a whole module: import datetime.

If you want to use a function/variable/class from a module, prepend it with the module's name plus a dot. For example, after importing datetime, you can use its date like datetime.date().

You can opt to import only select pieces of the module. For example, to import function from module, you can do it like from module import function. In our case:

from datetime import date, time, datetime

The Python docs are the definitive source for information on modules. You'll find information about the datetime module here.

If you find a module's name to be too long, you can rename it:

import datetime as dt

As a last note, you can create your own modules. For example, if you have some functions you use often, you can put them into a Python file (e.g., my_module.py). Your programs can import this program like any other module: import my_module. This will work if you place my_module.py in the same directory as your program. This has its limits, and Python offers other solutions. We'll see them in a future post.

It's good practice to import modules at the start of your program file. Below are all the imports for the current project.

import datetime
from datetime import date, time, datetime, timedelta

# Use pp like you would print when testing your code.
# it will print complex data structures in a pretty way
from pprint import pp

The datetime module

The datetime provides us with several interesting types.

  • a date type

  • a time type

  • a datetime type

  • a timedelta type

  • (two timezone-related classes we won't be using in our program: tzinfo and timezone)

What date and time do is self-explanatory. They provide a way to deal with idealized dates and times.

datetimedatetimedatetime
year
month
day
hour
minutes
seconds
microseconds

Let's write some code to show how the datetime module works. We'll write three functions: request_date, request_time, and make_datetime. request_date asks the user for a date and turns it from a string to a date object.

from datetime import date, time, datetime

def request_date():
    """
    Ask for input from user, return a date object.
    """
    raw_input = input("Please enter a date (YYYY-MM-DD): ")

    year, month, day = tuple(raw_input.split("-"))
    year, month, day = int(year), int(month), int(day)

    return date(year, month, day)

request_time does the same for hours.

def request_time():
    """
    Ask for input from user, return a time objject.
    """
    raw_input = input("Please enter a time in 24h format (HH:MM): ")

    hour, minutes = tuple(raw_input.split(':'))
    hour, minutes = int(hour), int(minutes)

    return time(hour, minutes)

In both request_date and request_time, we ask the user for input and store it in a string with input. We then use the split method to separate the year from the month from the day and the hours from the minutes. We can do this because we know which character separates the values in the input string ("-" in dates and ":" in times.) split creates a list of strings, which we turn into a tuple of strings using tuple. We then use unpacking to create separate variables from the tuples. We turn these variables from strings to integers with int. We create date and time in return values with date and time.

make_datetime combines a time object and a date object into a datetime object.

def make_datetime(date, time):
    return datetime.combine(date, time)

date and time objects turn into datetime objects when we combine them together.

The structure of the program

When you reserve a haircut, you reserve a 'slot' at the barbershop. For example, you might ask for an appointment with John on February 8th at 9 am. Let's call it that a slot. A slot will be a tuple of a datetime object and a worker string.

(datetime.datetime(2025, 3, 25, 13, 30), 'john')

We'll collect slots in sets. For example, we could have a set collecting all reserved slots for a specific date. Another example would be a set for all possible slots for a particular worker. I won't list all the possible slot collections here, as I'll define them as I write the program.

We'll have appointment data as a tuple of a client string and a slot.

("Ms. Smith", (datetime.datetime(2025, 3, 25, 13, 30), 'john'))

I'll also collect appointments in sets.

We also want to describe worker's schedules. We'll use dictionaries for this.

Abstraction layers

The program will show you a classic way to structure an application. I'll define layers of abstraction. Each layer has a few functions, using the functions of the layer below it. Those functions will be useful for the functions of the layer above.

For example, once we define functions to work with slots, we will manipulate them as if they were their own type. We won't reference how we defined them but will use our new 'abstractions'.

The lowest layer will be slots and worker schedules. On top of these, we'll build collections of slots. The topmost layer will be appointment abstractions.

If all that talk about abstraction was unclear, don't worry; you'll see it in action soon.

Slot logic

As I said, slots are some of the basic units of our program.

When I create a layer of abstraction around a data structure, I always do the same few things. First, I create a function to create a new instance of the data. Next, I write a function to pick apart that data.

def make_slot(datetime, worker):
    """
    Create a slot tuple from a datetime object and a worker string.
    """
    return datetime, worker

def slot_worker(slot):
    """
    Return the worker of a slot.
    """
    return slot[1]

def slot_date_time(slot):
    """
    Return the datetime object of a slot.
    """
    return slot[0]

This is the core of abstraction: future code will not have to be aware of slot implementation. The only info you'll need is that a slot has a datetime object and a worker string that you can access. You don't need to know that a slot is a tuple or datetime comes before the worker. It's under the hood, and we can keep the hood shut. We created a convenient black box. If we one day decide slots should be lists, dictionaries, or something else, nothing stops us. We'll change three functions, and the rest of the program doesn't even need to know about the changes.

Slot-slots interface

What does "slot-slots interface" mean? We need some functions to interface between a slot (singular) and a set of slots (plural).

We'll define functions to check if a slot is in a set, add a new one, and remove an existing slot.

def check_slot(slots, slot):
    """
    Check if slot exists in slots.
    Return True if slot exists, False if it doesn't.
    """
    return slot in slots

def add_slot(slots, slot):
    """
    Add slot to slots.
    Return slots
    """
    slots.add(slot)
    return slots

def remove_slot(slots, slot):
    """
    Remove a slot from slots.
    Return slots.
    """
    slots.discard(slot)
    return slots

Those three functions are a refresher of the last post. We test if an element is a set member with if element in set. We add a new element with the add method and remove an element with discard. I used pop instead of discard in the last post. I will give you a summary of their behavior differences below.

popdiscard
removes an element from the setyesyes
return valuethe removed elementNone
argumentsnonethe element
if the element is not in the setnot applicabledoes nothing
if the set is emptyKeyErrorno error; does nothing

What we created as an interface is very close to what programmers call a CRUD interface. CRUD stands for Create, Read, Update, Delete. Those are the fundamental operations needed to deal with data systems. add_slot does the Creating, check_slot does the Reading, remove_slot does the Deleting. I haven't written any function to do the Updating. You'll do it yourself as an exercise. The function should look like update_slot(slots, slot_old, slot_new). It should return slots. Below is the recipe:

  1. Add the new slot to slots.

  2. Remove the old slot from the slots.

Now, our 'slot abstraction' is complete.

Schedules

The second basic abstraction we need is the schedule. Each worker should have a schedule. For simplicity, this is an oversimplification of real-world schedules. We will pretend workers don't take vacations. They'll work the same schedule every week, without variation. That way, we can represent them using a dictionary. Its keys represent days, and its values are the corresponding work shifts. Below is an example:

{
    "mon": [
        ("09:00", "12:00"),
        ("13:00", "17:00")
    ],
    "tue": [
        ("09:00", "12:00"),
        ("13:00", "17:00")
    ],
    "wed": [
        ("09:00", "12:00")
    ],
    "thu": [
        ("09:00", "12:00"),
        ("13:00", "17:00")
    ],
    "fri": [
        ("10:00", "12:00"),
        ("13:00", "16:00")
    ],
    "sat": [
        ("13:00", "15:00")
    ],
    "sun": []
}

Since there can be several shifts daily, we'll collect them in a list. The shifts themselves will be tuples of strings. In the example above, the worker works two shifts on Mondays: from 9 am to noon and from 1 pm to 5 pm.

We'll collect the individual worker schedules inside a dictionary, too. The keys will be the worker's names and the keys their respective schedules.

{
    "john": sched,
    "anna": sched,
}

I decided the program would use strings like "mon" and "09:00". When reading the code, we humans understand it stands for "Monday" and "9 am". But that's not the case for our computer. We'll need to create some conversion code. This is good practice for you as you learn more about strings and the datetime module.

First, I create a function that returns a list of shifts when given a schedule and a day. For example, given the schedule above and the string "mon", it would return [("09:00", "12:00"), ("13:00", "17:00")]. This function is very simple since the schedule is a dictionary whose keys are the days of the week.

def schedule_day(schedule, day):
    """
    Get the schedule for a given day.
    e.g., [("09:00", "12:00"), ("13:00", "17:00")]
    """
    return schedule[day]

Next, we create a function that takes a dictionary of workers' schedules. Given a worker's name, it returns his particular schedule. Again, due to the dictionary structure of our data, the function is straightforward.

def find_worker_schedule(schedules, worker):
    """
    Find the schedule for worker
    Returns a schedule
    """
    return schedules[worker]

The following code will fill the gap between datetimes and weekday strings such as "mon". get_day, along with the global constant WEEKDAYS, will turn a datetime into a weekday. For example, if get_day receives date(2025, 1, 16) as an argument, it will return "wed".

WEEKDAYS = ("mon", "tue", "wed", "thu", "fri", "sat", "sun")

def get_day(date):
    """
    Given a datetime object, return the name of the day in a string.
    """
    day_num = date.weekday()
    return WEEKDAYS[day_num]

WEEKDAYS holds all our weekday strings in a tuple. I arranged them so that their indexes correspond to the output of date.weekday(). The weekday method matches a date object with a number corresponding to the day of the week. Dates falling on a Monday will return 0, Thursdays will be 1, Wednesdays 2, etc.

Slots have both a datetime and a worker. But a worker's schedule has shifts classed by the day of the week. How do we bridge "mon": [("09:00", "12:00"), ("13:00", "17:00")] with a slot's datetime? We'll create a function that takes a schedule and a date. Given the schedule, it will generate all the possible datetime objects for that date. I decided each slot would have a 30-minute duration. For a date falling on a Tuesday (e.g., 11/25/2025), "tue": [("09:00", "12:00"), ("13:00", "17:00")] would give

[datetime.datetime(2025, 11, 25, 9, 0),
datetime.datetime(2025, 11, 25, 9, 30),
datetime.datetime(2025, 11, 25, 10, 0),
datetime.datetime(2025, 11, 25, 10, 30),
datetime.datetime(2025, 11, 25, 11, 0),
datetime.datetime(2025, 11, 25, 11, 30),
datetime.datetime(2025, 11, 25, 13, 0),
datetime.datetime(2025, 11, 25, 13, 30),
datetime.datetime(2025, 11, 25, 14, 0),
datetime.datetime(2025, 11, 25, 14, 30),
datetime.datetime(2025, 11, 25, 15, 0),
datetime.datetime(2025, 11, 25, 15, 30),
datetime.datetime(2025, 11, 25, 16, 0),
datetime.datetime(2025, 11, 25, 16, 30),
datetime.datetime(2025, 11, 25, 17, 0),
datetime.datetime(2025, 11, 25, 17, 30)]

Here's a simple implementation:

SLOT_DURATION = timedelta(minutes=30)

def subdivise_day(schedule, date):
    """
    Generate a list of datetimes corresponding to slot start times in a day's schedule
    Uses SLOT_DURATION = 30 minutes.
    e.g., for 2025/11/29, the list of datetimes could be
    [datetime.datetime(2025, 11, 29, 9, 0), datetime.datetime(2025, 11, 29, 9, 30)]
    """
    time_list = []

    day = get_day(date)
    shifts = schedule_day(schedule, day)

    for shift in shifts:
        start, end = shift

        start = datetime.strptime(start, "%H:%M")
        end = datetime.strptime(end, "%H:%M")

        start = start.replace(date.year, date.month, date.day)
        end = end.replace(date.year, date.month, date.day)

        while start < end:
            time_list.append(start)
            start = start + SLOT_DURATION

    return time_list

SLOT_DURATION and timedelta objects

SLOT_DURATION is the first place we see a timedelta object. Remember how we cited timedelta when introducing the datetime module earlier. timedelta represents a duration and takes keyword arguments corresponding to the duration. For example, a length of time of two days, four hours, eighteen minutes, and seven seconds would look like:

datetime.timedelta(
    days=1,
    hours=4,
    minutes=18,
    seconds=7
)

The internal representation of timedelta holds only days, seconds, and microseconds. So the above timedelta object is the same as datetime.timedelta(days=1, seconds=15487). The available keywords are weeks, days, hours, minutes, seconds, milliseconds, and microseconds. The module doesn't give access to months or years. This is logical since not all months or years are the same length.

subdivise_day, strptime, and replace

Now, let's look into subdivise_day. To access a schedule with a date, we must first turn it into a day with get_day. We then use schedule_day to return the schedule for the given day. This is a list of shift tuples.

Since days can have no shifts, one shift, or even several shifts, we have to loop through the shifts list.

We'll also loop through each shift by increments of SLOT_DURATION.

We discover more datetime functionality: the function datetime.strptime and the replace method.

strptime turns a time string into a datetime object. It is quite flexible because you can specify a "format string" to explain how to interpret the time. Format strings use their own mini-language. In datetime.strptime(start, "%H:%M"), the format string is "%H:%M". This means hours (%H) in 24-hour format, then a colon (:), then minutes (%M) as a decimal number. You can learn all about format strings here. Here are a few examples:

>>> from datetime import datetime

>>> datetime.strptime('Saturday 18/01/2025 21:49:37', '%A %d/%m/%Y %H:%M:%S')
datetime.datetime(2025, 1, 18, 21, 49, 37)

>>> datetime.strptime('Sat, Jan 18 2025, 09PM, 49 minutes and 37 seconds', "%a, %b %d %Y, %I%p, %M minutes and %S seconds")
datetime.datetime(2025, 1, 18, 21, 49, 37)

The replace method lets you change a datetime method. It swaps the year, month, day, hour, etc., of the datetime object with the arguments you provide. We need to set the year, month, and day because, in the last step, strptime only had a notion of a time, not a date. So, it gave a bogus default value. We need to provide our own values to create a meaningful datetime.

Slot sets logic

Now that we have useful abstractions like slots and schedules, we can build slot sets. Before, the way we used sets could lead you to believe they were only unordered collections. The primary purpose of this part of the program is to show you some useful set operations.

Before we dig into set operations, I will write two useful functions.

make_day_slots

The first will bridge the gap between schedules and slots once and for all. I created the make_day_slots function to generate all possible slots for a given date. It will combine all possible workers' slots. For example, suppose John and Anna both work a morning 09:00-12:00 shift on Saturdays. make_day_slots could generate all work slots for a given Saturday for both John and Anna. Try to read and understand the code of make_day_slots by yourself. If you don't remember what the items method does, reread it here.

def make_day_slots(schedules, date):
    """
    Generate the slots for date.
    """
    slots = set()
    for worker, schedule in schedules.items():
        time_list = subdivise_day(schedule, date)
        for datetime_point in time_list:
            slot = make_slot(datetime_point, worker)
            add_slot(slots, slot)
    return slots

filter_slots_by_worker

The next function takes a set of slots and returns those that belong to a worker. Try to write the function yourself first. It should accept a set of slots and a worker as argument and return a new set of slots. Here's a solution below.

def filter_slots_by_worker(slots, worker):
    """
    take a set of slots and a worker.
    return those slots belonging to the worker.
    """
    s = set()
    for slot in slots:
        if slot_worker(slot) == worker:
            add_slot(s, slot)
    return s

Set operations

Next, now that we have sets of slots, I'll show you how they can interact. This will illustrate set operations.

def assemble_slots(slots_1, *rest_slots):
    """
    Return a slots set containing the elements of the slots arguments. UNION
    """
    return slots_1.union(*rest_slots)

assemble_slots takes a variable number of slot sets. It returns a set containing all unique elements. Such an operation is a set union in math (and Python). The body of our function is a one-liner. Think about how you would write a similar function if we stored slots inside lists instead of sets. It would be much longer!

def common_slots(slots_1, *rest_slots):
    """
    return the elements common to all the slot sets.
    """
    return slots_1.intersection(*rest_slots)

def slots_diff(slots_1, *rest_slots):
    """
    the difference between the first slot set and the others.
    """
    return slots_1.difference(*rest_slots)

def slots_sym_diff(slots_1, slots_2):
    """
    the symmetric difference of the slot sets.
    """
    return slots_1.symmetric_difference(slots_2)

The last three functions are very similar. Their body is a one-liner set operation. common_slots is the intersection of all sets (i.e., the elements common to all sets). slots_diff is the difference (i.e., the elements in the first set that are not present in the other sets). slots_sym_diff is the symmetric difference (i.e., the elements belonging to only one set).

Here are some pictures to illustrate set operations.

  • Intersection (shown by its math symbol "∩"):

intersection

  • Union (shown by the math symbol "∪"):

union

  • Difference (shown here as "-", the math symbol is often "\"):

difference

  • Symmetric difference (shown as "Δ"):

symmetric difference

In Python, apart from the methods we saw earlier, you can also use the following operations:

  • set1 | set2 is the union of set1 and set2

  • set1 & set2 is the intersection of set1 and set2

  • set1 ^ set2 is the symmetric difference of set1 and set2

  • set1 - set2 is the difference of set1 and set2.

Appointments

Let's create a new abstraction on top of the previous ones. After all, barbershop clients don't care about slots and schedules. They care about making reservations to get their hair cut.

Low-level appointments function

I decided appointments are tuples of a client name string and a slot. For example, if Mrs. Smith has made a reservation for a haircut with John on 01/20/2025 at 09:30, the appointment would be as follows:

('Mrs. Smith", (datetime.datetime(2025, 1, 20, 9, 30), 'john')

Try to write a make_appointment function by yourself first. It would take a client and a slot as arguments and return an appointment object. Below is my take on it.

def make_appointment(client, slot):
    """
    return an appointment object
    """
    return client, slot

Now create four functions (don't look at my solution before you try it yourself):

  1. appointment_client to access an appointment's client

  2. appointment_slot to access an appointment's slot

  3. appointment_worker to access an appointment's worker

  4. appointment_datetime to access an appointment's datetime

Below are my solutions:

def appointment_client(appointment):
    """
    return an appointment object's client string
    """
    return appointment[0]

def appointment_slot(appointment):
    """
    return an appointment object's slot
    """
    return appointment[1]

def appointment_worker(appointment):
    """
    return an appointment object's worker string
    """
    return slot_worker(appointment_slot(appointment))

def appointment_datetime(appointment):
    """
    return an appointment object's datetime
    """
    return slot_date_time(appointment_slot(appointment))

We will collect appointments in sets. Create add_appointment, check_appointment, update_appointment, and remove_appointment. If you struggle, remember how we created a CRUD interface to work with slots.

Below is my take on it:

def add_appointment(appointments, appointment):
    """
    add an appointment to an appointments set
    """
    appointments.add(appointment)
    return appointments

def remove_appointment(appointments, appointment):
    """
    remove an appointment from an appointments set
    """
    appointments.discard(appointment)
    return appointments

def check_appointment(appointments, appointment):
    """
    check if appointment is in the appointments set
    """
    return appointment in appointments

def update_appointment(appointments, old_app, new_app):
    """
    change the old_app appointment to the new_app appointment
    in the appointments set
    """
    remove_appointment(appointments, old_app)
    add_appointment(appointments, new_app)
    return appointments

High-level appointment functions

make_appointment returns an appointment object. add_appointment adds an appointment to a set of appointments. But those two actions don't take slots into account. After all, you can't book a specific slot more than once! We'll create a new function. It will take four arguments:

  • the appointment

  • the set of appointments

  • a set of available slots

  • a set of reserved slots

We don't care about its return value. Try writing it yourself first, then look at an answer.

def create_appointment(appointments, appointment, available_slots, reserved_slots):
    """
    create an appointment in the appointments set
    only create the appointment if the slot is available
    mark the slot as reserved after the reservation
    """
    slot = appointment_slot(appointment)
    if check_slot(available_slots, slot):
        add_appointment(appointments, appointment)
        add_slot(reserved_slots, slot)
        print("Reservation made!")
    else:
        print("Slot not available")

create_appointment first isolates the slot from the appointment. It then checks if it's available (i.e., part of available_slots). If so, it adds the appointment and adds the slot to reserved_slots. You'll see later that available_slots is created every time create_appointment runs. Since it is single-use, we don't need to update it by removing the slot.

As we'll see, our program will maintain schedules, a set of reserved slots, and a set of booked appointments. Given that this data will be available to our program, we can create reserve thus:

def reserve(client, date_time, worker, schedules, reserved_slots, appointments):
    """
    reserve a date_time for the client with the worker.
    uses the set of schedules, reserved_slots, and appointments
    as additional arguments
    """
    worker_schedule = find_worker_schedule(schedules, worker)
    date_slots = make_day_slots(schedules,
                                date_time.date())
    if not date_slots:
        print("No slots exist that day.")
        return
    possible_slots = filter_slots_by_worker(date_slots, worker)
    available_slots = possible_slots - reserved_slots
    slot = make_slot(date_time, worker)
    appointment = make_appointment(client, slot)
    create_appointment(appointments, appointment, available_slots, reserved_slots)

reserve's arguments are closer to the info you would need in real life to make a booking.

Our function is straightforward because we call previous functions in succession.

We can then create some helpful functions to explore our appointments' set.

from pprint import pp

def filter_appointments_for_worker(appointments, worker):
    """
    given a set of appointments, return those belonging to
    a certain worker
    """
    if not appointments:
        return None

    a = set()
    for appointment in appointments:
        if appointment_worker(appointment) == worker:
            add_appointment(a, appointment)
    return a

def filter_appointments_for_date(appointments, date):
    """
    given a set of appointments, return those on a given date
    """
    if not appointments:
        return None

    a = set()
    for appointment in appointments:
        d = appointment_datetime(appointment).date()
        if date == d:
            add_appointment(a, appointment)
    return a

def show_appointments(appointments, worker=None, date=None):
    """
    show the appointments in the appointments set
    possibility of filtering by worker or by date
    """
    a = appointments
    if worker:
        a = filter_appointments_for_worker(a, worker)
    elif date:
        a = filter_appointments_for_date(a, date)
    pp(a). # Easy way to pretty-pint our structures

Example main loop

Here's an example main loop (+ constant data) to interact with our booking system. I didn't include all our program possibilities in the menu, but you can try adapting it as an exercise. For example, you could add an option to delete an appointment.

####################################
#
# MAIN LOGIC
#
####################################

S_JOHN = {
    "mon": [("09:00", "12:00"), ("13:00", "17:00")],
    "tue": [("09:00", "12:00"), ("13:00", "17:00")],
    "wed": [("09:00", "12:00")],
    "thu": [("09:00", "12:00"), ("13:00", "17:00")],
    "fri": [("10:00", "12:00"), ("13:00", "16:00")],
    "sat": [("13:00", "15:00")],
    "sun": []
}

S_ANNA = {
    "mon": [("09:00", "12:00"), ("13:00", "18:00")],
    "tue": [("09:00", "12:00"), ("13:00", "18:00")],
    "wed": [("09:00", "12:00"), ("13:00", "17:00")],
    "thu": [("09:00", "12:00"), ("13:00", "18:00")],
    "fri": [("13:00", "17:00")],
    "sat": [("09:00", "10:00")],
    "sun": []
}

SCHEDULES = {
    "john": S_JOHN,
    "anna": S_ANNA
}

def main():
    schedules = SCHEDULES
    reserved_slots = set()
    appointments = set()

    while True:
        message = input("""What will you do?
    [R]eserve
    [S]how appointments
    Show [W]orker's appointments
    Show appointments by [D]ate
    Show [T]oday's appointments
    [Q]uit
 > """)
        if message == "R":
            name = input("Client name: ")
            dt = make_datetime(request_date(),
                               request_time())
            worker = input("Choose a worker: ")
            reserve(name,
                    dt,
                    worker,
                    schedules,
                    reserved_slots,
                    appointments)
        elif message == "S":
            show_appointments(appointments)
        elif message == "W":
            worker = input("Choose a worker: ")
            show_appointments(appointments,
                              worker=worker)
        elif message == "D":
            d = request_date()
            show_appointments(appointments,
                              date=d)
        elif message == "T":
            d = date.today()
            show_appointments(appointments,
                              date=d)
        elif message == "Q":
            break
        else:
            print("Wrong command. Try again.")

    return

if __name__ == "__main__":
    main()

Now, let's test it.

% python3 Project5.py
What will you do?
    [R]eserve
    [S]how appointments
    Show [W]orker's appointments
    Show appointments by [D]ate
    Show [T]oday's appointments
    [Q]uit
 > R
Client name: Ms. Smith
Please enter a date (YYYY-MM-DD): 2025-01-13
Please enter a time in 24h format (HH:MM): 09:30
Choose a worker: anna
Reservation made!
What will you do?
    [R]eserve
    [S]how appointments
    Show [W]orker's appointments
    Show appointments by [D]ate
    Show [T]oday's appointments
    [Q]uit
 > S
{('Ms. Smith', (datetime.datetime(2025, 1, 13, 9, 30), 'anna'))}
What will you do?
    [R]eserve
    [S]how appointments
    Show [W]orker's appointments
    Show appointments by [D]ate
    Show [T]oday's appointments
    [Q]uit
 > W
Choose a worker: anna
{('Ms. Smith', (datetime.datetime(2025, 1, 13, 9, 30), 'anna'))}
What will you do?
    [R]eserve
    [S]how appointments
    Show [W]orker's appointments
    Show appointments by [D]ate
    Show [T]oday's appointments
    [Q]uit
 > D
Please enter a date (YYYY-MM-DD): 2025-01-13
{('Ms. Smith', (datetime.datetime(2025, 1, 13, 9, 30), 'anna'))}
What will you do?
    [R]eserve
    [S]how appointments
    Show [W]orker's appointments
    Show appointments by [D]ate
    Show [T]oday's appointments
    [Q]uit
 > T
set()
What will you do?
    [R]eserve
    [S]how appointments
    Show [W]orker's appointments
    Show appointments by [D]ate
    Show [T]oday's appointments
    [Q]uit
 > Q

Topics we've covered

Abstraction layers

We built our program with layers of abstraction. They are a way to structure your program and manage complexity. You make each layer depending only on the one below it. This lets you ignore the implementation details of the layers below that.

What is an abstraction? It is a black box: you don't need to know what's inside to use it. Using layers of abstraction is like having black boxes nested like Russian dolls. Each abstraction builds upon the previous abstraction, not caring about the deeper abstractions.

The lowest layers use the programming language's basic data structures and functions. For example, make_slot is simple tuple creation, and lot_worker is simple indexing.

It's possible to write your program without stacking abstractions on each other. We could rewrite <main> to contain the whole program, using only basic Python features. It would be torture to write and even worse to read and maintain. You can try it as an exercise in masochism if you're so inclined.

If you feel overwhelmed by all this, don't worry. Structuring your programs by levels of abstraction takes practice. It is a balancing act between creating too many granular abstractions and too few. You can only feel where to set the cursor by making your own programs.

Operations between sets

Our program used set operations: union, intersection, difference, and symmetric difference. Those operations take sets as parameters.

Operations on sets have both a symbol and a method. For example, intersection is & and set.intersection. set1 & set2 and set1.intersection(set2) are the same. It is possible to chain operations. Python will evaluate them left-to-right: set1 & set2 & set3 is the same as

intermediate_result = set1 & set2
intermediate_result & set3

Parentheses change the order of evaluation. set1 & (set2 - set3) is the same as

intermediate_result = set2 - set3
set1 & intemediate_result

Note that the operations are associative: (set1 & set2) & set3 is the same as set1 & (set2 & set3) and set1 & set2 & set3.

Although we can combine more than two sets with operation symbols, using a method in that case is better.

Below is a brief overview of available operations. We didn't use subsets, supersets, and disjoint. I encourage you to add them to our program to see how they work.

  • Subset: the symbol is <=, and the method is set.issubset. It tests if all the elements of one set are in the other set. The value is a boolean. {1, 2} <= {1, 2, 3} returns True.

  • Superset: the symbol is >= and the method is set.issuperset. It tests if a set contains all the elements of another. This is the inverse operation of subset checking. The value is also a boolean. {1, 2, 3} >= {1, 2} returns True.

  • Disjoint: This operation has no operation symbol; the method is set.isdisjoint. It checks if two sets don't have any elements in common. The value is a boolean. {1, 2, 3}.isdisjoint({4, 5}, {6, 7}) returns True.

  • Union: the symbol is |, and the method is set.union. It combines the elements of several sets and removes duplicates. The result is a set. {1, 2, 3}.union({2, 4}, {6}) returns {1, 2, 3, 4, 6}.

  • Intersection: the symbol is &, and the method is set.intersection. The intersection is the set of elements common to all sets. The return value is thus a set. {1, 2, 3}.intersection({2, 4}, {2, 3, 7}) returns {2}.

  • Difference: the symbol is - and the method is set.difference. The difference is the set of elements in one set but not the others. The return value is a set. {1, 2, 3}.difference({2, 4, 6}, {3, 7}) is {1}.

  • Symmetric difference: the symbol is ^, and the method is set.symmetric_difference. It gets the elements appearing in either set but not both. You can only compute the symmetric difference between two sets. You'll get an error if you try {1, 2, 3}.symmetric_difference({1, 4}, {1, 2, 5}) . {1, 2, 4}.symmetric_difference({1, 4}) is {2, 3, 4}.

The difference between difference and symmetric_difference is subtle. Images make things clearer, so I'll repeat the graphics you saw earlier.

difference

symmetric difference

Modules

Modules and libraries are the greatest strength of Python.

A module is a single-file Python library. Libraries are reusable code you can use in your programs. They contain constants, variables, classes, and functions. You can create your own or download them online (I'll show you in a future post).

Access to a wide range of ready-to-use specialized code is a Good Thing. It relieves the programmer of reinventing the wheel with every program.

This has two drawbacks:

  1. You rely on someone else's code. Third-party code never fits 100% of your use case. Sometimes, the module programmers will make changes that break your code. This is not very pleasant, but by and large, the Python ecosystem is terrific. You need good reasons to avoid using a mature and popular library. Indeed, your own homegrown solution will be worse most of the time.

  2. It doesn't teach you programming skills to import a library that does all the heavy lifting. When you're learning, you need a roll-your-own mentality. Libraries abstract some standard programming algorithms and data structures. They are common and widely used, so you should know about them!

Using modules is straightforward. You either import a whole module or only select functionality. You then type from module import whatever_1, whatever_2. If you import an entire module, you must preface the symbols with the module's name. Both examples return today's date.

import datetime

datetime.date.today()
from datetime import date

date.today()

For convenience, we can rename modules: e.g., import datetime as dt.

The datetime module

The datetime module is a built-in module that deals with dates and times in your programs. We used some types of objects provided by the module:

  • date: only a date. It holds a year, a month, and a day.

  • time: only a time. It holds an hour, minutes, seconds, and microseconds.

  • datetime: a date and a time in the same object.

  • timedelta: a duration. You use it to calculate the difference between two dates or times.

To create a datetime object, you specify it directly or combine a date and a time. datetime.datetime(2025, 1, 31, 16, 42, 18) is the same as datetime.combine(datetime.date(2025, 1, 31), datetime.time(16, 42, 18)).

You can access parts of a date with date.year, date.month and date.day. The same goes for parts of a time object: time.hour, time.minute, time.second, and finally time.microsecond. Those don't only work on dates and times, but also on datetimes. Note the lack of parentheses after year, month, day, hour, etc. These are not methods or functions.

Using format strings, we turn strings into datetime objects with the strptime function. We do it the other way around (datetimes -> string) with strftime.

When we add or subtract a timedelta object from a datetime, we add or remove the specified duration.

import datetime as dt

new_date = dt.now() + dt.timedelta(days=5)

To close our talk about the datetime module, keep in mind we compare datetime objects with <, >, ==, <=, >=.

Exercises

Abstraction Layers

First, let's get you to create some simple abstractions.

  1. You'll create an abstraction representing groceries as a name and a price. The name will be a string, and the price will be a floating-point number. A grocery item will hold these two values inside a tuple. You'll create functions make_grocery to create a grocery item. grocery_name will extract the name and grocery_price will return the price.

  2. You'll create an abstraction for students. Represent students with a dictionary holding their name, age, major, year (e.g., 'freshman'), and GPA. Create the functions make_student, student_name, student_age, student_major, student_year, and student_GPA.

  3. Create a book abstraction as a tuple, holding the title and a list of authors. Write make_book, book_title, and book_authors.

  4. Create a system to manage student records. You have already created a student abstraction, so you will reuse it. Student records will sit inside a set. Create functions to create, read, update, and delete student records.

  5. Update the student records' structure. Now, it will also hold detailed grades inside a dictionary. E.g., {'math': 'A', 'physics': 'B-'}. Create the appropriate functions.

  6. (this will take some effort; take your time). Write a program to manage to-do lists with layers of abstractions. Tasks in the list will hold a title, priority, and due date. Users should be able to:

    1. Create new tasks

    2. Display the list (with the option to order tasks by priority and due date)

    3. mark tasks as completed

    4. edit tasks,

    5. delete tasks

Set Operations

  1. Create two sets of numbers and find their union, intersection, and difference.

  2. Check if one of those sets is a subset of another.

  3. Create a set of vowels. Write a function taking a word as input. Turn the word from a string to a set of its letters. Find how many vowels are in the word (hint: find an intersection).

  4. Here are two sets of students: morning_class = {"Alice", "Bob", "Charlie"} and evening_class = {"Charlie", "David", "Eve"}. Find the students attending only one class and those attending both.

  5. Here are three sets: set_a = {1, 2, 3}, set_b = {3, 4, 5}, and set_c = {5, 6, 7}. Find the union of all three sets. Find the intersection of all three sets.

  6. Create a dictionary of employees as keys and their skill set as values. Employees have different skills stored in a set. Find a function to find common skills between employees.

  7. Use the book abstraction you made before. Simulate a library with three sets: library, available and borrowed. Use set operations to manage borrowing and returning books.

  8. You want to meet up with your friends. They have all different availabilities (use datetime.date objects). Create a set for each friend's possible dates. Find the best day for your party.

  9. A worker logs the tasks he completed for two days. day_1 = {"task1", "task2", "task3", "task4"} and day2_tasks = {"task3", "task4", "task5", "task6"}. Find the tasks completed on either day but not both (symmetric difference). Determine how many tasks are unique to each day.

The datetime Module

  1. Write a program to print today's date in the format YYYY-MM-DD.

  2. Ask the user for a date and determine the day of the week.

  3. Create a datetime object for a specific date and time and print it.

  4. Write a program that calculates the number of days until your next birthday.

  5. Calculate the number of seconds between two datetime objects.

  6. Write a function to add a given number of days, hours, and minutes to a datetime object and return the new datetime.

< Previous Post

0
Subscribe to my newsletter

Read articles from Had Will directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Had Will
Had Will