from itertools import chain, repeat prompts = chain(["Enter a number: "], repeat("Not a number! Try again: ")) replies = map(input, prompts) valid_response = next(filter(str.isdigit, replies)) print(valid_response)
Enter a number: a Not a number! Try again: b Not a number! Try again: 1 1
or if you want the "incorrect input" message to be separated from the input request, as in other answers:
prompt_msg = "Enter a number: " bad_input_msg = "Sorry, I didn't understand that." prompts = chain([prompt_msg], repeat('\n'.join([bad_input_msg, prompt_msg]))) replies = map(input, prompts) valid_response = next(filter(str.isdigit, replies)) print(valid_response)
Enter a number: a Sorry, I didn't understand that. Enter a number: b Sorry, I didn't understand that. Enter a number: 1 1
How it works?
prompts = chain(["Enter a number: "], repeat("Not a number! Try again: "))
This combination of itertools.chain and itertools.repeat will create an iterator that itertools.repeat lines "Enter a number: " once, and "Not a number! Try again: " infinite number of times: for prompt in prompts: print(prompt)
Enter a number: Not a number! Try again: Not a number! Try again: Not a number! Try again: # ... and so on
replies = map(input, prompts) - here map will apply all prompts lines from the previous step to the input function. For example.: for reply in replies: print(reply)
Enter a number: a a Not a number! Try again: 1 1 Not a number! Try again: it does not care now it does not care now # and so on...
- We use
filter and str.isdigit to filter out strings containing only numbers: only_digits = filter(str.isdigit, replies) for reply in only_digits: print(reply)
Enter a number: a Not a number! Try again: 1 1 Not a number! Try again: 2 2 Not a number! Try again: b Not a number! Try again: # and so on...
And to get only the first line, consisting only of numbers, we use next .
Other validation rules:
String methods: Of course, you can use other string methods such as str.isalpha to get only alphabetic strings, or str.isupper to get only uppercase letters. See the docs for a complete list.
Membership Testing:
There are several different ways to do this. One of them is using the __contains__ method:
from itertools import chain, repeat fruits = {'apple', 'orange', 'peach'} prompts = chain(["Enter a fruit: "], repeat("I don't know this one! Try again: ")) replies = map(input, prompts) valid_response = next(filter(fruits.__contains__, replies)) print(valid_response)
Enter a fruit: 1 I don't know this one! Try again: foo I don't know this one! Try again: apple apple
Comparison of numbers:
There are useful comparison methods that we can use here. For example, for __lt__ ( < ):
from itertools import chain, repeat prompts = chain(["Enter a positive number:"], repeat("I need a positive number! Try again:")) replies = map(input, prompts) numeric_strings = filter(str.isnumeric, replies) numbers = map(float, numeric_strings) is_positive = (0.).__lt__ valid_response = next(filter(is_positive, numbers)) print(valid_response)
Enter a positive number: a I need a positive number! Try again: -5 I need a positive number! Try again: 0 I need a positive number! Try again: 5 5.0
Or, if you don't like the use of more complex methods (dunder = double underscore), you can always define your own functions or use functions from the operator module.
Way of Existence:
Here you can use the pathlib library and its Path.exists method:
from itertools import chain, repeat from pathlib import Path prompts = chain(["Enter a path: "], repeat("This path does not exist! Try again: ")) replies = map(input, prompts) paths = map(Path, replies) valid_response = next(filter(Path.exists, paths)) print(valid_response)
Enter a path: abc This path does not exist! Try again: 1 This path does not exist! Try again: existing_file.txt existing_file.txt
Attempt limit:
If you do not want to torture the user by asking him something an infinite number of times, you can specify a restriction in calling itertools.repeat . This can be combined with providing a default value for the next function:
from itertools import chain, repeat prompts = chain(["Enter a number:"], repeat("Not a number! Try again:", 2)) replies = map(input, prompts) valid_response = next(filter(str.isdigit, replies), None) print("You've failed miserably!" if valid_response is None else 'Well done!')
Enter a number: a Not a number! Try again: b Not a number! Try again: c You've failed miserably!
Input preprocessing:
Sometimes we donโt want to reject input if the user accidentally provided it with IN CAPS or with a space at the beginning or end of the line. To take these simple errors into account, we can pre-process the input using the str.lower and str.strip . For example, in the case of membership testing, the code would look like this:
from itertools import chain, repeat fruits = {'apple', 'orange', 'peach'} prompts = chain(["Enter a fruit: "], repeat("I don't know this one! Try again: ")) replies = map(input, prompts) lowercased_replies = map(str.lower, replies) stripped_replies = map(str.strip, lowercased_replies) valid_response = next(filter(fruits.__contains__, stripped_replies)) print(valid_response)
Enter a fruit: duck I don't know this one! Try again: Orange orange
In the case where you have many functions that you can use for preprocessing, it may be easier to use a function that composes the functions . For example, using here :
from itertools import chain, repeat from lz.functional import compose fruits = {'apple', 'orange', 'peach'} prompts = chain(["Enter a fruit: "], repeat("I don't know this one! Try again: ")) replies = map(input, prompts) process = compose(str.strip, str.lower)
Enter a fruit: potato I don't know this one! Try again: PEACH peach
Combining validation rules:
For example, for the simple case when the program asks for an age from 1 to 120 years, you can simply add another filter :
from itertools import chain, repeat prompt_msg = "Enter your age (1-120): " bad_input_msg = "Wrong input." prompts = chain([prompt_msg], repeat('\n'.join([bad_input_msg, prompt_msg]))) replies = map(input, prompts) numeric_replies = filter(str.isdigit, replies) ages = map(int, numeric_replies) positive_ages = filter((0).__lt__, ages) not_too_big_ages = filter((120).__ge__, positive_ages) valid_response = next(not_too_big_ages) print(valid_response)
But in the case where there are many rules, it is better to implement a function that performs a logical connection . In the following example, I will use the finished one from here :
from functools import partial from itertools import chain, repeat from lz.logical import conjoin def is_one_letter(string: str) -> bool: return len(string) == 1 rules = [str.isalpha, str.isupper, is_one_letter, 'C'.__le__, 'P'.__ge__] prompt_msg = "Enter a letter (CP): " bad_input_msg = "Wrong input." prompts = chain([prompt_msg], repeat('\n'.join([bad_input_msg, prompt_msg]))) replies = map(input, prompts) valid_response = next(filter(conjoin(*rules), replies)) print(valid_response)
Enter a letter (CP): 5 Wrong input. Enter a letter (CP): f Wrong input. Enter a letter (CP): CDE Wrong input. Enter a letter (CP): Q Wrong input. Enter a letter (CP): N N
Unfortunately, if someone needs a custom message for each unsuccessful case, then, I'm afraid there is no sufficiently functional way. Or at least I could not find.