Skip to content

Week 4: branches and boolean logic

Every program you have written so far runs the same statements in the same order no matter what the user types. Change the input and the numbers change, but the path through the program does not. This week that stops being true.

A conditional statement lets a program look at a value and decide what to do next, and once a program can decide, it can be right about one input and wrong about another. That is the real change this week, and it is bigger than the syntax. if and elif and else take about ten minutes to learn. Working out which inputs would prove your program wrong is the skill, and it is the one Quiz 2 is going to ask for.

Save each program in the Week04 folder inside ~/CS149, and commit and push your work when you finish. The exercises are ordered from easiest to hardest, and each one has an assignment on Gradescope so you can check your own work as you go.

One run is no longer a test

Up to now, if a program printed the right answer once, it printed the right answer always. A program with three branches in it has three ways to be wrong, and running it once exercises exactly one of them.

So the runs each spec names below are the minimum. Before you submit, count the branches in your own code, run an input that reaches each one, and then run the inputs that sit right on a boundary — the age that is exactly 12, the two guesses exactly the same distance away. That is where these programs break, every time.

Ex4.1 Positive or Negative?

Comparing two values gives you a value back, and it is a kind you have not seen yet:

>>> 35 > 0
True
>>> 35 < 0
False
>>> type(35 > 0)
<class 'bool'>

True and False are the only two values of type bool, and a conditional statement is a way of running some statements only when a bool comes out True.

Write a program named sign.py that reads a number and reports whether it is negative, zero, or positive. Use exactly the prompt "Enter a number: ". You may assume the user enters a whole number.

Enter a number: 35
35 is positive.

Run it a second time with 0, which reports 0 is neither negative nor positive., and a third time with -7, which reports -7 is negative.

Three outcomes, not two

An if/else splits the numbers into two groups, and this exercise needs three. Zero is the interesting one: it is not negative, and testing for it needs the comparison operator that asks whether two values are equal, which is not the same operator as the one that assigns a value to a variable.

Notice also that the middle case prints 0 rather than the number the user typed, because there is only one number it can be.

Ex4.2 Off By How Much?

Two people guess a number, and you want to know who got closer. That question is harder than it looks, because a guess can be off in either direction, and "off by 12" and "off by -12" are the same amount of wrong.

Python has a built-in function for exactly this, and rather than telling you what it does, this exercise sends you to ask Python. Type this in the Shell:

>>> help(abs)
Help on built-in function abs in module builtins:

abs(x, /)
    Return the absolute value of the argument.

help() works on any function, and it is the fastest way to find out what one does without leaving Thonny. Try help(round) and help(int) while you are there; the first one has more to say than you might expect.

Write a program named closer.py that reads a secret number and two guesses, then reports how far off each guess was and which one was closer. Use exactly the prompts "Secret number: ", "First guess: ", and "Second guess: ". You may assume all three inputs are whole numbers.

Secret number: 42
First guess: 30
Second guess: 50
The first guess was off by 12 and the second by 8.
The second guess was closer.

Run it again with 42, 40, and 47, where the first guess is closer, and then with 42, 39, and 45, which reports It was a tie.

Work out both distances before you compare anything

Neither distance is ever negative, and help(abs) just told you how to arrange that. Give each distance a name of its own, print the first line from those two names, and then the whole decision comes down to comparing two variables.

Do not skip the tie. Two guesses can be equally far off in opposite directions, and if your program only has an if and an else, one of them is quietly claiming a winner that does not exist.

Ex4.3 Monogram, Revisited

In Week 3 you wrote monogram.py, which read three names and built a monogram out of their first letters. It has a problem: not everybody has a middle name. Type an empty middle name into it, and here is what happens.

>>> middle = ""
>>> middle[0]
IndexError: string index out of range

An empty string has no character at index 0, so the program crashes on a customer who has two names instead of three. That is a job for a conditional, and Python makes the test shorter than you would expect:

>>> bool("Edward")
True
>>> bool("")
False

A string is True in a boolean context when there is something in it, and False when it is empty. So if middle: is a legal and idiomatic way of asking "did they give me a middle name?", and you will see that pattern for the rest of the semester.

Write a program named monogram2.py that reads a first, middle, and last name and prints the monogram, using two letters when the middle name is left blank. Use exactly the prompts "First name? ", "Middle name? ", and "Last name? ".

First name? Alfred
Middle name? Edward
Last name? Neuman
Monogram: AEN

To leave the middle name blank, press Enter without typing anything:

