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.
Raw input, checked thoroughly before it is trusted.
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.
A user types " " (only spaces) when asked for a country name. Why does a plain if not name: check fail to catch this?
.strip() first to remove leading/trailing whitespace, THEN check if what remains is empty.Why is checking "did int() succeed" not sufficient validation for a population value?
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?
A user types "abc" for population. Is this a bug in your program, or an expected condition to handle gracefully?
Which is the correct order of operations when validating a raw string meant to become a population number?
3. The Concept โ Thorough Input Validation
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
What you will build
Two files: validators.py and an updated menu.py.
Step-by-step instructions
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
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)
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
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())
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
| File | Action | Why |
|---|---|---|
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. |
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"
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.
A user types " " (only spaces) when asked for a country name. Why does a plain if not name: check fail to catch this?
.strip() first to remove leading/trailing whitespace, THEN check if what remains is empty.Why is checking "did int() succeed" not sufficient validation for a population value?
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?
A user types "abc" for population. Is this a bug in your program, or an expected condition to handle gracefully?
Which is the correct order of operations when validating a raw string meant to become a population number?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Why does validate_population strip the raw string before attempting int() on it? What edge case does that prevent?
- 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?
- 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?
- 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.