CIS 2020 · Python for Everyone · Module 6 · Study guide

Module 6 review: solving decisions, sections 3.5 to 3.9

The second half of Chapter 3 turns the if statement into a design skill. This module covers drawing a flowchart before you code one, choosing test cases that actually catch mistakes, the bool type and its operators, the string methods that let a program inspect its own input, and the validation loop that puts all of it to work keeping bad input out of a program.

Section 3.5

Problem Solving: Flowcharts

A flowchart is a picture of a decision before it becomes code. Draw the shapes first, and the if/elif/else almost writes itself.

A flowchart is a diagram that shows the steps of an algorithm and the order they run in, using a small, fixed set of shapes. It is a planning tool: you draw it before writing code, so you can catch a missing case or a backwards condition while it is still cheap to fix, not after you have already typed twenty lines of Python.

The four flowchart shapes used on this page An oval for start and end, a parallelogram for input and output, a rectangle for a process step, and a diamond for a decision. Start / End Input / Output Process Decision? Terminal Input / Output Process step Decision, two exits
Every flowchart on this page is built from just these four shapes, connected by arrows. A decision is the only shape with more than one way out.

A single decision: can this rider get on the coaster?

The simplest useful flowchart has one diamond. A theme park ride requires a minimum height; the flowchart reads the height, asks one yes-or-no question, and produces one of two messages.

Flowchart: can this rider get on the coaster Start, input the rider's height, decide whether height is at least 48 inches. Yes leads to a box saying the rider can board. No leads to a box computing how many more inches are needed. Both boxes lead to an output step and then end. Start Input: height height >= 48 ? Yes Set message = "You can ride!" No Set message = "Grow" + str(48 - height) + " more in" Output: message
One diamond, two exits. The Yes branch and the No branch each set a message, then both rejoin the main flow before the single output step.

Reading the shapes top to bottom is exactly the order the code runs in. Every arrow is one line of control flow; every diamond is one if:

height = int(input("Enter rider height in inches: "))

if height >= 48 :
    message = "You can ride!"
else :
    message = "Grow " + str(48 - height) + " more inches"

print(message)

A chain of decisions: parking garage fee

When a problem has more than two outcomes, the flowchart chains decisions vertically: the No exit of one diamond leads straight into the next diamond, exactly the shape an elif chain takes in code.

Flowchart: parking garage fee by hours parked Start, input hours parked. First decision: one hour or less pays a five dollar flat fee. If not, a second decision checks four hours or less, charging five dollars plus three dollars per hour after the first. Otherwise a twenty dollar flat rate applies. All three fees lead to the same output step, then end. Start Input: hours parked hours <= 1? Yes fee = 5 No hours <= 4? Yes fee = 5 + 3 * (hours - 1) No fee = 20 (flat rate) Output: fee End
Two diamonds stacked vertically produce three possible fees. Notice the No arrow is what carries the flow from one diamond into the next, exactly what elif does in code.
hours = float(input("Enter hours parked: "))

if hours <= 1 :
    fee = 5
elif hours <= 4 :
    fee = 5 + 3 * (hours - 1)
else :
    fee = 20

print("Fee: $" + str(fee))

Want more practice? Four more original flowcharts, including one with a nested decision and one that mirrors a full elif chain against a boolean condition, are worked out in More flowchart examples.

Section 3.6

Problem Solving: Test Cases

A flowchart tells you the logic is right on paper. A test case tells you whether the code actually matches the flowchart.

A test case is one specific input, paired with the output you expect for it, used to check that a program behaves correctly. Good test cases are not random: they specifically target the places a decision is most likely to be wrong.

Applying that to the roller coaster flowchart from Section 3.5, the boundary is 48 inches exactly:

Test cases for the height check. Two of these were picked specifically because they sit on either side of 48.
Test caseInputExpected outputWhy this input
Normal, tall enough60"You can ride!"Well above the boundary; the easy case.
Boundary, exactly 4848"You can ride!"Tests that >= was used, not >.
One below the boundary47"Grow 1 more inches"Confirms the switch happens at the right value, not one off.
Normal, too short30"Grow 18 more inches"Checks the arithmetic in the No branch, not just the message.

The boundary case is the one that actually catches bugs. A test suite made only of comfortable, normal-case inputs will pass even when >= was mistakenly typed as >. Only testing exactly 48 catches that.

Section 3.7

Boolean Variables and Operators

Every condition already produces a value. Section 3.7 gives that value a name: bool, and a small set of operators for combining them.

A bool is a data type with exactly two possible values, True and False. Every relational expression, such as age >= 18, evaluates to a bool, and that value can be stored in a variable just like an int or a str:

isAdult = age >= 18
print(isAdult)      # True or False
print(type(isAdult))  # <class 'bool'>

Combining conditions: and, or, not

Three boolean operators combine or invert existing conditions. and is true only when both sides are true. or is true when at least one side is true. not flips a single condition to its opposite.

Truth table for and, or, not.
ABA and BA or Bnot A
TrueTrueTrueTrueFalse
TrueFalseFalseTrueFalse
FalseTrueFalseTrueTrue
FalseFalseFalseFalseTrue
temperature = 75
isRaining = False

