1985

This has always confused me. It seems like this would be nicer:

["Hello", "world"].join("-")

Than this:

"-".join(["Hello", "world"])

Is there a specific reason it is like this?

Mateen Ulhaq
  • 21,459
  • 16
  • 82
  • 123
Evan Fosmark
  • 94,395
  • 33
  • 103
  • 116
  • 4
    For easy memory and understanding, `-` declares that you are joining a list and converting to a string.It's result oriented. – AbstProcDo Dec 04 '17 at 12:26
  • 7
    I think the original idea is that because join() returns a string, it would have to be called from the string context. Putting join() on a list doesn't make a ton of sense in that a list is a container of objects and shouldn't have a one-off function specific to only strings. – Joshua Burns May 29 '19 at 14:46
  • 4
    @BallpointBen "...because Python's type system isn't strong enough" is exactly wrong. As Yoshiki Shibukawa's answer (from 8 years before you comment!) says, iterable.join() was considered as possibility but was rejected because it's a less good API design - not because it wasn't possible to implement. – Arthur Tacca Jun 12 '20 at 09:39
  • 4
    I may be biased because I am used to javascript, but you want to join the list, it should be a method of list imo. It feels backwards. – Zimri Leijen Jan 09 '21 at 05:00
  • 1
    I think it's because of the fact that " `join` is a string method that results in a string" makes more sense? – Cheukting Apr 10 '21 at 23:30
  • 1
    Well, `str.split()` returns a non-string and makes quite a bit of sense. It seems like the same logic should be ok here, right? (Just talking about the conceptual problem of a non-string output) – ntjess Jan 14 '22 at 19:19

11 Answers11

1377

It's because any iterable can be joined (e.g, list, tuple, dict, set), but its contents and the "joiner" must be strings.

For example:

'_'.join(['welcome', 'to', 'stack', 'overflow'])
'_'.join(('welcome', 'to', 'stack', 'overflow'))
'welcome_to_stack_overflow'

Using something other than strings will raise the following error:

TypeError: sequence item 0: expected str instance, int found

user3840170
  • 22,750
  • 2
  • 21
  • 50
recursive
  • 80,919
  • 32
  • 145
  • 234
  • 94
    I do not agree conceptually even if It makes sense codewise. `list.join(string)` appears more an object-oriented approach whereas `string.join(list)` sounds much more procedural to me. – Eduardo Pignatelli Jan 14 '18 at 15:35
  • 36
    So why isn't it implemented on iterable? – Steen Schütt Mar 28 '18 at 10:50
  • 17
    @TimeSheep: A list of integers doesn't have a meaningful join, even though it's iterable. – recursive Mar 28 '18 at 17:21
  • 2
    @recursive, I think an iterable of integers can be understood as string. – krysopath Apr 12 '18 at 07:24
  • 3
    @krysopath: It *can* be, but there are multiple such understandings. Non-list iterables of strings need a way to be joined. And lists of strings are iterables. So it's possible to satisfy all the use cases with this single method. Lists *could* have a join method, like in javascript, but there are plenty of use cases in python where the existing join method would still be needed. And you can pretty trivially transform the existing one into what you're thinking. e.g. `", ".join(map(str,numbers))`. – recursive Apr 12 '18 at 17:17
  • @recursive, I think the words `"joiner" are always strings.` capture the essence very nicely. Your point about conventions is just true, though. I figure the reason might be the string handling/bytes encode/decode paradigm python has going on. A string is so very close to a sequence of numbers, thats why I was perhaps stating the obvious :) – krysopath Apr 12 '18 at 21:57
  • 22
    I have tried to use `print(str.join('-', my_list))` and it works, feels better. – pimgeek Jun 01 '18 at 06:29
  • 28
    @TimeSheep Because iterable is not a concrete type, iterable is an interface, any type that defines an `__iter__` method. Requiring all iterables to also implement `join` would complicate a general interface (which also covers iterables over non-strings) for a very particular use case. Defining `join` on strins side-steps this problem at the cost of the "unintuitive" order. A better choice might have been to keep it a function with the first argument being the iterable and the second (optional one) being the joiner string - but that ship has sailed. – user4815162342 Jun 11 '18 at 06:08
  • 1
    @user4815162342 Maybe this is the exact root cause here, that iterable is an interface instead of an abstract base class (with some non-abstract methods)? In that case, the base class would provide a reasonable, default implementation of `iterable.join` which implementers could override as needed. I'm not _sure_, but I suspect this is how it works in Ruby (where [1,2,3].join` is supported) – Per Lundberg Oct 08 '18 at 06:40
  • 6
    @PerLundberg But then _every_ iterable would sport a `join` method, even those where it makes no sense. For example, you'd have a `file.join` that has nothing to do with files, generators would have a `join` method that is entirely unrelated to generators (and very dangerous with infinite ones). I am not familiar with Ruby, but I suspect it simply implements `join()` as a list method, not as a method on every single iterator. – user4815162342 Oct 08 '18 at 09:51
  • 2
    @user4815162342 You are right, it is actually implemented on the `Array` class: https://ruby-doc.org/core-2.5.1/Array.html#method-i-join Your reasoning might be correct, there's probably a good reason why they didn't choose that route (and also why Ruby didn't choose it either). I think I'm now more in the camp of the "many hard-core Python programmers" mentioned in another answer to this question. :-) – Per Lundberg Oct 08 '18 at 11:59
