In order to learn how to program, you have to get into the right mindset. Specifically, you need to think about problem solving through the lens of algorithmic thinking. Only then can you tackle Python.
Fortunately, this article tries to help you get in the right mindset by giving you an overview of algorithmic thinking. For the most part, however, we’ll just be talking about how to order a pizza. Enjoy!
Table of Contents
Algorithmic Thinking Overview
To be honest, I’m probably not the best person to be explaining algorithmic thinking. After all, this is a term that I’ve only ever heard in passing, and I don’t know anyone in the industry that uses it. That said, I think it’s a useful enough idea to talk about as we transition into development.
For the uninitiated, algorithmic thinking is this idea of coming up with steps to solve a problem. Naturally, the product of algorithmic thinking is an algorithm: a sequence of steps someone can follow to solve a problem. Examples of algorithms include cooking recipes, GPS directions, etc.
As you can probably imagine, algorithms are the foundation of computing. In order to solve a problem, we write down steps in a language that the computer can understand. For us, that language is Python. For others, that language could be one of many.
Of course, before we can begin to write code, we need to start thinking like a computer. Fortunately, the rest of this article is dedicated to doing just that. In particular, we’ll take a look at a silly example of an algorithm for ordering a pizza. Then, we’ll get meta and talk about the process of generating and testing an algorithm—at least informally.
Algorithmic Thinking Exercises
While we could talk all day about what algorithmic thinking is and why it’s a useful habit to get into, I find it a lot more interesting to look at some real world examples. Specifically, let’s see if we can construct a set of instructions for a rather mundane task like ordering a pizza. Then, we’ll see if we can poke holes in our algorithm.
Pizza Ordering Algorithm
Typically, when I teach, I like to have students perform the following exercise: write an algorithm (i.e. a set of instructions) to order a pizza. Naturally, this sort of exercise results in a lot of really interesting revelations. After all, not every student in the class has coded before, so they each bring a unique perspective.
For our purposes, I’ll provide an algorithm. If you’d like to take a moment to come up with your own first, that might be a good idea! Otherwise, we’ll start with this one:
- Lookup the phone number of the pizza place
- Dial the number into a phone
- Wait until someone picks up
- Provide employee with address and credit card
- Place order
These directions seem pretty reasonable, right? If we want a pizza, we just have to pick up a phone, place the order, and pay. Before long, a pizza will be at our door!
Of course, as we’ll see, life is never quite this streamlined. In the next section, we’ll take some time to pick this algorithm apart.
What Could Go Wrong?
When it comes to putting together an algorithm, it’s helpful to think about what could go wrong at each step. For example, in our pizza ordering algorithm, the very first thing we do is lookup the phone number for the pizza place. Surely, there’s some information missing here, right? What pizza place? What does this lookup process look like? Are we using the internet? What if the internet goes down?
Clearly, we’re being pedantic here. When someone gives you directions for something, they make a lot of assumptions about what you already know. For example, it’s assumed that “pizza place” is a placeholder for whatever pizza place you want. Likewise, the lookup process should be pretty straightforward—although, we’re assuming we can actually call the pizza place in 2020.
That said, when I run this activity in class, I like to help students tighten up their algorithms by playing Devil’s Advocate. In other words, in addition to being pedantic, I also purposefully misinterpret directions that were ambiguous—just like this dad making a sandwich:
In our example, there are lot of fun ways we can misinterpret the directions. For example, there’s nothing specifying that the phone number from step 1 is the same phone number in step 2. As a result, I’d probably enter a random number and watch students look at me in disgust.
Another fun way to break this algorithm would be to purposefully misinterpret step three: wait until someone picks up. If my wife picks up the remote, does that count? People would think I’d lost my mind if I just started reciting my address after an event like that—especially considering that an employee would probably pick up in the middle of my rambling.
All kidding aside, approaching algorithms this way is a great way of figuring out if they’re ready to be shared. After all, if I were to write a recipe, I would probably ask a few people to follow it just to see how the steps are interpreted. That said, we’ll take a look at a more structured way of assessing an algorithm in the following sections.
Accepting User Input
Unfortunately, computers don’t really have the ability to infer information; we have to tell them everything. As a result, a better algorithm would have to be a lot more specific. For example, any time we refer to “pizza place”, we should indicate that it’s provided by the person following the directions (i.e. the user). Here’s an updated algorithm with all user-supplied information marked in brackets:
- Lookup the phone number of the [pizza place]
- Dial the number into a [phone]
- Wait until someone picks up
- Provide employee with [address] and [credit card]
- Place [order]
Here, we’ve called out five explicit pieces of information that the user has to bring to the table to be able to place an order: a pizza place, their address and credit card, a phone, and their order.
One benefit of explicitly marking the information provided by the user is that we now have an algorithm that is somewhat more generic. In other words, we can give this algorithm to various people, and they’ll be able to replace the placeholders with whatever they want.
An alternative to this type of design would be to provide explicit directions in place of all the placeholders. For example, instead of talking about a “pizza place,” we could talk explicitly about Domino’s. Likewise, we’d have to specify this algorithm for a specific person (e.g. Jessie’s Pepperoni Pizza Order to Domino’s) because we need an address and a credit card.
For our purposes, we’ll continue with the more generic design.
While we know what information is being provided by the user, there’s still a problem. How do we know what information is being passed from step to step? Surely, it’s obvious to us, but it wouldn’t be so obvious to a computer.
For example, how does the computer know what number to enter in the phone in step 2? We didn’t explicitly state that it was the same number from step 1. In other words, we need to introduce variables.
To do that, we’ll follow a similar syntax for user data. However, instead of square brackets, we’ll use parentheses:
- Lookup the (phone number) of the [pizza place]
- Dial the (phone number) into a phone
- Wait until an (employee) picks up
- Provide the (employee) with [address] and [credit card]
- Place [order]
Now, we’re tracking two variables: the phone number for the pizza place and the employee at the pizza place. This is how we transfer information between each step.
If we now take a look at our algorithm, we can start to see a pattern. All actions are defined using verbs, and all data is defined using nouns. In the next section, we’ll look at taking these ideas and converting them into a simple programming language.
At this point, our algorithm hasn’t really changed that match. All we’ve done is label some key pieces of information as either user input or a variable. That said, I’d argue that that’s pretty much all we need. Now, it’s just a matter of converting what we have into an informal programming language called pseudocode.
In general, there are no real rules around pseudocode. In fact, our original algorithm could be considered pseudocode. However, I find it beneficial to try to develop pseudocode that actually looks like code. That way, it’s a lot easier to convert to software.
In our case, we’ll be using a simple function notation for each of our steps. In other words, we’ll try to convert each verb into a function where the nouns are the input and output. For example, step 1 reads: “lookup the phone number of the pizza place.” As Python-like pseudocode, this might look as follows:
phone_number = lookup(pizza_place)
Here, we use a function called `lookup()`python that takes the input of a business and returns a phone number. In this case, we store the phone number in a variable called `phone_number`python.
Now, lookup isn’t defined. In other words, we don’t know how this function is going to lookup the phone number for the pizza place, but we expect it to do its job. With this structure, we can plug in any lookup function that fits our requirements—even one that someone else writes for us.
At any rate, it’s not too hard to convert our entire algorithm to pseudocode:
phone_number = lookup(pizza_place) dial(phone, phone_number) employee = wait(phone) give(employee, credit_card, address) place(employee, order)
With our algorithm in code, we can sort of see why certain aspects of our algorithm don’t need to be defined. For instance, the `dial()`python function abstracts the idea of dialing a number into a phone. In other words, we assume that it will work as expected.
That said, even with some of the steps abstracted, there are definitely issues with this algorithm. For instance, we never hang up the phone, so we might want to add a `hang_up()`python function:
phone_number = lookup(pizza_place) dial(phone, phone_number) employee = wait(phone) give(employee, credit_card, address) place(employee, order) hang_up(phone)
Also, there’s a lot of messiness associated with making a call. For example, there’s a bit of conversational back and forth, so we might combine steps 4 and 5 to simplify our algorithm:
phone_number = lookup(pizza_place) dial(phone, phone_number) employee = wait(phone) place(employee, order, credit_card, address) hang_up(phone)
In the next section, we’ll take a look at why we might choose to abstract some of these ideas to simplify our algorithm.
As I alluded to a bit earlier, it can sometimes be helpful to purposefully leave out the details of a step. I know previously I made a joke about misinterpreting steps, but programming is complicated. If we focus so hard on the details, we’ll never actually get anything done. In other words, it helps to look at the big picture: what is our goal with this algorithm, and what are the major steps in accomplishing that goal?
Also, while we’re hiding a bit behind abstraction with our current algorithm, that doesn’t stop us from defining any of the underlying functions. For example, we might decided to further explain step 1:
- Lookup the (phone number) of the [pizza place] on [computer]
- Turn on [computer]
- Open (browser) on [computer]
- Search for [pizza place] (URL) in (browser)
- Click (URL) of [pizza place] to open (website)
- Find (phone number) on (website)
This can then be turned into it’s own Python-like pseudocode:
def lookup(pizza_place, computer): power(computer) browser = browse(computer) url = search(browser, pizza_place) website = click(url) return find_phone_number(website)
Naturally, this process is iterative. In other words, we can outline the high level pieces of the algorithm—like looking up the phone number and placing the order—and further define those steps as needed until we reach the right level of detail.
To me, this makes a lot more sense than diving right down to the lowest level of detail. For instance, we don’t need to explain the entire process of purchasing a phone if the user already has one.
On a side note: it might be fun to create a series of articles this way. Define a high-level guide to doing a mundane task then go way down the rabbit hole writing how-to guides for every little detail.
Being able to organize ideas through abstraction is a key part of algorithmic thinking. If we weren’t able to abstract ideas, life would be a lot more difficult. Imagine trying to throw a ball when all you can think about is the number of degrees of rotation your shoulders and hips need to make reach a target. Yeah, that’s not ideal.
At this point, we’ve covered just about everything I think is worth talking about in terms of this example. In the next, section we’ll talk about how algorithmic thinking relates to code development—specifically in Python.
Converting Algorithms to Python Code
While I’m sure there’s a way to write a program to order a pizza—in fact, I know Jarvis made a video about that exact subject—it’s not exactly trivial to convert our algorithm to code:
Fortunately, there are tons of problems that are more suited for computing. For example, there are a lot of traditional sort of problems that programming was meant to solve like computation. Thanks to the convenience of a calculator, we can quickly tabulate sales and calculate sales tax.
Today, computing has gotten so abstract that we can actually solve some more interesting problems. For instance, what sort of problems do tools like Facebook, Twitter, and TikTok solve? Now, imagine writing up an a set of algorithms for those tools.
Now that we’ve had a chance to talk about algorithmic thinking, I think we can start getting into Python code. First, however, we’ll take a brief tour through a few Python concepts. Specifically, I want to talk about the interpreter, and what it allows us to do. Then, we’ll broadly talk about different types of data. Eventually, of course, we’ll get to the code!
In the meantime, if you want to support this series, you can head over to my list of ways to grow the site. There, you’ll find fun links to things like my newsletter, Patreon, and YouTube channel.
Alternatively, you can check out some of these related articles:
Likewise, here are some resources from the folks at Amazon (ad):
- Effective Python: 90 Specific Ways to Write Better Python
- Python Tricks: A Buffet of Awesome Python Features
- Python Programming: An Introduction to Computer Science
Of course, you can always keep reading (assuming the next part of the series is out). Otherwise, take care! Thanks for stopping by.
Magic numbers are numerical constants that have no clear meaning in the code and therefore make code harder to read. Anything that makes code harder to read is something we can use to obfuscate our...
Type hinting is a nice tool that dynamic typing languages employ to make code more readable. As you can probably imagine, readability is not the goal with obfuscating code, so we ought to get rid of...