63

I have a routine that takes a list of strings as a parameter, but I'd like to support passing in a single string and converting it to a list of one string. For example:

def func( files ):
    for f in files:
        doSomethingWithFile( f )

func( ['file1','file2','file3'] )

func( 'file1' ) # should be treated like ['file1']

How can my function tell whether a string or a list has been passed in? I know there is a type function, but is there a "more pythonic" way?

James McMahon
  • 48,506
  • 64
  • 207
  • 283
Graeme Perrow
  • 56,086
  • 21
  • 82
  • 121

8 Answers8

40
isinstance(your_var, basestring)
Ayman Hourieh
  • 132,184
  • 23
  • 144
  • 116
37

Well, there's nothing unpythonic about checking type. Having said that, if you're willing to put a small burden on the caller:

def func( *files ):
    for f in files:
         doSomethingWithFile( f )

func( *['file1','file2','file3'] ) #Is treated like func('file1','file2','file3')
func( 'file1' )

I'd argue this is more pythonic in that "explicit is better than implicit". Here there is at least a recognition on the part of the caller when the input is already in list form.

David Berger
  • 12,385
  • 6
  • 38
  • 51
  • 11
    Surely the more "explicit" method would be to require the user to pass the single-file in a list? Like func(['file1']) – dbr May 07 '09 at 19:08
  • 2
    I agree that that is explicit, but I don't see how it is more explicit. Both techniques use the same logic; one is the reverse of the other. I happen to think that the above is slightly more intuitive, because it emphasizes that the files in the list rather than the list itself is relevant to func. – David Berger May 07 '09 at 21:03
32

Personally, I don't really like this sort of behavior -- it interferes with duck typing. One could argue that it doesn't obey the "Explicit is better than implicit" mantra. Why not use the varargs syntax:

def func( *files ):
    for f in files:
        doSomethingWithFile( f )

func( 'file1', 'file2', 'file3' )
func( 'file1' )
func( *listOfFiles )
Dave
  • 10,369
  • 1
  • 38
  • 35
  • 1
    "Personally, I don't really like this sort of behavior", what are you referring to, specifically? – James McMahon May 08 '09 at 01:15
  • 1
    @nemo I think he meant the original question, which could be interpreted as asking for a function whose parameter is either a string or list of strings wherein the function call offers no mention of which type of parameter is passed in. I agree with this. – David Berger May 08 '09 at 16:15
  • 1
    FWIW, varargs only works if there're no other args: `func(file, someflag)`, can't be turned into `func(*files, someflag)` without turning `someflag` into a keyword arg – pjz Nov 23 '15 at 19:37
16

I would say the most Python'y way is to make the user always pass a list, even if there is only one item in it. It makes it really obvious func() can take a list of files

def func(files):
    for cur_file in files:
        blah(cur_file)

func(['file1'])

As Dave suggested, you could use the func(*files) syntax, but I never liked this feature, and it seems more explicit ("explicit is better than implicit") to simply require a list. It's also turning your special-case (calling func with a single file) into the default case, because now you have to use extra syntax to call func with a list..

If you do want to make a special-case for an argument being a string, use the isinstance() builtin, and compare to basestring (which both str() and unicode() are derived from) for example:

def func(files):
    if isinstance(files, basestring):
        doSomethingWithASingleFile(files)
    else:
        for f in files:
            doSomethingWithFile(f)

Really, I suggest simply requiring a list, even with only one file (after all, it only requires two extra characters!)

dbr
  • 165,801
  • 69
  • 278
  • 343
  • 15
    The problem is that if you only rely on duck typing here, a string *is* treated like a list with duck typing, and doesn't do the right thing. Instead of treating it like a single item, python will treat the string like a list of characters. Whups. – Robert P Dec 07 '11 at 19:01
  • Note that Python 3 has no `basestring`. Use `str` unless you're referring to a bytes object (which you definitely shouldn't be for this use case). – Soren Bjornstad Sep 04 '18 at 22:58
13
if hasattr(f, 'lower'): print "I'm string like"
Thorsten Kranz
  • 12,492
  • 2
  • 39
  • 56
limscoder
  • 3,037
  • 2
  • 23
  • 37
11
def func(files):
    for f in files if not isinstance(files, basestring) else [files]:
        doSomethingWithFile(f)

func(['file1', 'file2', 'file3'])

func('file1')
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • POP 8 seems to prefer isinstance(u'abc', basestring) instead of using the types module. – max Aug 02 '09 at 15:49
  • @mdorseif: I've edited the answer to use `basestring` instead of `types` module. – jfs Nov 13 '10 at 17:25
6

If you have more control over the caller, then one of the other answers is better. I don't have that luxury in my case so I settled on the following solution (with caveats):

def islistlike(v):
   """Return True if v is a non-string sequence and is iterable. Note that
   not all objects with getitem() have the iterable attribute"""
   if hasattr(v, '__iter__') and not isinstance(v, basestring):
       return True
   else:
       #This will happen for most atomic types like numbers and strings
       return False

This approach will work for cases where you are dealing with a know set of list-like types that meet the above criteria. Some sequence types will be missed though.

Scott Stafford
  • 43,764
  • 28
  • 129
  • 177
Dana the Sane
  • 14,762
  • 8
  • 58
  • 80
3

Varargs was confusing for me, so I tested it out in Python to clear it up for myself.

First of all the PEP for varargs is here.

Here is sample program, based on the two answers from Dave and David Berger, followed by the output, just for clarification.

def func( *files ):
    print files
    for f in files:
        print( f )

if __name__ == '__main__':
    func( *['file1','file2','file3'] ) #Is treated like func('file1','file2','file3')
    func( 'onestring' )
    func( 'thing1','thing2','thing3' )
    func( ['stuff1','stuff2','stuff3'] )

And the resulting output;

('file1', 'file2', 'file3')
file1
file2
file3
('onestring',)
onestring
('thing1', 'thing2', 'thing3')
thing1
thing2
thing3
(['stuff1', 'stuff2', 'stuff3'],)
['stuff1', 'stuff2', 'stuff3']

Hope this is helpful to somebody else.

James McMahon
  • 48,506
  • 64
  • 207
  • 283