Designing New Functions: A Recipe

Writing a good essay requires planning: deciding on a topic, learning the background material, writing an outline, and then filling in the outline until you’re done.

Similarly, writing a good function also requires planning. You have an idea of what you want the function to do, but you need to decide on the details. Every time you write a function, you need to figure out the answers to the following questions:

The function design recipe helps you find answers to all these questions.

This section describes a step-by-step recipe for designing and writing a function. Part of the outcome will be a working function, but almost as important is the documentation for the function. Python uses three double quotes to start and end this documentation; everything in between is meant for humans to read. This notation is called a docstring, which is short for documentation string.

Here is an example of a completed function. We’ll show you how we came up with this using a function design recipe (FDR), but it helps to see a completed example first:

 >>>​​ ​​def​​ ​​days_difference(day1:​​ ​​int,​​ ​​day2:​​ ​​int)​​ ​​->​​ ​​int:
 ...​​ ​​"""Return the number of days between day1 and day2, which are
 ... both in the range 1-365 (thus indicating the day of the
 ... year).
 ...
 ... >>> days_difference(200, 224)
 ... 24
 ... >>> days_difference(50, 50)
 ... 0
 ... >>> days_difference(100, 99)
 ... -1
 ... """
 ...​​ ​​return​​ ​​day2​​ ​​-​​ ​​day1
 ...

Here are the parts of the function, including the docstring:

There are five steps to the function design recipe. It may seem like a lot of work at first, and you will often be able to write a function without rigidly following these steps, but this recipe can save you hours of time when you’re working on more complicated functions.

  1. Examples. The first step is to figure out what name you want to give to your function, what arguments it should have, and what information it will return. This name is often a short answer to the question, “What does your function do?” Type a couple of example calls and return values.

    We start with the examples because they’re the easiest: before we write anything, we need to decide what information we have (the argument values) and what information we want the function to produce (the return value). Here are the examples from days_difference:

     ...​​ ​​>>>​​ ​​days_difference(200,​​ ​​224)
     ...​​ ​​24
     ...​​ ​​>>>​​ ​​days_difference(50,​​ ​​50)
     ...​​ ​​0
     ...​​ ​​>>>​​ ​​days_difference(100,​​ ​​99)
     ...​​ ​​-1
  2. Header. The second step is to decide on the parameter names, parameter types, and return type and write the function header. Pick meaningful parameter names to make it easy for other programmers to understand what information to give to your function. Include type annotations: Are you giving it integers? Floating-point numbers? Maybe both? We’ll see a lot of other types in the upcoming chapters, so practicing this step now while you have only a few choices will help you later. If the answer is, “Both integers and floating-point numbers,” then use float because integers are a subset of floating-point numbers.

    Also, what type of value is returned? An integer, a floating-point number, or possibly either one of them?

    The parameter types and return type form a type contract because we are claiming that if you call this function with the right types of values, we’ll give you back the right type of value. (We’re not saying anything about what will happen if we get the wrong kind of values.)

    Here is the header from days_difference:

     >>>​​ ​​def​​ ​​days_difference(day1:​​ ​​int,​​ ​​day2:​​ ​​int)​​ ​​->​​ ​​int:
  3. Description. Write a short paragraph describing your function: this is what other programmers will read in order to understand what your function does, so it’s important to practice this! Mention every parameter in your description and describe the return value. Here is the description from days_difference:

     ...​​ ​​"""Return the number of days between day1 and day2, which are
     ... both in the range 1-365 (thus indicating the day of the
     ... year).
  4. Body. By now, you should have a good idea of what you need to do in order to get your function to behave properly. It’s time to write some code! Here is the body from days_difference:

     ...​​ ​​return​​ ​​day2​​ ​​-​​ ​​day1
  5. Test. Run the examples to make sure your function body is correct. Feel free to add more example calls if you happen to think of them. For days_difference, we copy and paste our examples into the shell and compare the results to what we expected:

     >>>​​ ​​days_difference(200,​​ ​​224)
     24
     >>>​​ ​​days_difference(50,​​ ​​50)
     0
     >>>​​ ​​days_difference(100,​​ ​​99)
     -1

Designing Three Birthday-Related Functions

We’ll now apply our function design recipe to solve this problem: Which day of the week will a birthday fall upon, given what day of the week it is today and what day of the year the birthday is on? For example, if today is the third day of the year and it’s a Thursday, and a birthday is on the 116th day of the year, what day of the week will it be on that birthday?

We’ll design three functions that together will help us do this calculation. We’ll write them in the same file; until we get to Chapter 6, A Modular Approach to Program Organization, we’ll need to put functions that we write in the same file if we want to be able to have them call one another.

We will represent the day of the week using 1 for Sunday, 2 for Monday, and so on:

Day of the WeekNumber

Sunday

1

Monday

2

Tuesday

3

Wednesday

4

Thursday

5

Friday

6

Saturday

7

We are using these numbers simply because we don’t yet have the tools to easily convert between days of the week and their corresponding numbers. We’ll have to do that translation in our heads.

For the same reason, we will also ignore months and use the numbers 1 through 365 to indicate the day of the year. For example, we’ll represent February 1st as 32, since it’s the thirty-second day of the year.

How Many Days Difference?

We’ll start by seeing how we came up with function days_difference. Here are the function design recipe steps. Try following along in the Python shell.

  1. Examples. We want a clear name for the difference in days; we’ll use days_difference. In our examples, we want to call this function and state what it returns. If we want to know how many days there are between the 200th day of the year and the 224th day, we can hope that this will happen:

     ...​​ ​​>>>​​ ​​days_difference(200,​​ ​​224)
     ...​​ ​​24

    What are the special cases? For example, what if the two days are the same? How about if the second one is before the first?

     ...​​ ​​>>>​​ ​​days_difference(50,​​ ​​50)
     ...​​ ​​0
     ...​​ ​​>>>​​ ​​days_difference(100,​​ ​​99)
     ...​​ ​​-1

    Now that we have a few examples, we can move on to the next step.

  2. Header. We have a couple of example calls. The arguments in our function call examples are all integers, and the return values are integers too, so that gives us the type contract. In the examples, both arguments represent a number of days, so we’ll name them day1 and day2:

     >>>​​ ​​def​​ ​​days_difference(day1:​​ ​​int,​​ ​​day2:​​ ​​int)​​ ​​->​​ ​​int:
  3. Description. We’ll now describe what a call on the function will do. Because the documentation should completely describe the behavior of the function, we need to make sure that it’s clear what the parameters mean:

     ...​​ ​​"""Return the number of days between day1 and day2, which are
     ... both in the range 1-365 (thus indicating the day of the
     ... year).
  4. Body. We’ve laid everything out. Looking at the examples, we see that we can implement this using subtraction. Here is the whole function again, including the body:

     >>>​​ ​​def​​ ​​days_difference(day1:​​ ​​int,​​ ​​day2:​​ ​​int)​​ ​​->​​ ​​int:
     ...​​ ​​"""Return the number of days between day1 and day2, which are
     ... both in the range 1-365 (thus indicating the day of the
     ... year).
     ...
     ... >>> days_difference(200, 224)
     ... 24
     ... >>> days_difference(50, 50)
     ... 0
     ... >>> days_difference(100, 99)
     ... -1
     ... """
     ...​​ ​​return​​ ​​day2​​ ​​-​​ ​​day1
     ...
  5. Test. To test it, we fire up the Python shell and copy and paste the calls into the shell, checking that we get back what we expect:

     >>>​​ ​​days_difference(200,​​ ​​224)
     24
     >>>​​ ​​days_difference(50,​​ ​​50)
     0
     >>>​​ ​​days_difference(100,​​ ​​99)
     -1

Here’s something really cool. Now that we have a function with a docstring, we can call help on that function:

 >>>​​ ​​help(days_difference)
 Help on function days_difference in module __main__:
 
 days_difference(day1:int, day2:int) -> int
  Return the number of days between day1 and day2, which are both
  in the range 1-365 (thus indicating the day of the year).
 
  >>>​​ ​​days_difference(200,​​ ​​224)
  24
  >>>​​ ​​days_difference(50,​​ ​​50)
  0
  >>>​​ ​​days_difference(100,​​ ​​99)
  -1