if temperature >= 65 and not isRaining :
    print("Good day for a picnic")

Never write if isValid == True :. isValid is already True or False; comparing it to True adds nothing and is considered poor style. Write if isValid : instead, and if not isValid : for the opposite, rather than if isValid == False :.

Precedence

not binds tighter than and, which in turn binds tighter than or. When a condition mixes more than one operator, parentheses make the intent unmistakable even when they are not strictly required:

if not isMember and (age < 12 or age > 65) :
    price = discountedPrice

Section 3.8

Analyzing Strings

Before a program can validate a string, it has to be able to ask that string questions. Section 3.8 is the toolbox; Section 3.9 is what it gets used for.

Comparing strings: lexicographic order

Python compares strings the same way a dictionary orders words: character by character, using each character's underlying code point. This is called lexicographic order, and it means the relational operators you already know, <, >, ==, work on strings too:

print("apple" < "banana")   # True -- 'a' comes before 'b'
print("apple" < "Apple")    # False -- lowercase letters have larger code points than uppercase

Capital letters sort before every lowercase letter. "Zebra" < "apple" is True, which surprises almost everyone the first time. It has nothing to do with alphabetical order as a person understands it; it is purely comparing the numeric code point of 'Z' against the code point of 'a'.

Testing what a string contains

A set of string testing methods each answer one yes-or-no question about a string's characters, returning a bool that can go straight into a condition:

The most common string test methods.
MethodTrue when…
.isdigit()every character is a digit 0–9
.isalpha()every character is a letter
.isupper()every letter in the string is uppercase
.islower()every letter in the string is lowercase
.isspace()every character is whitespace
code = "A17"
print(code.isdigit())   # False -- contains a letter
print(code.isalpha())   # False -- contains digits

Searching inside a string

The in operator tests whether one string appears anywhere inside another, and .find() does the same search but returns the starting index, or -1 if nothing was found. len() gives the number of characters, and indexing or slicing pulls out a specific character or piece to examine on its own:

email = "[email protected]"
print("@" in email)          # True
print(email.find("@"))       # 7 -- the index where '@' starts
print(len(email))            # 19
print(email[0])              # 's' -- the first character

Putting several of these together

Realistic string checks almost always combine more than one of these tools in a single condition, exactly the way Section 3.7's and combines two boolean values:

zipCode = input("Enter a 5-digit zip code: ")

if len(zipCode) == 5 and zipCode.isdigit() :
    print("That looks like a valid zip code.")
else :
    print("That does not look like a valid zip code.")

Neither check alone is enough. len(zipCode) == 5 would accept "AAAAA"; zipCode.isdigit() alone would accept "8". Only both together, joined with and, describe an actual 5-digit zip code.

Section 3.9

Application: Input Validation

A decision that checks input once is not enough if the input can be wrong. Input validation keeps asking until the answer is usable, using exactly the string tools from Section 3.8 to decide what counts as usable.

Input validation is the practice of checking user input against the rules a program actually needs before doing anything with it, and re-prompting when the input breaks those rules. The standard pattern is a loop that keeps running exactly as long as the input is invalid:

age = int(input("Enter your age: "))

while age < 0 :
    print("Age cannot be negative. Try again.")
    age = int(input("Enter your age: "))

print("Age accepted:", age)

Read the loop the same way you read a flowchart: ask a question, and if the answer is still "invalid," loop back to the same question instead of moving on. The loop's condition is deliberately the opposite of the condition you actually want; the loop keeps running while the input is bad, and falls through the moment it becomes good.

The zip code check from Section 3.8 turns into a validation loop the same way: keep the same combined condition, just negate it and let the loop re-ask on failure.

zipCode = input("Enter a 5-digit zip code: ")

while len(zipCode) != 5 or not zipCode.isdigit() :
    print("Please enter exactly 5 digits.")
    zipCode = input("Enter a 5-digit zip code: ")

print("Zip code accepted:", zipCode)

Want more practice? Six more validation patterns, including checking for a non-empty string, catching a bad type with try/except, restricting input to a fixed menu of choices, and combining two rules at once, are worked out in More input validation examples.

Quick check

If you remember only these

  • A flowchart uses four shapes: an oval for start/end, a parallelogram for input/output, a rectangle for a process step, and a diamond for a decision. A diamond is the only shape with two exits.
  • Chaining decisions vertically in a flowchart, where the No exit of one diamond leads into the next diamond, is exactly what an elif chain does in code.
  • A good set of test cases includes a normal case, the exact boundary value, one value just past the boundary, and an invalid or unexpected input.
  • The boundary case is the one most likely to catch a real bug, such as > written where >= was needed.
  • bool has exactly two values, True and False. Every relational expression already produces one.
  • and needs both sides true, or needs at least one side true, not flips a single value. Precedence is not, then and, then or.
  • Never write if x == True : or if x == False :. Write if x : or if not x : instead.
  • Strings compare lexicographically, character by character; .isdigit(), .isalpha(), in, .find(), len(), and slicing all let a program inspect what a string actually contains before deciding what to do with it.
  • Input validation is a loop whose condition is the opposite of what you want: it keeps looping while the input is bad, and stops the moment the input is good.