Python split() and join(): strings to lists and back
Python split() turns a string into a list and join() turns it back. Why split() and split(' ') differ, plus maxsplit, partition, and when to use csv.
In Python, str.split() breaks a string into a list of smaller strings, and str.join() glues a list of strings back into one:
words = "red green blue".split()
print(words) # ['red', 'green', 'blue']
print(", ".join(words)) # red, green, blue
With no argument, split() splits on any run of whitespace and ignores whitespace at the ends. With a separator, such as split(","), it splits on exactly that text, every time it appears. join() works the other way round from what most people expect: you call it on the separator and pass the list in.
Those two sentences cover most uses. The rest of this post covers the differences that cause real bugs, starting with the one between split() and split(" "). Every output below was measured in Pyodide running Python 3.13.2, the same runtime Devpuff's Python lessons use.
split() vs split(" "): the difference that matters most
text = " hello big world "
print(text.split()) # ['hello', 'big', 'world']
print(text.split(" ")) # ['', '', 'hello', '', '', 'big', '', 'world', '']
Same string, 3 items against 9. These are two different algorithms that happen to share a name:
split()with no argument (orsplit(None)) treats any run of whitespace as a single separator and drops whitespace at the start and end. It never returns empty strings.split(" ")splits on every single space. Two spaces in a row produce an empty string between them, and a space at either end produces an empty string at that end.
The no-argument form also counts tabs, newlines and other whitespace characters, including the non-breaking space that sneaks in when text is copied from a web page:
print("a\tb\nc d".split()) # ['a', 'b', 'c', 'd']
The two forms disagree about empty input too:
print("".split()) # []
print(" ".split()) # []
print("".split(",")) # ['']
An empty string split on a comma is a list with one empty string in it, not an empty list. So len(line.split(",")) is 1 for a blank line, and code that counts comma-separated values reports one value where there are none. The no-argument form returns [] for empty or blank input, which is what you want for counting words.
The rule: for words in human text, use split() with no argument. Use an explicit separator only for data with a fixed delimiter, where an empty field means something.
A neat side effect of the no-argument form is a one-line whitespace cleaner:
print(" ".join(text.split())) # hello big world
Split on any whitespace, join with single spaces, and every run of spaces, tabs and newlines collapses to one space.
Split on a separator: maxsplit and rsplit
With an explicit separator, every occurrence counts, including ones next to each other or at the ends:
print("a,b,,c".split(",")) # ['a', 'b', '', 'c']
print(",a,".split(",")) # ['', 'a', '']
The second argument, maxsplit, limits how many splits happen. The remainder stays intact in the last item:
print("a,b,c,d".split(",", 1)) # ['a', 'b,c,d']
print("a,b,c,d".split(",", maxsplit=2)) # ['a', 'b', 'c,d']
maxsplit=N gives you at most N + 1 items. That "keep the rest" behaviour is different from JavaScript, where the equivalent argument cuts off the remainder and throws it away; split() and join() in JavaScript shows what that does to a key-value pair.
rsplit() does the same thing starting from the right, which matters as soon as maxsplit is involved:
print("a,b,c,d".rsplit(",", 1)) # ['a,b,c', 'd']
print("archive.tar.gz".rsplit(".", 1)) # ['archive.tar', 'gz']
File extensions are the classic use. Splitting "archive.tar.gz" from the left with maxsplit=1 would give 'tar.gz' as the extension; splitting from the right gives the last one.
partition() when you want exactly two parts
Splitting a key=value line once and unpacking the result is a common pattern:
key, value = "key=val=ue".split("=", 1)
print(key, value) # key val=ue
It breaks the moment a line has no separator:
key, value = "novalue".split("=", 1)
# ValueError: not enough values to unpack (expected 2, got 1)
partition() always returns a tuple of exactly three items: the part before the separator, the separator itself, and the part after. When the separator is missing, the last two are empty strings rather than an error:
print("key=val=ue".partition("=")) # ('key', '=', 'val=ue')
print("novalue".partition("=")) # ('novalue', '', '')
So parsing a config line becomes safe without a try:
key, sep, value = "novalue".partition("=")
if not sep:
print("no value for", key) # no value for novalue
rpartition() is the same thing, splitting on the last occurrence instead of the first.
Split a string into characters
You cannot split on an empty string:
"abc".split("")
# ValueError: empty separator
Use list() instead, which iterates the string one character at a time:
print(list("abc")) # ['a', 'b', 'c']
print(list("hi 😀")) # ['h', 'i', ' ', '😀']
Python strings are sequences of Unicode code points, so a plain emoji stays whole. That is a real advantage over JavaScript, where the equivalent split("") cuts an emoji in half. Characters built from several code points, such as flags and skin-tone emoji, still come apart into their pieces.
Often you do not need a list at all. A for loop already walks a string character by character.
Split on several delimiters with re.split
str.split() takes one separator. For several, or for a pattern, use re.split():
import re
print(re.split(r"[,;]\s*", "a, b;c")) # ['a', 'b', 'c']
print(re.split(r"\d", "a1b2c3")) # ['a', 'b', 'c', '']
print(re.split(r"(,)", "a,b")) # ['a', ',', 'b']
Three details from those lines:
- The pattern
[,;]\s*splits on a comma or a semicolon and swallows any spaces after it, so' b'does not come back with a leading space. - When the string ends with a separator, you get a trailing empty string, exactly as with
str.split(). - Parentheses in the pattern capture the separator and keep it in the result.
Write the pattern as a raw string, with the r prefix. Without it, Python 3.12 and later print SyntaxWarning: invalid escape sequence '\d' the moment the file runs. It still works today, but the warning is the first step towards it becoming an error.
splitlines() vs split("\n")
text = "a\r\nb\nc\n"
print(text.split("\n")) # ['a\r', 'b', 'c', '']
print(text.splitlines()) # ['a', 'b', 'c']
splitlines() fixes two things at once. It understands every line ending, including the \r\n Windows uses, so no stray \r is left on the end of a line. And it does not produce an empty item after a final newline, which almost every text file has.
Pass keepends=True if you need the line endings kept:
print("a\nb\n".splitlines(keepends=True)) # ['a\n', 'b\n']
join(): turn a list back into a string
print(", ".join(["a", "b", "c"])) # a, b, c
print("".join(["a", "b", "c"])) # abc
print("-".join("abc")) # a-b-c
join is a string method. The separator is the object you call it on, and the list is the argument. Lists do not have a join method, and trying it the JavaScript way fails:
["a", "b"].join(",")
# AttributeError: 'list' object has no attribute 'join'
The design makes sense once you notice that join accepts any iterable of strings: a list, a tuple, a generator, even another string, as the "-".join("abc") line shows. Putting join on str means one implementation that works for all of them, rather than a copy on every container type.
join only accepts strings
", ".join([1, 2, 3])
# TypeError: sequence item 0: expected str instance, int found
JavaScript's join converts numbers silently; Python's refuses. Convert first:
print(", ".join(map(str, [1, 2, 3]))) # 1, 2, 3
When the numbers need formatting, a generator with an f-string does both jobs at once:
prices = [4.5, 12, 0.99]
print(", ".join(f"${p:.2f}" for p in prices)) # $4.50, $12.00, $0.99
Why join beats += in a loop
Building a string piece by piece with += looks natural, and it is much slower than collecting the pieces and joining once:
| Method (100,000 words, 20 runs) | Time |
|---|---|
" ".join(words) |
25ms |
out += w + " " in a loop |
221ms |
That is close to nine times slower, measured in Pyodide. Strings are immutable, so += can end up building a new string on every pass, while join works out the total length once and fills it in a single copy. The exact ratio varies between Python builds, but the advice does not: collect the pieces in a list and call join at the end.
Everyday split and join recipes
Read numbers typed on one line.
numbers = [int(x) for x in "1 2 3".split()]
print(numbers) # [1, 2, 3]
list(map(int, "1 2 3".split())) gives the same result. Use the comprehension when you also want to filter or transform.
Split and strip in one go. Data typed by people has spaces around the separators. Strip each piece as you split:
print([p.strip() for p in "a, b ,c".split(",")]) # ['a', 'b', 'c']
Drop empty items. Add a condition to the comprehension. Strip inside the condition too if a piece can be only spaces:
print([p for p in "a,,b,".split(",") if p]) # ['a', 'b']
print([p.strip() for p in "a, , b".split(",") if p.strip()]) # ['a', 'b']
Split into chunks of a fixed length. split cuts on separators, so it cannot split "abcdefg" into groups of three. Slicing in steps can:
s = "abcdefg"
print([s[i:i + 3] for i in range(0, len(s), 3)]) # ['abc', 'def', 'g']
range() with a step of 3 gives the start of each chunk, and the slice takes three characters from there. The last chunk is shorter when the length does not divide evenly, and slicing past the end of a string never raises.
Count words. len(text.split()) counts words correctly however many spaces are between them, and returns 0 for an empty string.
Pair a header row with a data row.
header = "name,age,city"
row = "Ada,36,London"
print(dict(zip(header.split(","), row.split(","))))
# {'name': 'Ada', 'age': '36', 'city': 'London'}
zip() pairs the two lists up item by item, and dict() turns the pairs into a dictionary. That works for this row; the next section shows the row where it breaks.
When split is the wrong tool: CSV
row = 'Ada,"Hello, world"'
print(row.split(",")) # ['Ada', '"Hello', ' world"']
CSV allows commas inside quoted fields, and split knows nothing about quotes. One comma in an address or a comment shifts every later column. The standard library's csv module handles quoting correctly:
import csv
import io
print(next(csv.reader(io.StringIO(row)))) # ['Ada', 'Hello, world']
In real code you pass an open file straight to csv.reader and skip the StringIO. The same principle applies elsewhere: use urllib.parse for query strings, pathlib for file paths, and shlex.split() for shell-style commands. Each format has its own quoting rules, and split follows none of them. The Python docs for str.split list the exact whitespace rules if you need them.
Quick reference
s.split() # words; any whitespace; never empty items
s.split(",") # every comma; empty items kept
s.split(",", 1) # first comma only; rest kept intact
s.rsplit(".", 1) # last dot only
before, sep, after = s.partition("=") # never raises
list(s) # characters
re.split(r"[,;]\s*", s) # several separators
s.splitlines() # lines, any line ending
", ".join(items) # strings only
", ".join(map(str, numbers)) # anything else
" ".join(s.split()) # collapse whitespace
Want to practise string methods in lessons where the Python runs in your browser? Start the Python track.
More from the blog
What if __name__ == '__main__' actually does in Python
In Python, if __name__ == '__main__' runs code only when you run the file directly, not when it is imported. What __name__ holds and when you need it.
Read moreHow to reverse a list in Python (and a string)
To reverse a list in Python, use reverse() to change it in place, [::-1] for a reversed copy, or reversed() to loop backwards. Which to use and why.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.