First name? Alfred
Middle name?
Last name? Neuman
Monogram: AN

Run it a third time with Madison, no middle name, and Carter, which reports the monogram MC.

The order of the two tests matters

You can write this with one if that checks the middle name and builds a two-letter or three-letter monogram. You can also write it with a single expression that reaches for middle[0] only when there is a character there to reach for, using and.

That second version works because of short-circuit evaluation: in A and B, Python does not evaluate B at all once it knows A is False. Swap the two halves around, so that the indexing happens first, and the IndexError comes back. Try both orders in the Shell on an empty string and watch the difference; it is the clearest demonstration of short-circuiting you will get all semester.

Ex4.4 Steel Grade

Steel is one of the most useful materials in the world, and the grade of a piece of steel says how useful it is. Assume the grade depends on three measurements, and that each one either passes or fails:

  1. Hardness must be greater than 50.
  2. Carbon content must be less than 0.7.
  3. Tensile strength must be greater than 5600.

The grade is then assigned from how many conditions passed and which ones:

Grade Conditions met
10 all three
9 1 and 2
8 2 and 3
7 1 and 3
6 exactly one of them
5 none of them

Write a program named steel.py that reads the three measurements and reports the grade. Hardness and tensile strength are whole numbers; carbon content is a number between 0.0 and 1.0. Use exactly the prompts "Enter hardness: ", "Enter carbon content: ", and "Enter tensile strength: ".

Enter hardness: 40
Enter carbon content: 0.7
Enter tensile strength: 5089
Grade: 5

This program has six branches, so it needs six runs. Work out for yourself what each of these should report, then check: 60 0.5 6000, 60 0.5 5000, 40 0.5 6000, 60 0.8 6000, 60 0.8 5000, and the 40 0.7 5089 above.

Give each condition a name

You could write hardness > 50 out four separate times, once in each branch that needs it, and the program would work. Do not. Compare each measurement against its limit once, at the top, and store the True or False in a variable with a name that says what it means. Then every branch below reads like the table above, and the limits appear once each where you can check them.

One of the six branches needs a different logical operator from the others, because "exactly one of them" is not a question about any particular condition. Work out which operator asks "at least one," then convince yourself why putting that branch last among the six is what makes it mean "exactly one."

0.7 is not less than 0.7

The sample run enters a carbon content of exactly 0.7, and condition 2 wants less than 0.7, so it fails. Each of the three conditions has a value sitting exactly on its limit, and each one is a place a program can be wrong in a way six ordinary runs will never reveal. Try 50 for hardness and 5600 for tensile strength too.

Ex4.5 It Runs, and It Chooses Wrong

Two weeks ago you repaired a program that refused to start. Last week you repaired one that ran to completion and printed two wrong numbers on every input. This one is the version conditionals make possible: it prints a correct answer for some inputs and a wrong answer for others, and nothing about a run tells you which kind you just got.

The Shenandoah Science Museum charges $14.00 for admission. Children 12 and under and seniors 65 and over pay $8.00, and members of the museum pay half of whatever the price would otherwise be. The program asks for a visitor's age and whether they are a member, answered yes or anything else.

Save this into your Week04 folder as admission.py, exactly as it appears here.

"""HW4.5 Museum Admission.

Name: (your name here)
Date: (today's date)
"""

age = int(input("Age: "))
isMember = input("Member? ")

if age < 12 or age >= 65:
    price = 8.00
else:
    price = 14.00

if isMember == "yes" or "y":
    price = price / 2

print(f"The admission price for a {age}-year-old visitor to the Shenandoah Science Museum is ${price:.2f}")

There are four problems in it. Two are style violations, and ruff check admission.py will name both; one of them you fixed once before in payroll.py, and the other one you met in Week 3. The other two are logic, and nothing will report them, because a program that picks the wrong branch is still a perfectly valid Python program.

Start by running it for a 30-year-old who is not a member, which should cost $14.00. Then repair it, and check it against these four runs:

Age: 12
Member? no
The admission price for a 12-year-old visitor to the Shenandoah Science Museum is $8.00

A 30-year-old non-member pays $14.00, a 30-year-old member pays $7.00, and a 70-year-old non-member pays $8.00. When you are finished, ruff check admission.py should report nothing at all, and the sentence it prints must be word for word the one above.

One of the two style fixes is not a rename

The long line is over 100 characters, and shortening the sentence is not allowed, because the sentence is the specification. You solved this exact problem in Week 3 with monogram.py: when a line gets too long to read, name one of the things inside it.