What Day Will It Be in the Future?

It will help our birthday calculations if we write a function to calculate what day of the week it will be given the current weekday and how many days ahead we’re interested in. Remember that we’re using the numbers 1 through 7 to represent Sunday through Saturday.

Again, we’ll follow the function design recipe:

  1. Examples. We want a short name for what it means to calculate what weekday it will be in the future. We could choose something like which_weekday or what_day; we’ll use get_weekday. There are lots of choices.

    We’ll start with an example that asks what day it will be if today is Tuesday (day 3 of the week) and we want to know what tomorrow will be (1 day ahead):

     >>>​​ ​​get_weekday(3,​​ ​​1)
     4

    Whenever we have a function that should return a value in a particular range, we should write example calls where we expect either end of that range as a result.

    What if it’s Friday (day 6)? If we ask what day it will be tomorrow, we expect to get Saturday (day 7):

     >>>​​ ​​get_weekday(6,​​ ​​1)
     7

    What if it’s Saturday (day 7)? If we ask what day it will be tomorrow, we expect to get Sunday (day 1):

     >>>​​ ​​get_weekday(7,​​ ​​1)
     1

    We’ll also try asking about 0 days in the future as well as a week ahead; both of these cases should give back the day of the week we started with:

     >>>​​ ​​get_weekday(1,​​ ​​0)
     1
     >>>​​ ​​get_weekday(4,​​ ​​7)
     4

    Let’s also try 10 weeks and 2 days in the future so we have a case where there are several intervening weeks:

     >>>​​ ​​get_weekday(7,​​ ​​72)
     2
  2. Header. In our example calls, the arguments are all integers, and the return values are integers too, so that gives us our type contract.

    The first argument is the current day of the week, so we’ll name it current_weekday. The second argument is how many days from now to calculate. We’ll pick the name days_ahead, although days_from_now would also be fine:

     >>>​​ ​​def​​ ​​get_weekday(current_weekday:​​ ​​int,​​ ​​days_ahead:​​ ​​int)​​ ​​->​​ ​​int:
  3. Description. We need a complete description of what this function will do. We’ll start with a sentence describing what the function does, and then we’ll describe what the parameters mean:

     ...​​ ​​"""Return which day of the week it will be days_ahead days
     ... from current_weekday.
     ...
     ... current_weekday is the current day of the week and is in
     ... the range 1-7, indicating whether today is Sunday (1),
     ... Monday (2), ..., Saturday (7).
     ...
     ... days_ahead is the number of days after today.

    Notice that our first sentence uses both parameters and also describes what the function will return.

  4. Body. Looking at the examples, we see that we can solve the first example with this: return current_weekday + days_ahead. That, however, won’t work for all of the examples; we need to wrap around from day 7 (Saturday) back to day 1 (Sunday). When you have this kind of wraparound, usually the remainder operator, %, will help. Notice that evaluation of (7 + 1) % 7 produces 1, (7 + 2) % 7 produces 2, and so on.

    Let’s try taking the remainder of the sum: return current_weekday + days_ahead % 7. Here is the whole function again, including the body:

     >>>​​ ​​def​​ ​​get_weekday(current_weekday:​​ ​​int,​​ ​​days_ahead:​​ ​​int)​​ ​​->​​ ​​int:
     ...​​ ​​"""Return which day of the week it will be days_ahead days from
     ... current_weekday.
     ...
     ... current_weekday is the current day of the week and is in the
     ... range 1-7, indicating whether today is Sunday (1), Monday (2),
     ... ..., Saturday (7).
     ...
     ... days_ahead is the number of days after today.
     ...
     ... >>> get_weekday(3, 1)
     ... 4
     ... >>> get_weekday(6, 1)
     ... 7
     ... >>> get_weekday(7, 1)
     ... 1
     ... >>> get_weekday(1, 0)
     ... 1
     ... >>> get_weekday(4, 7)
     ... 4
     ... >>> get_weekday(7, 72)
     ... 2
     ... """
     ...​​ ​​return​​ ​​current_weekday​​ ​​+​​ ​​days_ahead​​ ​​%​​ ​​7
     ...
  5. Test. To test it, we fire up the Python shell and copy and paste the calls into the shell, checking that we get back what we expect:

     >>>​​ ​​get_weekday(3,​​ ​​1)
     4
     >>>​​ ​​get_weekday(6,​​ ​​1)
     7
     >>>​​ ​​get_weekday(7,​​ ​​1)
     8

    Wait, that’s not right. We expected a 1 on that third example, not an 8, because 8 isn’t a valid number for a day of the week. We should have wrapped around to 1.

    Taking another look at our function body, we see that because % has higher precedence than +, we need parentheses:

     >>>​​ ​​def​​ ​​get_weekday(current_weekday:​​ ​​int,​​ ​​days_ahead:​​ ​​int)​​ ​​->​​ ​​int:
     ...​​ ​​"""Return which day of the week it will be days_ahead days
     ... from current_weekday.
     ...
     ... current_weekday is the current day of the week and is in
     ... the range 1-7, indicating whether today is Sunday (1),
     ... Monday (2), ..., Saturday (7).
     ...
     ... days_ahead is the number of days after today.
     ...
     ... >>> get_weekday(3, 1)
     ... 4
     ... >>> get_weekday(6, 1)
     ... 7
     ... >>> get_weekday(7, 1)
     ... 1
     ... >>> get_weekday(1, 0)
     ... 1
     ... >>> get_weekday(4, 7)
     ... 4
     ... >>> get_weekday(7, 72)
     ... 2
     ... """
     ...​​ ​​return​​ ​​(current_weekday​​ ​​+​​ ​​days_ahead)​​ ​​%​​ ​​7
     ...

    Testing again, we see that we’ve fixed that bug in our code, but now we’re getting the wrong answer for the second test!

     >>>​​ ​​get_weekday(3,​​ ​​1)
     4
     >>>​​ ​​get_weekday(6,​​ ​​1)
     0
     >>>​​ ​​get_weekday(7,​​ ​​1)
     1

    The problem here is that when current_weekday + days_ahead evaluates to a multiple of 7, then (current_weekday + days_ahead) % 7 will evaluate to 0, not 7. All the other results work well; it’s just that pesky 7.

    Because we want a number in the range 1 through 7 but we’re getting an answer in the range 0 through 6 and all the answers are correct except that we’re seeing a 0 instead of a 7, we can use this trick:

    1. Subtract 1 from the expression: current_weekday + days_ahead - 1.

    2. Take the remainder.

    3. Add 1 to the entire result: (current_weekday + days_ahead - 1) % 7 + 1.

    Let’s test it again:

     >>>​​ ​​get_weekday(3,​​ ​​1)
     4
     >>>​​ ​​get_weekday(6,​​ ​​1)
     7
     >>>​​ ​​get_weekday(7,​​ ​​1)
     1
     >>>​​ ​​get_weekday(1,​​ ​​0)
     1
     >>>​​ ​​get_weekday(4,​​ ​​7)
     4
     >>>​​ ​​get_weekday(7,​​ ​​72)
     2

    We’ve passed all the tests, so we can now move on.

What Day Is My Birthday On?

We now have two functions related to day-of-year calculations. One of them calculates the difference between two days of the year. The other calculates the weekday for a day in the future given the weekday today. We can use these two functions to help figure out what day of the week a birthday falls on given what day of the week it is today, what the current day of the year is, and what day of the year the birthday falls on. Again, we’ll follow the function design recipe:

  1. Examples. We want a name for what it means to calculate what weekday a birthday will fall on. Once more, there are lots of choices; we’ll use get_birthday_weekday.

    If today is a Thursday (day 5 of the week), and today is the third day of the year, what day will it be on the fourth day of the year? Hopefully Friday:

     >>>​​ ​​get_birthday_weekday(5,​​ ​​3,​​ ​​4)
     6

    What if it’s the same day (Thursday, the 3rd day of the year), but the birthday is the 116th day of the year? For now, we can verify externally (looking at a calendar) that it turns out to be a Friday.

     >>>​​ ​​get_birthday_weekday(5,​​ ​​3,​​ ​​116)
     6

    What if today is Friday, April 26, the 116th day of the year, but the birthday we want is the 3rd day of the year? This is interesting because the birthday is a couple months before the current day:

     >>>​​ ​​get_birthday_weekday(6,​​ ​​116,​​ ​​3)
     5
  2. Header. In our example calls, the arguments are all integers, and the return values are integers too, so that gives us our type contract. We’re happy enough with the function name so again we’ll stick with it.

    The first argument is the current day of the week, so we’ll use current_weekday, as we did for the previous function. (It’s a good idea to be consistent with naming when possible.) The second argument is what day of the year it is today, and we’ll choose current_day. The third argument is the day of the year the birthday is, and we’ll choose birthday_day:

     >>>​​ ​​def​​ ​​get_birthday_weekday(current_weekday:​​ ​​int,​​ ​​current_day:​​ ​​int,
     ...​​ ​​birthday_day:​​ ​​int)​​ ​​->​​ ​​int:
  3. Description. We need a complete description of what this function will do. We’ll start with a sentence describing what the function does, and then we’ll describe what the parameters mean:

     ...​​ ​​"""Return the day of the week it will be on birthday_day,
     ... given that the day of the week is current_weekday and the
     ... day of the year is current_day.
     ...
     ... current_weekday is the current day of the week and is in
     ... the range 1-7, indicating whether today is Sunday (1),
     ... Monday (2), ..., Saturday (7).
     ...
     ... current_day and birthday_day are both in the range 1-365.

    Again, notice that our first sentence uses all parameters and also describes what the function will return. If it gets more complicated, we’ll start to write multiple sentences to describe what the function does, but we managed to squeeze it in here.

  4. Body. It’s time to write the body of the function. We have a puzzle:

    1. Using days_difference, we can figure out how many days there are between two days.

      Using get_weekday, we can figure out what day of the week it will be given the current day of the week and the number of days away.

    We’ll start by figuring out how many days from now the birthday falls:

     ...​​ ​​days_diff​​ ​​=​​ ​​days_difference(current_day,​​ ​​birthday_day)

    Now that we know that, we can use it to solve our problem: given the current weekday and that number of days ahead, we can call function get_weekday to get our answer:

     ...​​ ​​return​​ ​​get_weekday(current_weekday,​​ ​​days_diff)

    Let’s put it all together:

     >>>​​ ​​def​​ ​​get_birthday_weekday(current_weekday:​​ ​​int,​​ ​​current_day:​​ ​​int,
     ...​​ ​​birthday_day:​​ ​​int)​​ ​​->​​ ​​int:
     ...​​ ​​"""Return the day of the week it will be on birthday_day,
     ... given that the day of the week is current_weekday and the
     ... day of the year is current_day.
     ...
     ... current_weekday is the current day of the week and is in
     ... the range 1-7, indicating whether today is Sunday (1),
     ... Monday (2), ..., Saturday (7).
     ...
     ... current_day and birthday_day are both in the range 1-365.
     ...
     ... >>> get_birthday_weekday(5, 3, 4)
     ... 6
     ... >>> get_birthday_weekday(5, 3, 116)
     ... 6
     ... >>> get_birthday_weekday(6, 116, 3)
     ... 5
     ... """
     ...​​ ​​days_diff​​ ​​=​​ ​​days_difference(current_day,​​ ​​birthday_day)
     ...​​ ​​return​​ ​​get_weekday(current_weekday,​​ ​​days_diff)
     ...
  5. Test. To test it, we fire up the Python shell and copy and paste the calls into the shell, checking that we get back what we expect:

     >>>​​ ​​get_birthday_weekday(5,​​ ​​3,​​ ​​4)
     6
     >>>​​ ​​get_birthday_weekday(5,​​ ​​3,​​ ​​116)
     6
     >>>​​ ​​get_birthday_weekday(6,​​ ​​116,​​ ​​3)
     5

And we’re done!