Layer 3 Session 24 State & Interactivity

Validating Input

Session 22 handled the happy path and basic type errors. Real input has more edge cases: empty strings, whitespace, out-of-range values, and wrong types entirely.

? True False

Raw input, checked thoroughly before it is trusted.

Estimated time: 35โ€“40 minutes  ยท  Pre-quiz โ†’ Concept โ†’ Lab โ†’ Commit โ†’ Post-quiz

1. Learning Objective

By the end of this session you will be able to:

  • Strip and normalize text input before validating it
  • Validate a string is non-empty after stripping whitespace
  • Validate a number falls within an acceptable range, not just that it parses
  • Write a single reusable validation function used by multiple input paths
  • Explain the difference between a validation error a user caused and a programming bug

2. Pre-Coding Quiz

Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ€” that is the point. You need 4/5 to proceed to the lab.

Question 1 of 5

A user types " " (only spaces) when asked for a country name. Why does a plain if not name: check fail to catch this?

A string of spaces is not empty โ€” it has length 3. It passes a naive truthiness check. You must call .strip() first to remove leading/trailing whitespace, THEN check if what remains is empty.
Question 2 of 5

Why is checking "did int() succeed" not sufficient validation for a population value?

Parsing (can this text become a number at all?) and validation (is this number acceptable for our purposes?) are separate concerns. "-5" parses successfully as an integer but is not a valid population โ€” both checks are needed.
Question 3 of 5

Why write one shared validate_population(value) function instead of repeating the same range check in prompt_set_population, a data-import function, AND a test?

This is the same principle from Session 10 (one place to change) applied to validation logic specifically โ€” a single source of truth for what "valid" means prevents different code paths from silently drifting apart on the rule.
Question 4 of 5

A user types "abc" for population. Is this a bug in your program, or an expected condition to handle gracefully?

Anything a user might reasonably (or accidentally) type is expected input to handle gracefully โ€” that is the whole point of Sessions 22โ€“24. A bug is when the PROGRAM itself does something it was never supposed to do, like a typo'd variable name.
Question 5 of 5

Which is the correct order of operations when validating a raw string meant to become a population number?