389

This was discussed in the String methods... finally thread in the Python-Dev achive, and was accepted by Guido. This thread began in Jun 1999, and str.join was included in Python 1.6 which was released in Sep 2000 (and supported Unicode). Python 2.0 (supported str methods including join) was released in Oct 2000.

  • There were four options proposed in this thread:
    • str.join(seq)
    • seq.join(str)
    • seq.reduce(str)
    • join as a built-in function
  • Guido wanted to support not only lists and tuples, but all sequences/iterables.
  • seq.reduce(str) is difficult for newcomers.
  • seq.join(str) introduces unexpected dependency from sequences to str/unicode.
  • join() as a built-in function would support only specific data types. So using a built-in namespace is not good. If join() supports many datatypes, creating an optimized implementation would be difficult, if implemented using the __add__ method then it would ve O(n²).
  • The separator string (sep) should not be omitted. Explicit is better than implicit.

Here are some additional thoughts (my own, and my friend's):

  • Unicode support was coming, but it was not final. At that time UTF-8 was the most likely about to replace UCS2/4. To calculate total buffer length of UTF-8 strings it needs to know character coding rule.
  • At that time, Python had already decided on a common sequence interface rule where a user could create a sequence-like (iterable) class. But Python didn't support extending built-in types until 2.2. At that time it was difficult to provide basic iterable class (which is mentioned in another comment).

Guido's decision is recorded in a historical mail, deciding on str.join(seq):

Funny, but it does seem right! Barry, go for it...
Guido van Rossum

Yoshiki Shibukawa
  • 4,049
  • 1
  • 12
  • 7
  • Nice, this documents the reasoning. It'd be nice to know more about the "_unexpected dependency from sequences to str/unicode._" -- and whether that is still so. – zdim Mar 16 '22 at 16:59
256

Because the join() method is in the string class, instead of the list class?

I agree it looks funny.

See http://www.faqs.org/docs/diveintopython/odbchelper_join.html:

Historical note. When I first learned Python, I expected join to be a method of a list, which would take the delimiter as an argument. Lots of people feel the same way, and there’s a story behind the join method. Prior to Python 1.6, strings didn’t have all these useful methods. There was a separate string module which contained all the string functions; each function took a string as its first argument. The functions were deemed important enough to put onto the strings themselves, which made sense for functions like lower, upper, and split. But many hard-core Python programmers objected to the new join method, arguing that it should be a method of the list instead, or that it shouldn’t move at all but simply stay a part of the old string module (which still has lots of useful stuff in it). I use the new join method exclusively, but you will see code written either way, and if it really bothers you, you can use the old string.join function instead.

--- Mark Pilgrim, Dive into Python

Community
  • 1
  • 1
Bill Karwin
  • 499,602
  • 82
  • 638
  • 795
  • 16
    The Python 3 `string` library has removed all the redundant `str` methods, so you no longer can use `string.join()`. Personally, I never thought it 'funny', it makes perfect sense, as you can join much more than just lists, but the joiner is always a string! – Martijn Pieters Aug 31 '18 at 11:51
75

I agree that it's counterintuitive at first, but there's a good reason. Join can't be a method of a list because:

  • it must work for different iterables too (tuples, generators, etc.)
  • it must have different behavior between different types of strings.

There are actually two join methods (Python 3.0):

>>> b"".join
<built-in method join of bytes object at 0x00A46800>
>>> "".join
<built-in method join of str object at 0x00A28D40>

If join was a method of a list, then it would have to inspect its arguments to decide which one of them to call. And you can't join byte and str together, so the way they have it now makes sense.

Kiv
  • 30,432
  • 6
  • 42
  • 59
47

Why is it string.join(list) instead of list.join(string)?

This is because join is a "string" method! It creates a string from any iterable. If we stuck the method on lists, what about when we have iterables that aren't lists?

What if you have a tuple of strings? If this were a list method, you would have to cast every such iterator of strings as a list before you could join the elements into a single string! For example:

some_strings = ('foo', 'bar', 'baz')

Let's roll our own list join method:

class OurList(list): 
    def join(self, s):
        return s.join(self)

And to use it, note that we have to first create a list from each iterable to join the strings in that iterable, wasting both memory and processing power:

>>> l = OurList(some_strings) # step 1, create our list
>>> l.join(', ') # step 2, use our list join method!
'foo, bar, baz'

So we see we have to add an extra step to use our list method, instead of just using the builtin string method:

>>> ' | '.join(some_strings) # a single step!
'foo | bar | baz'

Performance Caveat for Generators

The algorithm Python uses to create the final string with str.join actually has to pass over the iterable twice, so if you provide it a generator expression, it has to materialize it into a list first before it can create the final string.

Thus, while passing around generators is usually better than list comprehensions, str.join is an exception:

>>> import timeit
>>> min(timeit.repeat(lambda: ''.join(str(i) for i in range(10) if i)))
3.839168446022086
>>> min(timeit.repeat(lambda: ''.join([str(i) for i in range(10) if i])))
3.339879313018173

Nevertheless, the str.join operation is still semantically a "string" operation, so it still makes sense to have it on the str object than on miscellaneous iterables.

Russia Must Remove Putin
  • 337,988
  • 84
  • 391
  • 326
24

Think of it as the natural orthogonal operation to split.

I understand why it is applicable to anything iterable and so can't easily be implemented just on list.

For readability, I'd like to see it in the language but I don't think that is actually feasible - if iterability were an interface then it could be added to the interface but it is just a convention and so there's no central way to add it to the set of things which are iterable.

Andy Dent
  • 16,995
  • 6
  • 83
  • 111
14

- in "-".join(my_list) declares that you are converting to a string from joining elements a list.It's result-oriented. (just for easy memory and understanding)

I made an exhaustive cheatsheet of methods_of_string for your reference.

string_methods_44 = {
    'convert': ['join','split', 'rsplit','splitlines', 'partition', 'rpartition'],
    'edit': ['replace', 'lstrip', 'rstrip', 'strip'],
    'search': ['endswith', 'startswith', 'count', 'index', 'find','rindex', 'rfind',],
    'condition': ['isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isnumeric','isidentifier',
                  'islower','istitle', 'isupper','isprintable', 'isspace', ],
    'text': ['lower', 'upper', 'capitalize', 'title', 'swapcase',
             'center', 'ljust', 'rjust', 'zfill', 'expandtabs','casefold'],
    'encode': ['translate', 'maketrans', 'encode'],
    'format': ['format', 'format_map']}
Iulian Onofrei
  • 8,409
  • 9
  • 67
  • 106
AbstProcDo
  • 17,381
  • 14
  • 68
  • 114
13

Primarily because the result of a someString.join() is a string.

The sequence (list or tuple or whatever) doesn't appear in the result, just a string. Because the result is a string, it makes sense as a method of a string.

S.Lott
  • 373,146
  • 78
  • 498
  • 766
3

Both are not nice.

string.join(xs, delimit) means that the string module is aware of the existence of a list, which it has no business knowing about, since the string module only works with strings.

list.join(delimit) is a bit nicer because we're so used to strings being a fundamental type(and lingually speaking, they are). However this means that join needs to be dispatched dynamically because in the arbitrary context of a.split("\n") the python compiler might not know what a is, and will need to look it up(analogously to vtable lookup), which is expensive if you do it a lot of times.

if the python runtime compiler knows that list is a built in module, it can skip the dynamic lookup and encode the intent into the bytecode directly, whereas otherwise it needs to dynamically resolve "join" of "a", which may be up several layers of inheritence per call(since between calls, the meaning of join may have changed, because python is a dynamic language).

sadly, this is the ultimate flaw of abstraction; no matter what abstraction you choose, your abstraction will only make sense in the context of the problem you're trying to solve, and as such you can never have a consistent abstraction that doesn't become inconsistent with underlying ideologies as you start gluing them together without wrapping them in a view that is consistent with your ideology. Knowing this, python's approach is more flexible since it's cheaper, it's up to you to pay more to make it look "nicer", either by making your own wrapper, or your own preprocessor.

Dmitry
  • 4,752
  • 4
  • 35
  • 48
  • 3
    "string module is aware of the existence of a list, which it has no business knowing about" Not true. The parameter to the `join()` method is any iterable, so `str` doesn't need to know about `list` (at least, not for that method). Clearly "iterable" is a more fundamental than `str`, because `str` actually is iterable itself! (Also, I would argue that `list` is more fundamental than `str` because Unicode character handling is much trickier than just storing a sequence of objects, but as I said it's irrelevant here.) – Arthur Tacca Jun 12 '20 at 09:48
  • "if the python runtime compiler knows that list is a built in module, it can skip the dynamic lookup" (You mean "class" rather than "module".) This is odd. If `l` is a list and `s` is a string then `l.join(s)` and `s.join(l)` involve dynamic lookup using the class system either way. Maybe if you're using a string literal `"-".join(...)` it could avoid it but that would also apply to list literals `[...].join("-")`. I suppose maybe the former is more common. But I don't think this optimisation is done anyway, and, as Yoshiki's answer shows, this certainly wasn't the reason for the decission. – Arthur Tacca Jun 12 '20 at 09:52
2

You can't only join lists and tuples. You can join almost any iterable. And iterables include generators, maps, filters etc

>>> '-'.join(chr(x) for x in range(48, 55))
'0-1-2-3-4-5-6'

>>> '-'.join(map(str, (1, 10, 100)))
'1-10-100'

And the beauty of using generators, maps, filters etc is that they cost little memory, and are created almost instantaneously.

Just another reason why it's conceptually:

str.join(<iterator>)

It's efficient only granting str this ability. Instead of granting join to all the iterators: list, tuple, set, dict, generator, map, filter all of which only have object as common parent.

Of course range(), and zip() are also iterators, but they will never return str so they cannot be used with str.join()

>>> '-'.join(range(48, 55))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sequence item 0: expected str instance, int found
  • 1
    "_Instead of granting join to all the iterators: [...], all of which only have object as common parent._" -- this seems a sensible reason (to not have `iter.join()`) – zdim Mar 16 '22 at 17:43
1

The variables my_list and "-" are both objects. Specifically, they're instances of the classes list and str, respectively. The join function belongs to the class str. Therefore, the syntax "-".join(my_list) is used because the object "-" is taking my_list as an input.

fiftytwocards
  • 77
  • 1
  • 5