input() always hands you a string. That single fact is where most beginner input bugs
start, and this post is mostly about dealing with it — then argparse, for when the input
arrives as command-line arguments instead.
input() returns a string, always
quantity = "3" # what input("How many? ") would have returned
print(quantity + 1 if isinstance(quantity, int) else "cannot add 1 to a string")
# Output: cannot add 1 to a string
print(int(quantity) + 1) # Output: 4
Even when the user types 3, you get "3". Adding a number to it raises
TypeError; comparing it to a number raises too. Convert immediately, at the boundary, so
the rest of your program works with real numbers.
The examples in this post use fixed strings rather than live input() calls so they can
be run and verified — but every one of them is what you would write around a real prompt.
Convert inside a try
def to_amount(text):
try:
return float(text)
except ValueError:
return None
print(to_amount("15.50")) # Output: 15.5
print(to_amount("fifteen")) # Output: None
print(to_amount("")) # Output: None
Do not try to predict whether the conversion will work — "15.50".isdigit() is
False because of the dot, and writing a regex for "is this a number" means reimplementing
float() badly. Attempt the conversion and catch the failure; the function is the authority
on what it accepts.
Loop until the answer is usable
The standard shape. Written as a function so the loop has somewhere to return from:
def read_amount(inputs):
for text in inputs: # stands in for input() in this example
try:
amount = float(text)
except ValueError:
print(f"'{text}' is not a number, try again")
continue
if amount <= 0:
print("amount must be positive")
continue
return amount
return None
print(read_amount(["abc", "-5", "25.00"]))
# Output: 'abc' is not a number, try again
# Output: amount must be positive
# Output: 25.0
In real code the for is while True: and text comes from
input(). Everything else is identical: validate, complain specifically, continue,
and return only when the value is good.
Tell the user what was wrong with what they typed. "Invalid input" makes them guess; "amount must be positive" does not.
Normalise before comparing
def wants_to_continue(text):
answer = text.strip().lower()
if answer in ("y", "yes"):
return True
if answer in ("n", "no"):
return False
return None
print(wants_to_continue(" YES ")) # Output: True
print(wants_to_continue("N")) # Output: False
print(wants_to_continue("maybe")) # Output: None
.strip().lower() on anything a person typed, always. Users add spaces and use whatever
case they like, and neither should be your problem. Accepting several spellings of yes costs one tuple
and removes a whole category of complaint.
Menu choices
OPTIONS = {"1": "View accounts", "2": "Deposit", "3": "Withdraw", "4": "Sign out"}
def choose(text):
choice = text.strip()
if choice not in OPTIONS:
return f"'{choice}' is not an option"
return OPTIONS[choice]
for line in ["2", " 4 ", "9"]:
print(choose(line))
# Output: Deposit
# Output: Sign out
# Output: '9' is not an option
A dict keyed by the string the user types beats a chain of if statements: adding an
option is one line, and the same dict prints the menu. Note the keys are strings, because that is what
input() gives you — no conversion needed at all.
Passwords
import getpass
print(callable(getpass.getpass)) # Output: True
getpass.getpass("Password: ") reads a line without echoing it to the screen. Use it
instead of input() for anything secret — it is in the standard library and there is no
excuse for a password appearing in a terminal, a screen recording or someone's scrollback.
It cannot be demonstrated in a runnable block here because it needs a real terminal, which is also why it silently falls back to a warning when run under some IDEs.
argparse, when the input is arguments
For a script run from the command line, do not read sys.argv by hand:
import argparse
parser = argparse.ArgumentParser(description="Show an account statement.")
parser.add_argument("account", help="account number")
parser.add_argument("--limit", type=int, default=10, help="rows to show")
parser.add_argument("--all", action="store_true", help="ignore the limit")
args = parser.parse_args(["1001-0001", "--limit", "5"])
print(args.account) # Output: 1001-0001
print(args.limit) # Output: 5
print(args.all) # Output: False
type=int does the conversion and the error message for free.
action="store_true" makes a flag. And --help is generated from the
help= strings, so the documentation cannot drift from the arguments.
In a real script you call parser.parse_args() with no arguments and it reads
sys.argv. The list is passed explicitly above so the example runs.
What the bank app does
The console bank this track draws on wraps input() once and routes every prompt through
it, which is the pattern worth copying:
@staticmethod
def read_line(prompt: str) -> str | None:
"""Prompt, then return the trimmed answer, or None when input has run out."""
try:
return input(prompt).strip()
except EOFError:
print()
return None
One place strips, and one place decides what happens at end of input. That EOFError is the detail worth stealing: it is raised on Ctrl-D, and also when a piped script runs out of lines. Returning None distinguishes "there is no more input" from "the user pressed enter on an empty line" — and a menu loop that confuses those two spins forever.
Because every prompt goes through this one function, adding logging or reading from a file for testing is a single change.
That last point is the practical one. Code that calls input() in twenty places cannot be
tested without a real person at a keyboard; code that calls one
wrapper can be tested by passing a different wrapper.
Never trust the input
Everything above is one rule applied repeatedly: data from outside your program is untrusted
until you have checked it. That covers input(), command-line arguments, files,
environment variables and HTTP requests equally.
Convert at the boundary, validate immediately, and let the rest of the program assume the values are good. Validation scattered through the code is validation you will eventually forget to do.
Next: Modules & Packages.