A condition that is never False is not a condition

Try this in the Shell, with any string at all:

>>> answer = "no"
>>> answer == "yes"
False
>>> answer == "yes" or "y"
???

Predict the third result before you press Enter. Then work out what or is actually being handed on either side of it, remembering from Ex4.3 which strings Python treats as True, and you will have found one of the two logic problems.

This is the most common mistake beginners make with or, and it is dangerous precisely because it is not an error. Python cannot tell the difference between a condition you meant and a condition that happens to be true every time.

The other logic problem shows up for exactly one age

Once the first one is fixed, the program prices a 30-year-old, a 70-year-old, an 11-year-old, and a 13-year-old all correctly. There is exactly one age in the whole range where it still gets the wrong answer. Read the rules and the comparison operators again and find it by reading, rather than by guessing ages.

Then ask yourself how many ages you would have tried at random before stumbling onto that one. That number is the argument for testing your boundaries on purpose.

Ex4.6 Pet Age Calculator

Our pets have shorter lifespans than we do, so it is interesting to know how old they are in human years. People used to use a simple formula for this — the pet's age times seven — but veterinarians now believe it is more complicated: cats and dogs age differently from each other, and both age faster when they are young.

Age Dog, in human years Cat, in human years
0 0 0
1 12 15
2 24 24
3 and up add 4 to the previous year add 4 to the previous year

Write a program named pet_age.py that reads a pet's type and age and reports the age in human years. Use exactly the prompts "Type: " and "Age: ". The type is entered in lowercase as either dog or cat; for anything else the program reports that it does not recognize the pet.

Type: dog
Age: 10
Your dog's age in human years is 56.

Run it again for a cat aged 12, which reports 64, and for a parrot aged 18, which reports Pet type not recognized. Then run it for a dog aged 1 and a cat aged 1, which are the only two runs where the two animals disagree, and for a dog aged 0, which reports 0.

Two questions, one inside the other

There are two independent things to find out: what kind of animal it is, and how old it is. Neither one alone determines the answer, and there is no point asking the age of a parrot, so the age test goes inside the type test. That is a nested conditional, and the indentation is what says which if a branch belongs to — get it wrong and Python will either complain or, worse, quietly do something you did not mean.

Do not write out a table of ages

A dog can be nineteen, and you are not going to write nineteen branches. Everything from age 2 upward follows one rule, so it takes one arithmetic expression rather than a branch of its own; only ages 0 and 1 are special cases. Work that expression out from the table before you type anything, and check it by hand against ages 2, 3, and 10.

Extra Challenges

The six exercises above are the ones with Gradescope assignments, and they are the ones that count. These two have nothing to submit. Both are longer than anything above, and both are here because their conditions do not decompose as neatly — working out the structure before you type is most of the work.

Grade calculator. A professor does not trust Canvas to compute final grades and wants a program that does it instead. It reads a student's name, their homework, test, and lab averages as floating-point numbers, and a course id, then prints Final grade for NAME in COURSE is GRADE with two digits after the decimal point. The weights differ by course: CT201 is 30% homework, 50% test, 20% lab; CT222 is 45/40/15; CT301 is 20/25/55. Any other course id prints COURSE - no such course. instead. Check yourself against these: 100.0, 94.32, 88.0 in CT301 gives 91.98, and 85.0, 93.3, 24.6 in CT201 gives 77.07.

Sales tax. In a certain state, sales tax works like this. Everything except food and baby items is taxed at 5%. Food is taxed at 3% on the first $200 and 4% on any amount above that, so $150 of food owes $4.50 and $250 of food owes $8.00. Baby items are taxed at 3.5%. Senior citizens 65 and over get a 50% discount on the tax they owe, unless they are 90 or over, in which case they owe none at all. Read an amount, a category, and an age, and print the tax owed to the cent. This one has more boundaries in it than anything you have written: $200 exactly, age 65 exactly, age 90 exactly. Write down what each should produce before you run anything, then check that your program agrees with you.

When you're done

Commit and push your Week04 folder:

git add -A
git commit -m "Add week 4 practice programs"
git push

Then do what the warning at the top of this page asked for, program by program, because Quiz 2 will be exactly this and it is on a computer. Open each of the six, count the branches, and write next to each one an input that reaches it. Add the inputs that sit on a boundary between two branches, and run them all.

A program that passes on Gradescope has passed the two or three cases its autograder happens to check, which is not the same thing as being right. Finding the case that breaks your own program, before anyone else runs it, is the whole skill this week is teaching.