Part 5 - Barbershop Appointment Booking System


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
typea
time
typea
datetime
typea
timedelta
type(two timezone-related classes we won't be using in our program:
tzinfo
andtimezone
)
What date
and time
do is self-explanatory. They provide a way to deal with idealized dates and times.
datetime | datetime | date | time |
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.
pop | discard | |
removes an element from the set | yes | yes |
return value | the removed element | None |
arguments | none | the element |
if the element is not in the set | not applicable | does nothing |
if the set is empty | KeyError | no 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:
Add the new slot to slots.
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 "∩"):
- Union (shown by the math symbol "∪"):
- Difference (shown here as "-", the math symbol is often "\"):
- Symmetric difference (shown as "Δ"):
In Python, apart from the methods we saw earlier, you can also use the following operations:
set1 | set2
is the union of set1 and set2set1 & set2
is the intersection of set1 and set2set1 ^ set2
is the symmetric difference of set1 and set2set1 - 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):
appointment_client
to access an appointment's clientappointment_slot
to access an appointment's slotappointment_worker
to access an appointment's workerappointment_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 isset.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}
returnsTrue
.Superset: the symbol is
>=
and the method isset.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}
returnsTrue
.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})
returnsTrue
.Union: the symbol is
|
, and the method isset.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 isset.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 isset.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 isset.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.
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:
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.
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.
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 andgrocery_price
will return the price.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
, andstudent_GPA
.Create a book abstraction as a tuple, holding the title and a list of authors. Write
make_book
,book_title
, andbook_authors
.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.
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.(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:
Create new tasks
Display the list (with the option to order tasks by priority and due date)
mark tasks as completed
edit tasks,
delete tasks
Set Operations
Create two sets of numbers and find their union, intersection, and difference.
Check if one of those sets is a subset of another.
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).
Here are two sets of students:
morning_class = {"Alice", "Bob", "Charlie"}
andevening_class = {"Charlie", "David", "Eve"}
. Find the students attending only one class and those attending both.Here are three sets:
set_a = {1, 2, 3}
,set_b = {3, 4, 5}
, andset_c = {5, 6, 7}
. Find the union of all three sets. Find the intersection of all three sets.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.
Use the book abstraction you made before. Simulate a library with three sets:
library
,available
andborrowed
. Use set operations to manage borrowing and returning books.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.A worker logs the tasks he completed for two days.
day_1 = {"task1", "task2", "task3", "task4"}
andday2_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
Write a program to print today's date in the format
YYYY-MM-DD
.Ask the user for a date and determine the day of the week.
Create a datetime object for a specific date and time and print it.
Write a program that calculates the number of days until your next birthday.
Calculate the number of seconds between two datetime objects.
Write a function to add a given number of days, hours, and minutes to a datetime object and return the new datetime.
Subscribe to my newsletter
Read articles from Had Will directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
