`str.join(str(i) for i in value)` for `f-strings`

some_iter = [1, 2, 5., None]
print(f"{some_iter:, }")
# instead
print(', '.join(str(i) for i in some_iter))

or more:

print(f"{some_iter!a:, }")
# instead
print(', '.join(ascii(i) for i in some_iter))

this would simplify the code for many (including me), often you have to write something similar, plus it seems not difficult to implement.

the format for this, of course, may differ, the main thing is the idea.

Your thoughts?

2 Likes

So, basically, you want a directive that says “iterate over these things and use this format string for each one”. Not sure whether it’s better to design it with join() semantics or simply as “use this for every element” (which would result in another comma at the end), but either way, it’s definitely useful.

One way to implement this would be a function-like class.

>>> class each:
...     def __init__(self, iterable):
...             self.iterable = iterable
...     def __format__(self, fmt):
...             return "".join(format(x, fmt) for x in self.iterable)
... 
>>> some_iter = [1, 2, 5., None]
>>> f"Stuff: {each(some_iter)}"
'Stuff: 125.0None'

Design a suitable way to separate the separator from the format string (since you should, for instance, be able to format a list of numbers 03d and then join them with ", "), and this would be a useful little tool.

2 Likes

perhaps, given the dynamic typing, it will be problematic to apply the format for each, so for now I suggest only concatenating them together.

But something like this, of course, is not ideal, but it will work.

class Iterable:
    def __format__(self, fmt):
        concat, *fmts = fmt.split('|')
        def _format(el) -> str:
            for opt in fmts:
                try: return format(el, opt)
                except TypeError: pass
            return str(el)
        return concat.join(map(_format, self))

print(f'{some_iter:, |0.f|d}')

Actually, adding __format__ to Iterable won’t work, because list only inherits virtually from Iterable. What you want is adding a __format__ to list itself. Then it could work. I think it’s cute, but it’s also pretty cryptic, so I’m not sure I’d want it in my language if it was still my language (which it isn’t – it’s the community’s language now).

1 Like

If it matters, this member of the community really doesn’t want it in our language either, for precisely that reason: it’s too cryptic and excessively terse.

I just gave an example of how it could look (I agree, it turned out a little esoteric)
I really don’t know how to actually implement this, but I would suggest looking at the only non-ambiguous option, it’s just short version of str.join(map(str, list)), without applying formats (This is what I suggested at first).

The motivation for this post is not to reinvent the wheel every time you need to join a list of different types.

hmm – in that case, write a little utility function, which I think is what Chris A suggested. Something like:

In [104]: def joinf(iterable, joiner=", ", format=""):
     ...:     format=f"{{{format}}}"
     ...:     return joiner.join(format.format(i) for i in iterable)

In [105]: some_iter = [1, 2, 5., None]

In [106]: joinf(some_iter)
Out[106]: '1, 2, 5.0, None'

In [107]: joinf(some_iter, ", ", "!a:10s")
Out[107]: '1         , 2         , 5.0       , None 

One trick is that the format string would have to work with all the values in the iterable, which makes this less useful, at least for non-homogenous iterables.

Now that I think about it, perhaps a format specifier could be added as an optional parameter to the str.join() method, so your above examples would be:

", ",join(some_iter, format="")
or
", ",join(some_iter, format="!a")

I think that would be a lot less cryptic than adding an implied iteration to a f-string, since str.join() is already about iteration.

We are on the territory of template engines now. There are issues with a terse syntax:

  1. We need not one, but at least two arguments: separator and format (or even expression) for formatting items.
  2. In English you need more complex formatting for enumeration: “Alice, Bob, Charlie, and David” but “Alice and Bob” (without comma). And it may be “or” or “nor” instead of “and”. In different languages there are different rules.
  3. What about multidimensional collections? Things become even more complicated with nested formatting.

Currently it is simpler to write a helper function which perform formatting which you need, with parametrization enough for your needs, and use it, as was suggested by Christopher. It does not require new syntax, it is as powerful and flexible as you need, and it is as readable as any Python code.

2 Likes

