Validate Your Input
Why this matters
Everything that comes into your program from outside is untrusted until you check it. input() always returns a string, and the user can type anything: letters where you expected a number, a negative quantity, a blank line, or ten thousand characters. If your program reads files, those files may be edited to contain something malicious or unexpected.
If you use a bad input value without checking it, one of three things happens. The program crashes with a traceback. The program keeps running with bad data, and the problem shows up later somewhere confusing. Or, if the value reaches something powerful like a file path or a database query, the user can make your program do things you never intended. Validation stops all three.
The rule: Every value that enters the program is checked for type, range, and format before it is used. Values that fail the check are rejected with a clear message. The program never “repairs” bad input by guessing.
Benefits
- Errors are caught at the door, where you know exactly what went wrong and can tell the user.
- The rest of the code can assume its inputs are sane, which makes it simpler.
- Bad data never reaches your files or your calculations.
- Deliberate misuse (a path like
../../secrets.txt, a quantity of-1000) is stopped at the same place as honest mistakes.
Red flags (violations)
int(input(...))orfloat(input(...))with notry/exceptaround it.- A menu that reads a choice and uses it without checking that it is one of the options.
- A quantity, price, or age that is converted to a number but never checked for a sensible range.
- A filename from the user passed straight to
open(). - Code that “fixes” bad input silently, such as
abs(quantity)to make a negative number positive.
Example 1 - A number that is not a number
Problematic code
def ask_quantity() -> int:
return int(input("Quantity: "))
Type five and the program dies:
ValueError: invalid literal for int() with base 10: 'five'
Type -3 and the program accepts it, and now the inventory has a negative count.
Fixed with type and range checks
def ask_quantity() -> int:
"""Ask until the user enters a whole number greater than zero."""
while True:
raw = input("Quantity: ").strip()
try:
quantity = int(raw)
except ValueError:
print(f"'{raw}' is not a whole number. Try again.")
continue
if quantity <= 0:
print("Quantity must be greater than zero. Try again.")
continue
return quantity
Why this is better: The conversion is wrapped in try/except ValueError, so a bad string produces a message instead of a crash. The range check rejects zero and negatives. The loop keeps asking until the input is valid, so every caller of ask_quantity() can trust the number it gets back.
Example 2 - A menu choice
Problematic code
def main_menu():
print("1. Add item 2. Remove item 3. List items 4. Quit")
choice = input("Choose: ")
if choice == "1":
add_item()
elif choice == "2":
remove_item()
elif choice == "3":
list_items()
elif choice == "4":
save_and_quit()
Type 7 and nothing happens. Type 1 with a trailing space and nothing happens. The user has no idea why.
Fixed by checking against the valid set
MENU_CHOICES = {"1", "2", "3", "4"}
def ask_menu_choice() -> str:
"""Ask until the user enters one of the menu numbers."""
while True:
choice = input("Choose: ").strip()
if choice in MENU_CHOICES:
return choice
print(f"'{choice}' is not an option. Enter 1, 2, 3, or 4.")
def main_menu():
print("1. Add item 2. Remove item 3. List items 4. Quit")
choice = ask_menu_choice()
if choice == "1":
add_item()
elif choice == "2":
remove_item()
elif choice == "3":
list_items()
else:
save_and_quit()
Why this is better: The valid choices are written down once. The input is stripped of stray whitespace and checked against that set before the if chain runs. The if chain can now use a plain else for the last option because every other value has already been rejected.
Example 3 - A filename from the user
Problematic code
def load_inventory():
filename = input("Load which file? ")
with open(filename) as f:
return f.read()
The user can type ../../.ssh/id_rsa or /etc/passwd and your program will read it for them.
Fixed by restricting what is allowed
from pathlib import Path
DATA_DIR = Path("data")
def ask_data_filename() -> Path:
"""Ask for a filename inside the data folder. Reject paths that leave it."""
while True:
name = input("Load which file? ").strip()
if not name or "/" in name or "\\" in name or name.startswith("."):
print("Enter a plain filename with no folders, for example inventory.txt.")
continue
path = DATA_DIR / name
if not path.is_file():
print(f"There is no file named {name} in the data folder.")
continue
return path
Why this is better: The program decides where files live. The user only supplies a name, and anything that looks like a path is rejected. The check that the file exists happens here, where the program can ask again, instead of deep inside the loading code.
How to apply this rule
- Find every entry point. Search your project for
input(,open(,sys.argv, and any place a file is read. Each one is a place where untrusted data enters. - Decide what valid means. For each entry point, write down the type, the allowed range or set of values, and the format.
- Check before you use. Convert and check the value in one small function, like
ask_quantity(). Return only valid values. - Reject, do not repair. When a value fails, tell the user what was wrong and ask again. Do not guess what they meant.
- Keep the check near the input. The function that reads the value is the right place to validate it. Code further in should be able to trust what it receives.
Knowledge Check
- A program reads a price with
price = float(input("Price: ")). What happens when the user types$5?- Python strips the dollar sign and stores 5.0
- The program raises
ValueErrorand crashes unless the call is inside atryblock priceis set toNone- The program asks again automatically
Answer
2 — `float("$5")` raises `ValueError`. Without a `try`/`except`, the exception propagates up and ends the program with a traceback. - Which check is missing from this code?
age = int(input("Age: "))inside atry/except ValueErrorthat prints a message and asks again.- A type check
- A range check
- A format check for letters
- Nothing is missing
Answer
2 — the `try`/`except` handles the type, but `-40` and `9999` are both accepted as valid ages. A range check is still needed. - Why is it a bad idea to “fix” a negative quantity with
quantity = abs(quantity)?abs()is slow- The user typed something wrong, and the program is guessing what they meant instead of asking
- Negative numbers cannot be stored in Python
- It is fine; this is the recommended approach
Answer
2 — silently changing bad input hides the mistake from the user. If they meant to type 10 and typed -1, the program now records 1 and nobody knows. - A user is asked for a filename and types
../config/passwords.txt. What is the safest response?- Open it, since the user asked for it
- Strip the
../and openconfig/passwords.txt - Reject any name containing a path separator and ask again
- Open it only if it exists
Answer
3 — the program should control which folder files come from. Any input that tries to leave that folder is rejected outright. Stripping the `../` still lets the user reach a folder you did not intend.
Next up
Validation stops bad values at the door. The next principle is about what to do when something still goes wrong: handle exceptions at the lowest sensible level.