You must have clean text before attempting to parse it (stripping first avoids surprises), successfully parse it into a number before you can meaningfully compare it (Session 11's try/except), and only then check whether that number is in an acceptable range.

3. The Concept โ€” Thorough Input Validation

PARSEcan it becomea number?RANGE CHECKis it acceptable?

Two distinct checks, in order: can this become a number at all, and then, is that number acceptable?

Whitespace-only input is not empty

A naive if not text: check misses whitespace-only strings, since they have nonzero length and are therefore truthy. Always .strip() before checking emptiness.

name = "   "
print(bool(name))          # True! Not empty by length
print(bool(name.strip()))  # False โ€” after stripping, it really is empty

def validate_name(raw):
    cleaned = raw.strip()
    if not cleaned:
        raise ValueError("name cannot be empty or whitespace-only")
    return cleaned

Parsing success is not the same as business validity

A number can parse just fine and still be unacceptable for your specific rules. Both checks are needed, and they check different things.

def validate_population(raw):
    raw = raw.strip()
    try:
        value = int(raw)
    except ValueError:
        raise ValueError(f"'{raw}' is not a valid whole number")

    if value < 0:
        raise ValueError(f"population cannot be negative, got {value}")
    if value > 2_000_000_000:
        raise ValueError(f"population {value} exceeds a plausible maximum")

    return value

One shared validation function, used everywhere

Rather than repeating a range check inline in the menu, in a data importer, and in a test, define it once and call it from every path that needs it โ€” connecting back to Session 10's module discipline.

# validators.py
def validate_population(raw):
    raw = raw.strip()
    try:
        value = int(raw)
    except ValueError:
        raise ValueError(f"'{raw}' is not a valid whole number")
    if value < 0:
        raise ValueError(f"population cannot be negative, got {value}")
    return value

# input_lab.py (Session 22) can now import and reuse this directly
from validators import validate_population

def prompt_set_population(country):
    raw = input(f"New population for {country.name}: ")
    try:
        value = validate_population(raw)
        country.set_population(value)
    except ValueError as e:
        print("Rejected:", e)

User error vs a program bug

A user typing bad input is expected and must be handled gracefully with a clear message โ€” this is not a bug. A genuine bug is the program itself doing something it should never do, like a typo'd variable name raising NameError. Session 11's "catch narrowly" advice is exactly why we only catch the specific exceptions we expect from user input.


4. Lab

Lab objective: Build a shared validators module with thorough name and population validation, and wire it into the Session 22 menu.

What you will build

Two files: validators.py and an updated menu.py.

Step-by-step instructions

1

Create validators.py with validate_name and validate_population

# validators.py
def validate_name(raw):
    cleaned = raw.strip()
    if not cleaned:
        raise ValueError("name cannot be empty or whitespace-only")
    return cleaned

def validate_population(raw):
    raw = raw.strip()
    try:
        value = int(raw)
    except ValueError:
        raise ValueError(f"'{raw}' is not a valid whole number")
    if value < 0:
        raise ValueError(f"population cannot be negative, got {value}")
    if value > 2_000_000_000:
        raise ValueError(f"population {value} exceeds a plausible maximum")
    return value
2

Test both validators directly with edge cases

Try an empty string, a whitespace-only string, "abc", "-5", and a valid number for population; and an empty/whitespace/valid string for name.

test_cases = ["", "   ", "abc", "-5", "54000000"]
for case in test_cases:
    try:
        print(f"'{case}' ->", validate_population(case))
    except ValueError as e:
        print(f"'{case}' -> rejected:", e)
3

Create Country and a menu.py that imports and uses the validators

# menu.py
from validators import validate_name, validate_population

class Country:
    def __init__(self, name, region, population):
        self.name = name
        self.region = region
        self.population = population

    def summary(self):
        return f"{self.name} ({self.region}): pop. {self.population:,}"

    def set_population(self, value):
        if value < 0:
            raise ValueError(f"population must be non-negative, got {value}")
        self.population = value
4

Wire prompt_set_population to use validate_population before calling set_population

def prompt_set_population(country):
    raw = input(f"New population for {country.name}: ")
    try:
        value = validate_population(raw)
    except ValueError as e:
        print("Invalid input:", e)
        return
    country.set_population(value)
    print("Updated:", country.summary())
5

Run the menu interactively and confirm all edge cases are handled gracefully

Try empty input, whitespace, non-numeric text, a negative number, and an implausibly huge number.

if __name__ == "__main__":
    kenya = Country(name="Kenya", region="Africa", population=54000000)
    while True:
        choice = input("(s)et population, (v)iew, (q)uit: ").strip().lower()
        if choice == "q":
            break
        elif choice == "s":
            prompt_set_population(kenya)
        elif choice == "v":
            print(kenya.summary())

5. Expected Files Changed

FileActionWhy
validators.py Created A shared, reusable validation module for name and population input.
menu.py Created Wires the interactive menu to use the shared validators before touching state.
docs/sessions/session-24/index.html Created This session document.
If you find yourself editing any other file, stop. This session touches exactly 3 files.

6. Commit Checkpoint

Once the lab is complete and you can explain every line, make this exact commit:

git add validators.py menu.py docs/sessions/session-24/index.html
git commit -m "session-24: add thorough, reusable input validation for name and population"
Do not commit until you can answer out loud: "Why does validate_population need both a try/except around int() AND a separate range check afterward?"

7. Code Review Checklist

Go through your code line by line and check each item:


8. Post-Coding Quiz

Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.

Question 1 of 5

A user types " " (only spaces) when asked for a country name. Why does a plain if not name: check fail to catch this?

A string of spaces is not empty โ€” it has length 3. It passes a naive truthiness check. You must call .strip() first to remove leading/trailing whitespace, THEN check if what remains is empty.
Question 2 of 5

Why is checking "did int() succeed" not sufficient validation for a population value?

Parsing (can this text become a number at all?) and validation (is this number acceptable for our purposes?) are separate concerns. "-5" parses successfully as an integer but is not a valid population โ€” both checks are needed.
Question 3 of 5

Why write one shared validate_population(value) function instead of repeating the same range check in prompt_set_population, a data-import function, AND a test?

This is the same principle from Session 10 (one place to change) applied to validation logic specifically โ€” a single source of truth for what "valid" means prevents different code paths from silently drifting apart on the rule.
Question 4 of 5

A user types "abc" for population. Is this a bug in your program, or an expected condition to handle gracefully?

Anything a user might reasonably (or accidentally) type is expected input to handle gracefully โ€” that is the whole point of Sessions 22โ€“24. A bug is when the PROGRAM itself does something it was never supposed to do, like a typo'd variable name.
Question 5 of 5

Which is the correct order of operations when validating a raw string meant to become a population number?

You must have clean text before attempting to parse it (stripping first avoids surprises), successfully parse it into a number before you can meaningfully compare it (Session 11's try/except), and only then check whether that number is in an acceptable range.

9. Reflection Questions

Think through these after the post-quiz. No right answer โ€” they are for discussion.

  1. Why does validate_population strip the raw string before attempting int() on it? What edge case does that prevent?
  2. Session 21's set_population also checks value < 0. Is that check now redundant given validate_population already checks it? Should you remove one? Why or why not?
  3. What is an example of a validation rule that belongs in validate_population (business rule) versus one that belongs in set_population (object invariant)? Are they always the same thing?
  4. How does having validators.py as a separate, focused module connect back to the reasoning from Session 10 about splitting code?

10. What Breaks If This Knowledge Is Missing?

  • Whitespace bugs slipping through: Without stripping first, a user pasting " Kenya " with trailing spaces would create a country whose name never quite matches "Kenya" in comparisons or lookups โ€” a maddening, hard-to-spot bug in real data entry.
  • Duplicated, drifting validation rules: Without a shared validators module, the range check might get updated in the menu but forgotten in a future data-import path (Layer 4), silently allowing invalid data in through the back door.
  • Data contracts (Layer 4): Session 30 formalizes exactly this kind of validation using type hints and dataclasses. This session is the hand-rolled foundation for understanding why that formalization is valuable, not just theoretical.

11. What We Learned

Python concept mastered: Thorough input validation โ€” stripping whitespace, separating parse errors from range errors, and centralizing validation rules in one reusable module.

Unlocks: User input can no longer sneak invalid data past the program through whitespace tricks or out-of-range numbers that merely happen to parse.

Next session: Session 25 โ€” Passing State Between Functions. We look at how state and validated values move between functions and objects โ€” including when a value should be shared versus recomputed independently.