Handling User Input
State changes need a trigger. In a real program, that trigger is often a user typing something. We connect input() to the controlled methods from Session 21.
A person types something โ and the program has to react safely.
1. Learning Objective
By the end of this session you will be able to:
- Read a line of text from the user with input()
- Explain why input() always returns a string, and why that matters for numeric fields
- Convert and validate user input before passing it into a controlled state method
- Build a small text-based menu loop that dispatches to different actions
- Handle invalid user input (non-numeric text) without crashing the program
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.
What is the type of the value returned by input("Enter population: "), regardless of what the user types?
input() always returns a string, no matter what characters the user typed. If you need a number, you must convert it yourself, typically with int() or float(), and handle the case where the conversion fails.A user types "fifty" when asked for a population. What happens if you call int(user_input) directly with no error handling?
int() raises ValueError on text that is not a valid integer literal. Per Session 11, this needs a try/except around it, or the program crashes on the very first bad keystroke.Why should raw user input be validated and converted BEFORE being passed to a method like set_population, rather than passing the raw string directly?
value < 0 raises a TypeError in Python. Converting and validating at the input boundary produces a clearer, more specific error message right where the bad data entered the program.In a text menu loop like while True: choice = input("> ")\n if choice == "quit": break, what happens if the user just presses Enter with no text?
== "quit" check like any other non-matching input, so the loop naturally continues and prompts again.What is the safest pattern for reading a required number from a user who might type invalid text?
3. The Concept โ Reading and Validating User Input
Raw text is validated and converted at the boundary, before it ever reaches a method expecting a number.
input() always returns a string
The built-in input() function pauses the program, waits for the user to type a line and press Enter, and returns exactly what they typed โ always as a string, never automatically converted.
name = input("Enter a country name: ")
print(type(name)) # <class 'str'> โ always, even if they typed "54000000"
Converting and validating before using the value
A numeric field like population needs explicit conversion, and that conversion can fail on bad input โ exactly the kind of case Session 11's try/except exists for.
raw = input("Enter population: ")
try:
population = int(raw)
except ValueError:
print(f"'{raw}' is not a valid number.")
population = None
if population is not None:
print("Parsed population:", population)
Connecting validated input to a controlled method
Once input is converted and confirmed valid, it can safely flow into the Session 21 methods, which add their own domain-specific validation (like rejecting negative numbers) on top.
def prompt_population(country):
raw = input(f"New population for {country.name}: ")
try:
value = int(raw)
except ValueError:
print(f"'{raw}' is not a valid number. No change made.")
return
try:
country.set_population(value)
print("Updated.")
except ValueError as e:
print("Rejected:", e)
A simple menu loop
Combining a loop, input(), and conditional branching gives us an interactive text menu โ a small but real interactive program.
def run_menu(country):
while True:
choice = input("(g)row population, (s)how summary, (q)uit: ").strip().lower()
if choice == "q":
print("Goodbye.")
break
elif choice == "g":
prompt_population(country)
elif choice == "s":
print(country.summary())
else:
print("Unrecognised option, try again.")
4. Lab
What you will build
A file called input_lab.py.
Step-by-step instructions
Create the file with Country including set_population and grow_population
# input_lab.py
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
def grow_population(self, amount):
if amount < 0:
raise ValueError(f"amount must be non-negative, got {amount}")
self.population += amount
Write a function that reads and validates a number from the user
def read_int(prompt):
raw = input(prompt)
try:
return int(raw)
except ValueError:
print(f"'{raw}' is not a valid number.")
return None
Write prompt_set_population using read_int and the controlled method
def prompt_set_population(country):
value = read_int(f"New population for {country.name}: ")
if value is None:
return
try:
country.set_population(value)
print("Updated:", country.summary())
except ValueError as e:
print("Rejected:", e)
Write the menu loop and run it
When you run this file directly, try typing an invalid number, then a negative number, then a valid one โ confirm all three cases behave correctly.
def run_menu(country):
while True:
choice = input("(g)row, (s)et, (v)iew, (q)uit: ").strip().lower()
if choice == "q":
print("Goodbye.")
break
elif choice == "s":
prompt_set_population(country)
elif choice == "g":
amount = read_int("Amount to grow by: ")
if amount is not None:
try:
country.grow_population(amount)
print("Updated:", country.summary())
except ValueError as e:
print("Rejected:", e)
elif choice == "v":
print(country.summary())
else:
print("Unrecognised option.")
if __name__ == "__main__":
kenya = Country(name="Kenya", region="Africa", population=54000000)
run_menu(kenya)
Confirm the __main__ guard from Session 10 is used correctly
The menu should only run when this file is executed directly, not if it were imported elsewhere.
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
input_lab.py |
Created | An interactive menu reading validated user input into controlled state methods. |
docs/sessions/session-22/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 input_lab.py docs/sessions/session-22/index.html
git commit -m "session-22: read and validate user input, connect it to controlled state methods"
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.
What is the type of the value returned by input("Enter population: "), regardless of what the user types?
input() always returns a string, no matter what characters the user typed. If you need a number, you must convert it yourself, typically with int() or float(), and handle the case where the conversion fails.A user types "fifty" when asked for a population. What happens if you call int(user_input) directly with no error handling?
int() raises ValueError on text that is not a valid integer literal. Per Session 11, this needs a try/except around it, or the program crashes on the very first bad keystroke.Why should raw user input be validated and converted BEFORE being passed to a method like set_population, rather than passing the raw string directly?
value < 0 raises a TypeError in Python. Converting and validating at the input boundary produces a clearer, more specific error message right where the bad data entered the program.In a text menu loop like while True: choice = input("> ")\n if choice == "quit": break, what happens if the user just presses Enter with no text?
== "quit" check like any other non-matching input, so the loop naturally continues and prompts again.What is the safest pattern for reading a required number from a user who might type invalid text?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Why are there two separate layers of validation here โ read_int's conversion check and set_population's value check? Could you combine them into one? Should you?
- What would happen if you skipped read_int entirely and called int(input(...)) directly inside prompt_set_population? Trace through what happens on bad input.
- How does this session's "validate at the boundary, then trust the value" pattern relate to what you learned about function contracts in Session 08 and 11?
- Where else in a real application (not just population) would you need this same read-validate-apply pattern for user input?
10. What Breaks If This Knowledge Is Missing?
- Crash on first typo: Without read_int's try/except, the very first time a user makes a typo, the entire program crashes instead of gracefully asking again โ completely unacceptable for anything meant to be used by another person.
- Tracing state changes (Session 23): The next session adds logging/debugging so you can trace exactly when and why state changed. Without a controlled input path like this session's, there would be too many uncontrolled entry points to trace meaningfully.
- Real API responses (Layer 7): Session 42's real API data needs exactly this same "validate untrusted external input before trusting it" discipline โ user keyboard input and network responses are both untrusted data from outside your program's control.
11. What We Learned
Python concept mastered: Reading user input with input(), converting and validating it safely, and connecting it to controlled state-change methods via a menu loop.
Unlocks: You have built your first genuinely interactive program โ one that reacts safely to unpredictable human input instead of only running pre-written code.
Next session: Session 23 โ Tracing State Changes. We add visibility into exactly when and why state changes, using logging and print-based tracing.