2

I run into the following two issues at the same time fairly frequently

  1. I have a function with an argument that's expected to be a container of strings
  2. I would like to simplify calls by passing the function either a container of strings or a single string that is not a singleton list

I usually handle this with something like the following, which seems somehow not pythonic to me (I don't know why). Is there a more pythonic way to handle the situation? Is this actually a bad habit and it's most appropriate to require function calls like my_func(['just_deal_with_it'])?

Note the function iterable_not_string below is from sorin's answer to another question

from collections.abc import Iterable

def iterable_not_string(x):

    is_iter = isinstance(x, Iterable)
    is_not_str = (not isinstance(x, str))
    return (is_iter and is_not_str)

def my_func(list_or_string):

    if iterable_not_string(list_or_string):
        do_stuff(list_or_string)
    else:
        do_stuff([list_or_string])
shortorian
  • 1,082
  • 1
  • 10
  • 19
  • 2
    Yes, `my_func([...])` is more appropriate. – chepner Feb 16 '22 at 20:54
  • 2
    Have you considered taking a `*args` vararg argument? Then anyone who wants to pass a list simply does `foo(*mylist)`. As a bonus, you can easily pass finite amounts of strings to the function without brackets or any special syntax. – Silvio Mayolo Feb 16 '22 at 20:55
  • 3
    Ideally, you want your function to accept a consistent kind of argument. If your function accepts an iterable `arg` and operates on each of the elements in `arg`, you should expect its caller to give such an argument instead of trying to infer their meaning and fix any perceived errors within the function – Pranav Hosangadi Feb 16 '22 at 20:56
  • But I agree with the other commenters broadly. "I'll take whatever you give me and figure it out" is a very Javascript-esque mindset. Python functions tend to be more structured in what they expect and what they do. – Silvio Mayolo Feb 16 '22 at 20:56
  • There is the ```type``` builltin function and the ```types``` module to look at. If by ```pythonic``` you mean more intuitively readable balanced with greater coding efficiency (??) then I would look at the object class in its id. Becuse even though in this question you want a string, the argument could be any customised object. – InhirCode Feb 16 '22 at 21:01
  • from the comments and answers here it seems like `my_func(['just_deal_with_it'])` is the way to go but I haven't found an "official" source that confirms it (like a PEP). If someone can point to some documentation on the issue I'll accept an answer. – shortorian Feb 17 '22 at 21:45
  • 1
    @shortorian, what did you see in the answers that made you conclude that? For cases where a single argument is the norm, forgetting to embed it in a list is a common source of mistakes ... and remembering to do so is at best a hassle. Your question was not "should I do this" but "how do I do this", so don't take the criticisms too seriously, you only got one side of the argument. Languages like Python allow this pattern in simple and easy ways (much simpler than in java, for example), so why not benefit from the flexibility? – alexis Feb 18 '22 at 08:44
  • @alexis, that's a fair point, and the answers so far also only make part of the argument. I'll accept an answer that explains both sides and/or points to more complete discussion somewhere else. – shortorian Feb 18 '22 at 17:42
  • 1
    But such an answer would be off-topic, because that's not what you asked. I gave you a short argument in comments, but I won't include it in my answer, for example. And if you do ask that (in a separate question), you need to be careful how to formulate it because you risk having it closed as "opinion-based" :-D – alexis Feb 18 '22 at 17:54

3 Answers3

2

I use the following idiom, which works with any flexibly typed language:

def my_func(arg):
    """arg can be a list, or a single string"""

    if isinstance(arg, str):
        arg = [ arg ]

    # the rest of the code can just treat `arg` as a list
    do_stuff_with_a_list(arg)

By normalizing the argument to a list at the start, you avoid code duplication and type-checking later... and the attendant bugs if you forget a check.

alexis
  • 48,685
  • 16
  • 101
  • 161
  • Haha, that was quick. What is the reason for the downvote? – alexis Feb 16 '22 at 21:15
  • 1
    Your implementation is great - I simply don't think such a pattern should be encouraged. Using `*args` or just passing `my_func(["this is a string"])` seem like better patterns to me. – Andrew McClement Feb 16 '22 at 21:16
  • 1
    But the convenience of accepting either a single value or a list makes for a great interface, and is common in configuration contexts (e.g. Spring xml config). Anyway you are entitled to your opinion, thanks for the explanation. I can spare the -2 points ;-) – alexis Feb 16 '22 at 21:20
2

Another options is to accept arbitrary arguments

def my_func(*strings):
    do_stuff(strings)

my_func('string')
l = ['list', 'of', 'strings']
my_func(*l)

However, I advise to only do this, if the amount of elements is expected to be small, since the unpacking of the iterable may take some time and consume a lot of memory (i.e. on long generators).

Richard Neumann
  • 2,986
  • 2
  • 25
  • 50
-2

You can do this with the python library typing

Typing provids type hinting for python

For example:

from typing import List, Union

def my_func(list_or_string: Union[List, str]):
   ...

Python 3.10 provides a cleaner approach to this:

from typing import List

def my_func(list_or_string: List | str):
  ...

In this case Union is replaced by the | (pipe)

Coder N
  • 252
  • 1
  • 8