Why You Should Write Code Like a Recipe

Anissa Mike
When I Work Data
Published in
8 min readApr 7, 2022

Increase the readability and organization of your code

Note: This post is written with Python users in mind.

Recipes are easy to understand, have a standard format, and contain everything you need to replicate a dish. Formatting your code like a recipe will help you have more readable code that is easy to jump back into and to share with others.

Let’s take an example recipe (it’s ok to skim it — we’re looking at overall structure here):

Love Noodles

A favorite in our house, this easy pasta dish can be made with one pan and a pot!

Note: You’ll need a large pan for this recipe

Ingredients

6–8 cloves garlic

1 cup fresh basil

8 to 16 ounces spinach

8.5 ounce jar of dried tomatoes, in oil

2 14-ounce cans quartered artichoke hearts

8 ounces plant-based feta (I like violife)

16 ounces spiral or penne noodles (gluten free or regular)

salt, to taste

pepper, to taste

Instructions

Start noodles cooking according to packages instructions. When finished, drain and rinse with cool water.

Drain artichoke hearts and toss a few times in colander to get off excess liquid. Cut the artichokes so that the top and the leaves are separated.

Add oil from dried tomatoes into pan over medium heat. Once heated, add drained artichoke hearts to pan and heat artichoke hearts for 6–8 minutes.

While artichokes are cooking, finely chop garlic and roughly chop spinach and basil. Cut feta into small squares.

Lower to medium-low heat and add garlic and sun dried tomatoes to pan with artichokes. Cook until sun dried tomatoes are softened, stirring occasionally.

Add spinach in batches, stirring with other ingredients, and let wilt before adding another batch. Sprinkle basil into the pan, stirring to incorporate. Make sure not add it in a big clump because it will tend to cook together.

Once spinach and basil are wilted, add noodles and feta to the pan. Stir to incorporate everything evenly and let everything heat thoroughly for another 5–7 minutes. Add salt and pepper to taste.

Serve!

All recipes start with a title and may also contain some basic information before the bulk of the recipe. This lets the reader know what they are in for and is an opportunity to give them some background information and context for what’s going on. Similarly, having a docstring at the top of your code can help a reader or reviewer (or even you, if you’re digging back into an old repository) gain a quick understanding of what is being achieved in a certain file.

"""
A module for processing ingredients information into expected format (love noodles).
"""

Recipes contain two basic sections: an ingredient list and an instructions section.

All recipes start with their ingredient list, which gives the basic set up and lets you know everything that will be used throughout the recipe so you know what you need before you start. This is similar to placing all the imports and configurations at the top of a file.

Of course, code needs to be much more explicit than a recipe. With a recipe, we generally assume a base level of knowledge and resources. For example, we assume the reader has basic kitchen supplies like a knife, cutting board, pan, colander, etc. In code we need to specify everything we’re bringing to a project.

import colander
import knife
from oven import stove
low_heat = 3
medium_heat = 5
high_heat = 8
ingredients = {
'garlic': '6 cloves garlic',
'basil': '1 cup basil',
'spinach': '8 ounces spinach',
'tomatoes': 'dried tomatoes’,
'oil': 'oil from dried tomatoes',
'artichokes': '2 cans artichoke hearts, drained',
'feta': '8 ounce block feta',
'noodles: '16 ounce box penne',
'salt': '1/2 tsp',
'pepper': '1/2 tsp'
}

(Note that while we are being explicit in everything we’re bringing to the file, we can still capitalize on existing knowledge by using imports from existing libraries. Instead of giving instructions on how to sharpen_knife or buy_oven, I know that work has already been done and so can import them, instead of recreating the wheel.)

This organization scheme is helpful because it gives some preparation for what’s to come and it also gives a central location to look to for definitions. For example, if I come across a variable target_date and need to know what date it contains, I can look to the top of the file instead of searching for target_date across the entire file. Using a configs.py file or an init step in a notebook can also be helpful for storing information if it will be used across multiple files.

The instructions section tells you how to take all your ingredients and tools and put them all together to get a delicious dish. This is what the rest of your code should do too — give clear, linear instructions on how to achieve the desired output. If we take the first few steps of the recipe as an example, it might look something like:

