Python Part I · Module 4 · Strings
join(): Building One String From Many
Chapter 4.8 took a line apart. This chapter puts one together, and it is the exact inverse: split turns one string into pieces, join turns pieces into one string. It is also the most notoriously backwards-looking method in the language, because it is written on the separator rather than on the pieces — you write a space and ask the space to do the joining. That reads strangely until you see the reason for it, which takes one paragraph and then never bothers you again. Chapter 4.3 promised this method as the right tool for assembling text from many pieces; this is where that promise is kept, and where you find out what it does when a number sneaks into the pieces.
Foundation12 min readPythonPart IFundamentalsStringsAssembly
Part I · Module 4 · Chapter 4.9 · join(): Building One String From Many
1. The Problem, and the Call
You have several pieces of text and you want one string with something between them. A command line, for instance — four separate arguments that need to become one command:
parts = ["vcs", "-full64", "-f", "files.f"]
print(" ".join(parts))vcs -full64 -f files.fOne call, one string, spaces in all three places between the four pieces and nowhere else.
The square brackets on the first line are a list written out directly rather than produced by split() — the same kind of value Chapter 4.8 introduced, holding four strings in order. Module 5 teaches how to build them properly; here it is just a way to have four pieces on hand.
Compare the concatenation you would otherwise write, from Chapter 4.3:
print("vcs" + " " + "-full64" + " " + "-f" + " " + "files.f")vcs -full64 -f files.fSame answer. Seven operands, three of them separators typed out by hand, and one place to accidentally leave a space out. That is the comparison this chapter is about.
2. Why the Separator Owns the Method
Now the thing nobody explains. You wrote:
" ".join(parts)and it looks inside out. The natural instinct is parts.join(" ") — take the pieces and join them with a space. Python refuses that outright:
print(["a", "b"].join(","))AttributeError: 'list' object has no attribute 'join'There genuinely is no join on the pieces. The reason is worth understanding rather than memorising.
join is a string operation because its result is a string, and the separator is the only part of that result it contributes. The pieces supply their own characters; the separator is the thing being inserted. So the method belongs to the separator, and the pieces are what you hand it.
Read it in that order and it says exactly what it does:
print(",".join(["a", "b", "c"]))
print(" | ".join(["a", "b", "c"]))a,b,c
a | b | cseparator.join(pieces) means: put this separator between these pieces.
The separator can be anything, including nothing at all:
print("".join(["a", "b", "c"]))abcAn empty separator puts nothing between the pieces, which glues them together. That is occasionally exactly what you want.
3. Between, Not After
Here is the fact that follows from the previous section and answers most "off by one" surprises: the separator goes between the pieces, so joining n pieces uses n − 1 separators.
p3 = ["a", "b", "c"]
print(",".join(p3))
print(len(",".join(p3)))a,b,c
5Three pieces, two commas, five characters. There is no trailing comma, because there is no fourth piece for it to sit in front of.
The edges confirm the rule rather than contradicting it:
print(",".join(["only"]))
print(repr(",".join([])))only
''One piece gets no separator at all — there is nothing for it to be between. Zero pieces give an empty string — nothing to join, so nothing comes out. Neither is an error, and both are what "between the pieces" has to mean at the boundary.
4. split and join Are Inverses
The two chapters fit together, and it is worth seeing as one movement rather than two techniques:
In code, the round trip returns the original:
line = "axi_smoke 4210 PASS 182.4"
print(" ".join(line.split()) == line)TrueAnd here is where the pair earns its keep. Run the same round trip on the uneven spacing from Chapter 4.8:
messy = "axi_smoke 4210 PASS"
print(repr(" ".join(messy.split())))'axi_smoke 4210 PASS'The runs of spaces became single spaces. split() threw away every run of whitespace, and join put back exactly one space between each pair of pieces — so the line came back normalised.
That one line is the standard way to tidy irregular spacing in Python, and you now have both halves of why it works.
5. What join Refuses
join will only assemble strings. A number among the pieces stops it:
print(",".join(["a", 42]))TypeError: sequence item 1: expected str instance, int foundThe message is precise and worth reading properly: sequence item 1 is telling you which piece was wrong — item 1, counting from zero, so the second one. expected str instance, int found is telling you what was wrong with it.
This is the same refusal as "seed_" + 42 in Chapter 4.3, for the same reason: Python will not silently decide how a number should look as text. The fix is the same too — convert first with str() from Chapter 2.11:
print(",".join(["a", str(42)]))a,42When the pieces come from split() this never arises, because splitting text produces text. It arises when you assemble pieces yourself and one of them is a measured value.
6. Three Things join Is Actually For
A command string, which is the canonical example from Section 1 — several arguments becoming one line.
A separated data row. A comma between the fields and no comma on the end:
print(",".join(["axi_smoke", "4210", "PASS"]))axi_smoke,4210,PASSSeveral lines of a report, using the newline from Chapter 4.2 as the separator:
print("\n".join(["line one", "line two"]))line one
line twoThat last one is the answer to the cost problem Chapter 4.3 raised. Rather than building a report by repeatedly adding to a string — which, because strings are immutable, rebuilds the whole thing every time — you collect the lines and join them once. Module 10 will give you the means to collect many lines; the assembly step is this one, and it does not get slower as the report grows.
7. One Surprise Worth Knowing
join works on anything that supplies pieces in order — and a string supplies its characters in order:
print("-".join("PASS"))P-A-S-SThe four characters of "PASS" were treated as four pieces, with hyphens between them. This is rarely what you want, and it is included because it is a real source of confusion: if you accidentally pass a single string where you meant several, join will not complain — it will quietly space out the characters. A result full of separators between individual letters is the symptom.
8. Predict, Run, Observe
Predict all five before reading on.
print("-".join(["a", "b", "c"]))
print(len("-".join(["a", "b", "c"])))
print("/".join(["runs", "axi_smoke", "sim.log"]))
print(repr(",".join(["solo"])))
print(" ".join("ab"))The verified output:
a-b-c
5
runs/axi_smoke/sim.log
'solo'
a bThe second is 5 — three pieces plus two separators, never three separators. The fourth has no comma anywhere, because one piece needs no separator. The fifth is the Section 7 surprise: "ab" was read as two pieces, a and b, so a space went between them.
If you predicted ab for the last one you expected join to leave a single string alone. It does not; it takes the string's characters as the pieces.
9. Exercises
Exercise 1 — build three strings. Using join, produce each of these from the pieces ["runs", "axi_smoke", "sim.log"]:
(a) runs/axi_smoke/sim.log (b) runs axi_smoke sim.log (c) runsaxi_smokesim.log
Exercise 2 — count the separators. Without running anything, say how many separator characters appear in each result, then verify with len():
(a) ",".join(["a","b","c","d"]) (b) ",".join(["a"]) (c) ",".join([]) (d) "--".join(["a","b","c"])
Exercise 3 — read the error. A colleague's script fails with TypeError: sequence item 2: expected str instance, float found. Say which piece is at fault, counting from where, and give the fix.
Exercise 4 — the inversion. Explain in two sentences why join is written on the separator rather than on the pieces. Then say what error you get for writing it the other way, and why that error is an AttributeError rather than a TypeError.
Exercise 5 — normalise a line. Write one expression that turns " reg_rw FAIL 91.2 " into exactly "reg_rw FAIL 91.2". Then say why you did not need strip().
Exercise 6 — the dangling comma. A colleague builds a CSV row by adding field + "," for each field and then slicing the last character off. Say in two sentences what join does differently, and what breaks in their version when a row has exactly one field.
Exercise 7 — the accidental string. Predict the result of ", ".join("PASS"), run it, and explain in one sentence what join treated as the pieces.
10. Summary
separator.join(pieces)builds one string by putting the separator between the pieces.- The separator owns the method because the separator is the only part of the result that
joincontributes.pieces.join(sep)is anAttributeError. - Read it as "comma, join these", not "join these with a comma".
- Separators go between, so n pieces use n − 1 separators. One piece gets none; zero pieces give
"". There is never a trailing separator. joinis the inverse ofsplit." ".join(line.split())returns the line, and on irregular spacing it returns a normalised version of it — the standard way to tidy whitespace.joinaccepts only strings. A number givesTypeError: sequence item N: expected str instance, int found, and the item number names the offending piece counting from zero. Convert withstr().- Use
joinrather than repeated+for many pieces — it assembles in one step instead of rebuilding the string at each step, which is the cost Chapter 4.3 described. "\n".join(lines)builds a multi-line block, and is how reports get assembled.- Handing
joina single string spaces out its characters rather than raising. A result with separators between individual letters is that mistake.
Splitting and joining cover taking text apart and putting it together. The next chapter asks precise questions about what is inside it, and changes parts of it in place.
Where this fits
Part of the Python curriculum.
