1
\$\begingroup\$

Let's say I want to write a super simple Python program to ask a user for their personal information and list it back out in a "pretty" format.

def get_info():
    first = input("enter your first name: ")
    while not first.isalpha():
        first = input("enter your first name: ")

    last = input("enter your last name: ")
    while not last.isalpha():
        last = input("enter your last name: ")

    age = input("enter your age: ")
    while not age.isnumeric():
        age = input("enter your age: ")
    age = int(age)

    has_pet = input("do you have a pet?: ").lower()
    while has_pet not in ["yes", "no", "y", "n"]:
        has_pet = input("do you have a pet?: ").lower()

    if "y" in has_pet:
        has_pet = True
    else:
        has_pet = False
    
    return [first, last, age, has_pet]

def print_info(info):
    first, last, age, has_pet = info
    print(f"Name: {first} {last}")
    print(f"Age: {age}")
    print(f"Has pet: {has_pet}")

print_info(get_info())

Clearly, I have violated DRY multiple times.
(1) Should I just throw the input() functions away and use arguments (with sys.argv)?
(2) The reason I can't use recursion is because I would have to ask the user for all of their information again if they mess up on, like, answering the pet question for example. Is there another way to clean up this code without sacrificing speed / resources?

\$\endgroup\$
0

3 Answers 3

1
\$\begingroup\$

Yes, abandon input(), which is usually a less powerful and flexible choice. Here's an approximation of your code using argparse. Notice several things: (1) very little repetition in our code; (2) much less algorithmic code, which means fewer chances for error; (3) greater simplicity overall and thus higher readability; (4) good validation and user help messages out of the box; (5) many other possibilities (see the library's documentation); (6) less hassle for users to run the code repeatedly or in any kind of automated context; and (7) less hassle during development of the code, for the same reason.

# Usage examples:

$ python demo.py George Washington 289 --pet
Namespace(age=289, first_name='George', last_name='Washington', pet=True)

# The code:

import argparse
import sys

def main(args):
    ap, opts = parse_args(args)
    print(opts)

def parse_args(args):
    ap = argparse.ArgumentParser()
    ap.add_argument('first_name', type = personal_name)
    ap.add_argument('last_name', type = personal_name)
    ap.add_argument('age', type = int)
    ap.add_argument('--pet', action = 'store_true')
    opts = ap.parse_args(args)
    return (ap, opts)

def personal_name(x):
    if x.isalpha():
        return x
    else:
        raise ValueError(f'Invalid name: {x}')

if __name__ == '__main__':
    main(sys.argv[1:])

But if you cannot or do not want to take that route, the roll-your-own approach looks similar in spirit: we need a general-purpose function to get input; it needs to take some type of text or label to tell the user what we are asking for; and it needs a converter/validator to check the reply. Here's a rough sketch of the new or different parts:

def main(args):
    d = dict(
        first = get_input(label = 'first name', convert = personal_name),
        last = get_input(label = 'last name', convert = personal_name),
        age = get_input(label = 'age', convert = int),
        pet = get_input(label = 'pet status [y/n]', convert = yesno),
    )
    print(d)

def yesno(x):
    if x in ('yes', 'y'):
        return True
    elif x in ('no', 'n'):
        return False
    else:
        raise ValueError(f'Invalid yes-no: {x}')

def get_input(label, convert):
    while True:
        reply = input(f'Enter {label}: ')
        try:
            return convert(reply)
        except (ValueError, TypeError):
            print('Invalid reply')
\$\endgroup\$
0
\$\begingroup\$

You could abstract the character checking into a function, and do one print statement:

def get_info():
    def valid_check(alpha, message):
        """
        Assumes:
            alpha is a boolean discerning whether to check for alpha or numeric
            message is an input request string
        Returns:
            validated input, in string or int type according to alpha
        """
        assert type(alpha) == bool, "alpha should be boolean"
        if alpha:
            inp = input("%s: " % message)
            while not inp.isalpha():
                inp = input("%s: " % message)
            return inp
        else:
            inp = input("%s: " % message)
            while not inp.isnumeric():
                inp = input("%s: " % message)
            return int(inp)

    first = valid_check(True,"enter your first name")
    last = valid_check(True,"enter your last name")
    age = valid_check(False,"enter your age")
    
    has_pet = input("do you have a pet?: ").lower()
    while has_pet not in ["yes", "no", "y", "n"]:
        has_pet = input("do you have a pet?: ").lower()

    if "y" in has_pet:
        has_pet = True
    else:
        has_pet = False
    
    return [first, last, age, has_pet]

def print_info(info):
    first, last, age, has_pet = info
    print(f"Name: {first} {last}\nAge: {age}\nHas pet: {has_pet}")

print_info(get_info())
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Can you explain the changes you've made - how and why the changes you've made are better than the OP's code. \$\endgroup\$
    – Peilonrayz
    Mar 10, 2021 at 20:58
  • \$\begingroup\$ @Peilonrayz The OP asked to implement DRY. my code isn't the best answer here, but it does abstract the input request into a function which could be reused \$\endgroup\$
    – Alon Parag
    Mar 12, 2021 at 7:50
0
\$\begingroup\$

I don't see much of a reason to use sys.argv or the superior argparse. Both seem like added complexity for not much benefit.

You can DRY your code in two ways:

  • Do while loop
    Python doesn't have do while loops, but we can mimic one by using while True: and ending the block with an if to break from the loop.

    Doing so we can use one string rather than two.

    while True:
        first = input("enter your first name: ")
        while first.isalpha():
            break
    
  • Custom input
    We can expand input to have the functionality you want.

    We can see your loops have two similar parts:

    1. Transform the value.
      int(age) and str.lower()

    2. Validate the value.
      str.isalpha()

    As such we can build a function to do everything your while loops are doing.

    def super_input(prompt, *, transform=lambda v: v, validate=lambda v: True, input=input):
        while True:
            try:
                value = transform(input(prompt))
            except ValueError:
                pass
            else:
                if validate(value):
                    return value
    
def get_info():
    first = super_input("enter your first name: ", validate=str.isalpha)
    last = super_input("enter your last name: ", validate=str.isalpha)
    age = super_input("enter your age: ", transform=int)
    has_pet = super_input(
        "do you have a pet?: ",
        transform=str.lower,
        validate=lambda v: v in ["yes", "no", "y", "n"]
    )
    return first, last, age, has_pet.startswith("y")
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.