stovetop = stove.on(setting=high_heat)
pot = stovetop.put_on_pot()
pot = pot.add('water')
cooked_noodles = pot.add(ingredients['noodles'])
drained_noodles = colander.drain(cooked_noodles, hot_water)
prepped_noodles = colander.rinse(drained_noodles)
drained_artichokes = colander.drain(artichokes, liquid)
cut_artichokes = knife.chop(drained_artichokes)
stovetop = stove.on(setting=medium_heat)
pan = stovetop.put_on_pan()
pan = pan.add(ingredients['oil'])
pan = pan.add(ingredients['artichokes'])
pan = stovetop.heat(pan)
chopped_garlic = knife.chop(ingredients['garlic'])
chopped_spinach = knife.rough_chop(ingredients[‘spinach'])
chopped_basil = knife.rough_chop(ingredients['basil'])
feta = knife.square(ingredients['feta'])

The code follows a recipe pattern by having sequential steps organized into groups by objective (e.g., boil water, prep artichokes, cook artichokes, prep ingredients).

Notice that again, this code is much more explicit than a recipe and each step has more detail. In a recipe, we assume some base level of knowledge (we don’t give instruction on how to hold a knife or chop a vegetable) and we also know the reader can go to other sources for additional information if needed. In the above recipe, let’s pretend noodles means homemade pasta (just to be clear, it doesn’t and never will). Instead of cramming the pasta recipe into the same overall recipe as Love Noodles, we would reference an outside recipe for pasta making instructions.

We can take advantage of this organization scheme within our own code. We already rely on some imported libraries to simplify the instructions, but we’re still spelling out quite a lot. It’s not too difficult to understand as-is, but we can take out some of the additional explicitness and reduce the overall length of the file, which will make each step and the overall file easier to understand. Similar to the side pasta recipe, we’ll build additional modules and functions to house details, leaving the core information in the main file. That way, someone who just needs to understand the basic steps of what’s happening can easily look at the main code and have a ready understanding of each of the steps, the order they come in, and how they relate to one another. Anyone that needs or wants more detail can explore the specific functions, with the added benefit of understanding the overall context and objective of what is happening before looking at more nuanced details.

We’ll build three functions, one to detail how to cook noodles, one to prep the pan, and one to prep the other ingredients. We’ll put them in a cooking.py file.

def cook_noodles(setting: str, noodles) -> str:
"""Prepare cooked noodles"""
stovetop = stove.on(setting=setting)
pot = stovetop.put_on_pot()
pot = pot.add('water')
cooked_noodles = pot.add(noodles)
drained_noodles = colander.drain(cooked_noodles, 'water')

return colander.rinse(drained_noodles)
def prep_pan(setting: str, oil: str) -> object:
"""Prepare pan for cooking by heating and adding oil"""
stovetop = stove.on(setting=setting)
pan = stovetop.put_on_pan()

return pan.add(oil)
def chop_ingredients() -> list:
"""Prepare garlic, spinach, basil, and feta"""
chopped_garlic = knife.chop(ingredients['garlic'])
chopped_spinach = knife.rough_chop(ingredients['spinach'])
chopped_basil = knife.rough_chop(ingredients['basil'])
feta = knife.square(ingredients['feta'])
return [chopped_garlic, chopped_spinach, chopped_basil, feta]

Which would turn the original code into:

prepped_noodles = cooking.cook_noodles(high_heat, ingredients)drained_artichokes = colander.drain(artichokes, liquid)
cut_artichokes = knife.chop(artichokes)
pan = cooking.prep_pan(medium_heat, ingredients)
pan = pan.add(ingredients['artichokes'])
pan = stovetop.heat(pan)
prepped_ingredients = cooking.chop_ingredients(ingredients)

This does add lines of code to the overall project, but it

  1. shortens the main file
  2. makes the main file easier to follow along with
  3. gives the reader context before they dig into the additional code. For example, once the reader knows the objective is to cook the noodles, they can go to the function to get more detail on how it’s done.

Breaking the code up in this way also makes it easier to test and find where something is going awry if your end result is unexpected. If something goes wrong with making the noodles (which is why I’ll never make homemade pasta), I’ll know the problems are isolated to that step, versus having vague issues I need to search the code for in order to pin down.

Of course, there is a balancing act between giving too much detail and not giving enough detail. Having a single make_recipe function that encompasses all the steps isn’t very helpful or illuminating, but detailing every single step without any summarization can be very difficult to parse. That’s why I find the recipe analogy so helpful. It provides a way to think about breaking up code into discrete sections to make it simpler and more readable.

It’s also important to note that recipes give instructions for single dishes — that is, there’s not a single recipe called “Thanksgiving Dinner.” This keeps the scope of each recipe smaller and more manageable. If someone is in fact making Thanksgiving dinner, it’s easier to have multiple recipes to reference than to have a single recipe that’s twenty pages long. Similarly, it’s best to focus on one objective per code file, rather than trying to put too much into a single file. Think of your overall project as the cookbook, and each file as an individual recipe. The recipes may overlap and reference each other, but each recipe has its own objective and its meaning can stand on its own.

Tips for turning your code into a recipe:

  1. A file should have a single overall objective or topic, rather than trying to accomplish too many things and mixing too many ideas in a single file. Think of a recipe that requires a special sauce or dressing — the sauce or dressing will likely have its own recipe page that is referenced in the main recipe.
  2. A docstring at the top of the file can be helpful to provide context so the reader has some information on what to expect before diving in.
  3. Store definitions and configurations (ingredients) in a central place — at the top of a file if it is only necessary in that file, or in a configs or init file if it will be used across multiple files.
  4. Code should generally be organized in sequential steps organized into groups by goal (think of the numbered instructions in a recipe)
  5. The main file should contain high level steps and more detailed or multistep processes can be broken out into functions and other files for organization.

Additional tips for more readable code:

  1. Use informative naming schemes (and avoid single letter names). prepped_ingredients is better than output or pi and will be easier to understand throughout the file.
  2. Functions should also only tackle one objective (chop_ingredients is preferred over chop_and_heat_ingredients. )
  3. Functions can be broken up into smaller functions to help with readability and organization, similar to the overall step. Reference the chop_ingredients function above, which uses the knife.chop and knife.rough_chop functions, instead of spelling out how to chop for each vegetable.

--

--