Multi-purpose function for simple user input

I understand that all bases can’t be covered (or maybe they can), but if one wanted a function to return some simple user input, such as a ‘y’ or ‘n’ answer or a integer value for a menu option or even a quantity value, would this be done with a class object?

If so, how would said class be constructed?

As an example, I have a function that returns either ‘y’ or ‘n’, to which can be passed a string object that’s the question to be answered:

def get_ask(q):
    a = ''
    while not a:
        a = input(q)
        if a != 'y' and a != 'n':
            print("Please enter y or n")
            a = ''
        else:
            return a

So, one would code ask = get_ask("Do you want to continue (y/n)? ") or ask = get_ask("Are you sure you want to delete that (y/n)? ") or any question to which a simple yes or no is required; ask then holds the return value.

Could that be extended to instead return either a integer value or a ‘y’ / ‘n’, depending on what is passed to the function? I’m thinking that it could also include error checking so that if a integer value is being requested, but the user enters a string character, (or vice versa) this would be caught, in much the same way as a try: / except: does.

I don’t know too much about class objects or even if this is a use case for such.

Any advice, please?

edit to add: I think I can do this with a function within a function, but I’m unsure if this is the only way to achieve the objective.

Yes, it can be done without complications. You do not need to use classes for that. …but the question is if you should do it this way. Sometimes it is preferred that a function returns a single type but we have many generic functions (working with multiple types) anyway.

Here is a simple example based on yours:

def get_answer(question, answer_converter, valid_answers):
    while True:
        answer = answer_converter(input(question))
        if answer in valid_answers:
            return answer
        print(f"Please enter one of: {valid_answers}")

Can you call the function to ask for an integer in the range 0-9?

Can you call the function to act as your get_ask() function?

The function is missing some of the value validation handling. Do you see what is missing?

Thank you.

The range of the number input would (in my use case) be checked for sanity by the calling routine, as in:

==========Menu==========
1: Edit order
2: Add items
3: Cancel order
4: Continue to checkout

Option: 

… the option is then checked for a range between 1 and 4, again calling get_ask() if the option is out of range. The same get_ask() is used for the item quantity when an order is being constructed.

I have this:

def get_ask(question, nature):
    answer = ''
    if nature == 'q':
        # return y or n
        while not answer:
            answer = input(question)
            if answer != 'y' and answer != 'n':
                print("Please enter y or n")
                answer = ''
            else:
                return answer
    elif nature == 'n':
        error = 0
        # return a number
        while not answer:
            answer = input(question)
            try:
                answer = int(answer)
            except:
                error = 1
            if error:
                answer = ''
                error = 0
            else:
                return answer

… which could work (I think; I’ve yet to test it, as I’ve only just put it together), but it seems a little clumsy to me and could do with some refining.

So, nature refers to the nature of the question; ‘n’ for a number and ‘q’ for a ‘y’ / ‘n’ question…

ask = get_ask("Option: ", 'n')

# or

ask = get_ask("Do you want to continue (y/n)? ", 'q')

Whenever somebody asks whether they should write a class, I always give
the same answer:

Stop writing classes!

Write more classes!

Hope that is clear now.

wink

Comment for this part of your code:

            try:
                answer = int(answer)
            except:
                error = 1

Never catch all the possible exceptions as you do here. Catch only the exceptions you expect and you want to specifically take care of. Otherwise you are hiding possible errors. Which exception you need to catch here?


Back to my example function get_answer()[1] and the hints how to use it:

In your last get_ask() function you call int() as a converter. It can be one of arguments for get_answer(). Another hint for this argument: Have you ever used the sorted() function with a custom sorting key argument? Look how this argument for sorted() is being given.

You are using these values in your code: 'y', 'n' they can make another argument for get_answer().

Now can you do the three exercises I have written below the function?


  1. Which can do all your new function should do with just a little tweak. ↩︎

Again, my thanks to you.

I want to catch any input that is not a integer. I could do a type conversion on the input: answer = int(input(question)), but the input() would crash for any number of inputs that do not conform, which means simply moving the try: / except: routine to handle that:

try:
    answer = int(input(question))
except:

… which (so far as I can see) only simplifies the code to some degree, but nothing more. But having done that, it becomes a higher maintenance overhead, if one in fact wants to check on exactly what was entered at the input, which is why I avoid altering in any way, what a user enters; better (IMHO) to handle what was entered after the event and take the appropriate action, rather than to fudge the input.

I will study the routine that you’ve posted, over the next day or so; I’ve other, less interesting, but more important things that I need to do right now.

Many thanks and I’ll be back.

The hint was that instead of bare except: you should use except TheInterestingExceptionClass: otherwise you are catching all the exceptions.

The exception class name is in the error message: python3 -c "int('a')"

Hi Rob,

Even though this snippet seems harmless now:


try:

    answer = int(input(question))

except:

    ...

it is a bad habit to get into. Unless you really know what you are doing, a bare except without an exception class following it is rightfully known as the most diabolical Python anti-pattern.

Quote:

“We were exhausted, yet jubilant. After two other engineers had tried for three days each to fix a mysterious Unicode bug before giving up in vain, I finally isolated the cause after a mere day’s work. And ten minutes later, we had a candidate fix. The tragedy is that we could have skipped the seven days and gone straight to the ten minutes.”

Your example may not be quite so diabolical, but this is still a bad habit to get into.

The two “Best Practices” rules that apply to 99% of try...except blocks are:

  1. Put as little code as possible inside the try block.

  2. Only catch explicitly-named exceptions which you know how to handle.

Like all rules, there are exceptions (pun intended!) but as a beginner you should take these as Best Practice.

In your case, the risk is that accidental typos or bugs in the try block could be hidden:


try:

    answer = imt(input(question))  # Oops!

except:

    ...

You will never see the error from calling imt by mistake, because it will be caught and handled by the except clause.

Better is to ask yourself, what possible exceptions can int(some_string) give? There is only one such exception. So you should catch that:


try:

    answer = imt(input(question))  # Oops!

except ValueError:

    ...

2 Likes

Hi Rob. This pattern of using a flag (your nature variable) to choose between two different behaviors is generally considered bad. What you have is essentially two different functions, get_ask_yn and get_ask_number.

This doesn’t matter when you are just trying out code to see what works, but if you want to add tests or collaborate with other people, you are going to run into issues.

Testing becomes more difficult because you need to essentially repeat the if-else clause in the tests, and collaboration becomes more difficult because the code becomes opaque. Consider this example:

# some amount of code that defines a `nature` variable

answer = get_ask("Your answer is: ", nature)

# some more code

It is hard to tell from a glance if you are using the yes/no or number functionality, whereas using

answer = get_ask_yn("Your answer is:")

makes the code easier to reason about.

2 Likes

My thanks to each and every one of you, for your help and advice.

In reading all that’s been said, I’ve come to the conclusion that this was a bad idea from the get-go. I think that I’m being a little lazy and forgetting something that I learned some time ago, which is the UNIX philosophy: “Do one thing, and do it well.

It’s good to be reminded of that from time to time and is all to easy to forget; I think a post-it note or a pop-up window is needed, so that I never forget that principle.

Cheers guys.

1 Like