8

I have a function that makes a call to a REST API. One of the parameter is a comma-separated list of names. To generate it, I do this:

','.join(names)

However, I also want to allow the user to provide a single name. The problem is that if names = ERII, for instance, it results in ['E', 'R', 'I', 'I'], but I need it to be ['ERII'] instead.

Now, I could force the user to enter a list with only one value (names=['ERRI'] or names=('ERII',). I would prefer to allow the user to provide a single String. Is there a clever way to do that without a if else statement checking if the provided value is an Iterable?

Also, I am uncertain as what would be the best practice here, force to provide a list, or allow a single string?

2
  • @Carcigenicate, I don't have a variable name. Commented Aug 8, 2018 at 0:44
  • 2
    What about taking *names in the params? Then the user can send you one name, or five names, without needing brackets—or, if he has a list handy, he can just *lst it at you. Commented Aug 8, 2018 at 0:48

3 Answers 3

25

Parameters that can be either a thing or an iterable or things are a code smell. It’s even worse when the thing is a string, because a string is an iterable, and even a sequence (so your test for isinstance(names, Iterable) would do the wrong thing).

The Python stdlib does have a few such cases—most infamously, str.__mod__—but most of those err in the other direction, explicitly requiring a tuple rather than any iterable, and most of them are considered to be mistakes, or at least things that wouldn’t be added to the language today. Sometimes it is still the best answer, but the smell should make you think before doing it.

I don’t know exactly what your use case is, but I suspect this will be a lot nicer:

def spam(*names):
    namestr = ','.join(names)
    dostuff(namestr)

Now the user can call it like this:

spam('eggs')
spam('eggs', 'cheese', 'beans')

Or, if they happen to have a list, it’s still easy:

spam(*ingredients)

If that’s not appropriate, another option is keywords, maybe even keyword-only params:

def spam(*, name=None, names=None):
    if name and names:
        raise TypeError('Not both!')
    if not names: names = [name]

But if the best design really is a string or a (non-string) iterable of strings, or a string or a tuple of strings, the standard way to do that is type switching. It may look a bit ugly, but it calls attention to the fact that you’re doing exactly what you’re doing, and it does it in the most idiomatic way.

def spam(names):
    if isinstance(names, str):
        names = [names]
    dostuff(names)
Sign up to request clarification or add additional context in comments.

4 Comments

Thank you for that answer, I appreciate the advice on best coding practices. In my specific case, this is my function's signature : def get_symbols(names=None, ids=None, id=None). I am already using the keywork pattern for ids. The only reason I am doing this for ids and not for names is because this is a python wrapper for a REST API and this is how they designed it (with those three fields) and I wanted to keep it this way so that users can refer to the their doc and still seemingly use my code.
The * option seems nice too. However, I am not familiar with this kind of structure when the function accepts multiple parameters. How does Python differentiate between the list of *names passed and the next params in the function's signature? Where could I find documentation on this?
@AntoineViscardi The *args has to be the last parameter, except for keyword-only parameter. The actual documentation may be a bit hard to follow; first see More on Defining Functions in the official tutorial, especially the Arbitrary Argument Lists section.
@AntoineViscardi Assuming you're using Python 3, if you do def get_symbols(*names, ids=None, id=None), then ids and id will become keyword-only parameters—that is, you can still only them by name, like get_symbols('spam', 'eggs', ids=[1,2,3]).
2

Using isinstance() to identify the type of input may provide a solution:

def generate_names(input_term):
    output_list = []
    if isinstance(input_term,str):
        output_list.append(','.join(input_term.split()))
    elif isinstance(input_term,list):
        output_list.append(','.join(input_term))
    else:
        print('Please provide a string or a list.')
    return(output_list)

This will allow you to input list, string of a single name, as well as string containing several names(separated by space):

name = 'Eric'
name1 = 'Alice John'
namelist = ['Alice', 'Elsa', 'George']

Apply this function:

print(generate_names(name))
print(generate_names(name1))
print(generate_names(namelist))

Get:

['Eric']

['Alice,John']

['Alice,Elsa,George']

Comments

1

I'd allow both list and single strings as arguments. Also, I don't think there's anything wrong with the if/else solution, except that you could directly check if the argument is an instance of the str class (rather then checking if the argument is iterable):

def foo(arg1):
    if isinstance(arg1, str):
        print("It's a string!")
    else:
        print("It's not a string!")

Be careful if checking whether the argument is iterable - both strings and lists are iterable.

1 Comment

Yes I think this is what I'll do for now! And you are right, checking for Iterable was not a good idea on my part: the fact that a string is Iterable is what is causing my problem in the first place... oups :p

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.