bytes.join()

The separator is the object you call the method on, which reads backwards until it clicks. It is also the only sane way to build a large buffer from many pieces.

Bytes methodPython 3.0+Live demo
Common call
b','.join(parts)
Returns
a single bytes object
Replaces
a += loop, which copies the whole buffer every iteration
Watch out
every item must be bytes — one str in the list and it raises
bytes.join(iterableiterable — Any iterable of bytes-like objects. A generator works; the items are consumed once.type: iterable · required)
→ bytes

Demo

Live evaluation
Try:
Inputs
sepstrseparator
partslistparts, comma separated
Output
bytes(',', 'utf-8').join([bytes(p, 'utf-8') for p in ['a', 'b', 'c']])
b'a,b,c'

The separator goes BETWEEN the parts, never at the ends — so three parts get two separators. A single part gets none at all, and an empty iterable gives empty bytes rather than an error. The empty-separator case is the standard way to concatenate a list of chunks into one buffer with nothing between them.

Parameters

NameTypeRequiredDescription
iterableiterableyesAny iterable of bytes-like objects. A generator works; the items are consumed once.

Return value

bytes — One bytes object with the separator placed between each item. An empty iterable gives empty bytes.

Common patterns

Concatenate chunks efficiently
The correct way to assemble a buffer read in pieces.
body = b''.join(chunks)
Rebuild a delimited record
The inverse of split.
line = b','.join(fields)
Join from a generator
No intermediate list is built, though join must still buffer internally.
data = b''.join(read_chunk() for _ in range(n))

Examples

1. Comma separated
b','.join([b'a', b'b', b'c'])
Returns
b'a,b,c'
2. Concatenate
b''.join([b'a', b'b'])
Returns
b'ab'
3. One part
b','.join([b'a'])
Returns
b'a'
4. No parts
b','.join([])
Returns
b''
5. str item raises
b','.join([b'a', 'b'])
Returns
TypeError: sequence item 1: expected a bytes-like object, str found
6. Round trip
b','.join(b'a,b'.split(b','))
Returns
b'a,b'

Pitfalls

1. The separator is the receiver, not an argument
Reading it aloud helps — "join these with a comma" is written as the comma joining them. Newcomers reliably try to call join on the list instead.
Backwards
[b'a', b'b'].join(b',')
AttributeError: 'list' object has no attribute 'join'
Separator first
b','.join([b'a', b'b'])
b'a,b'
2. One str in the list and it raises
Every item must be bytes-like. The error names the offending index, which is the one helpful thing about it — but mixed lists are easy to build accidentally when part of the data was decoded.
Mixed types
b','.join([b'a', 'b'])
TypeError: sequence item 1: expected a bytes-like object, str found
Encode them all
b','.join(p.encode() for p in ['a', 'b'])
b'a,b'
3. Building with += is quadratic
Bytes are immutable, so each += copies everything accumulated so far. Over many chunks this becomes O(n squared) — join exists precisely to avoid it.
Copies every time
buf = b''
for c in chunks:
    buf += c
quadratic
Join once
buf = b''.join(chunks)
linear
4. The separator does not appear at the ends
join places it strictly between items. Code expecting a trailing delimiter — as many line-based formats want — has to add it explicitly.
No trailing newline
b'\n'.join([b'a', b'b'])
b'a\nb'
Add one
b'\n'.join([b'a', b'b']) + b'\n'
b'a\nb\n'

When to use

Use it
  • Assembling a buffer from many chunks
  • Rebuilding a delimited record after editing its fields
  • Any loop that would otherwise concatenate with +=
Reach for something else
  • Two or three fixed pieces → plain + is clearer
  • The parts are text → join the strings, then encode once
  • You are appending repeatedly over time → bytearray

Notes

Complexity
O(total length) — one pass to size the result, one to fill it
Return
A new bytes object; the parts are unchanged
CPython impl
Objects/bytesobject.c :: bytes_join
Memory
Allocates the result once, which is why it beats repeated concatenation
Thread-safe
Yes — bytes are immutable

FAQ

Because join belongs to the separator type, and any iterable can supply the parts. Putting it on the list would mean every sequence type needed its own version. It reads oddly at first and then becomes second nature.

b','.join(parts)

History

3.0
bytes.join arrived with the bytes type in the text/binary split.