Yes, but let’s be honest, it’s mostly homogenous iterables where you tend to think in terms of “format all these things”. Or at least, as homogenous as the formatting requirements (eg “give me the repr for every object in this list”).

I do think this would be a useful feature, although personally I don’t think it needs syntax. It doesn’t necessarily even need to be in the standard library.

1 Like

IMO, it’s fine as it is. I’ll concede that ", ".join(str(i) for i in value) isn’t an obvious idiom, but once you’ve learned the components (string methods, the idiomatic way join works, generator expressions) it’s far more explicit and flexible than a dedicated function would be. And it’s short enough that you can wrap it in a local function if you want.

One advantage of writing your own wrapper that’s not mentioned often enough is that they can be much simpler and ignore any edge cases that don’t matter for your specific situation. And they can be named using domain-specific terminology. … Among the advantages… :slightly_smiling_face:

1 Like

A simpler version would be:

def joinf(iterable, joiner=", ", fmtstr=""):
    return joiner.join(format(i, fmtstr) for i in iterable)

print(joinf(some_iter))

Or, if you want some conversion:

def joinf(iterable, joiner=", ", fmtstr="", conv=lambda x:x):
    return joiner.join(format(conv(i), fmtstr) for i in iterable)

print(joinf(some_iter, ", ", "10s", conv=ascii))

But I don’t think this needs to be in the stdlib, and certainly not any new syntax.

2 Likes

Yeah, I was trying to hint to the OP that this would be a perfect thing for a personal toolbox :slight_smile:

Thanks Eric, I always forget about the fornat() function. I don’t think I’ve even used it.

I also don’t think this is needed, and frankly, I don’t think I’d ever put that function in my toolbox – it’s only tricky if you try to make it general – the comprehension is straightforward.

But if it were to go in the stdlib, adding it to the join method makes sense to me. Certainly better than new syntax.

I would prefer separator.join(iterable) to automatically call str on each element in the iterable.

1 Like

I’ve thought about that – str.join only works on strings anyway, so why not? I can’t see the harm.

though it would be backward incompatible, someone may be counting on an exception if non-strings are passed in. I suppose it could be an optional parameter:

str.join(iterable, stringify=False)

I would like that.

2 Likes

Why call str on the elements? Why not repr, or ascii, or some other converter?

We need to distinguish between functions which are part of a low-level API, and those expected to work as part of a higher level API.

print has a high-level API. It should be polymorphic, and work with any type. I should be able to print any object at all, and get something sensible, without caring too much about it. It’s okay for print to guess what converter we want.

I’m unlikely to capture the output of print and use it in other computations, so “something sensible” doesn’t need to be too precise.

str.join is part of a low-level string API. It is right and proper that it only works on strings, not arbitrary objects. Being low-level, it shouldn’t guess what to do with non-string values:

  • is it an error? if so, raise
  • or does the programmer intend there to be a non-string in the input?
  • if so, how does the programmer want to convert the value into a string?

A low-level API should not guess what is wanted. In this case, explicit is better than implicit:

sep.join(map(ascii, values))

If you want a high-level joiner that works on anything, like print, its a one-liner:

def join(values, *, sep=''):
    return sep.join([str(obj) for obj in values])

But it hardly seems worth it, for such a simple operation. YMMV.

There was a previous discussion of implicit string coercion for str.join here.

1 Like

Your argument makes sense.

Don’t forget that print() has a sep argument, so this can also be written

print(*some_iter, sep=', ')

If you just want the string without printing map() can help

', '.join(map(str, some_iter))

And if you want some formatting

', '.join(map('{:04d}'.format, some_iter))

or

', '.join(f'{n:04d}' for n in some_iter)

None of these seem so awkward as to need adding further wrinkles to f-strings.

I think being explicit is better.

separator.join(map(str, iterable))
1 Like

Just as an idea. I sometimes just do

str([1,2,3])[1:-1]

Which in the very specific use case of wanting to join a list of numbers on ', ' is pretty nice.
But I have to say. I like this by @steven.rumbalski a lot too. I will definitely try that one out.

print(*some_iter, sep=', ')