Joining is how you turn a Python list to a string: ", ".join(my_list). The text before .join is the separator, and the list goes inside the parentheses:
groceries = ["apples", "bread", "milk"]
print(", ".join(groceries)) # apples, bread, milk
That covers a list of strings. Numbers, None values and speed each need a little more, and I’ll show each one with real output from Python 3.14.7.

join() puts the separator between items, never after the last one.Convert a list to a string in Python with join()
Here’s the same list joined three ways, plus what str() does to it:
groceries = ["apples", "bread", "milk"]
print(repr("".join(groceries)))
print(repr(" ".join(groceries)))
print(repr(", ".join(groceries)))
print()
print("str(groceries) gives:", str(groceries))
Output:
'applesbreadmilk'
'apples bread milk'
'apples, bread, milk'
str(groceries) gives: ['apples', 'bread', 'milk']

join on, including an empty one."".join() glues the items together with nothing between them, " ".join() puts a space between them, and ", ".join() gives a readable list.
The last line shows why str() is rarely the answer. It gives you the list exactly as Python prints it, brackets and quotes included.
Can you use str() on a list?
You can, but it answers a different question. str(groceries) returns "['apples', 'bread', 'milk']", a text copy of the list’s code-like form.
That’s useful for logging or debugging, where you want to see the structure. For anything a person will read, like a message or a CSV cell, use join().
If you only need to print a list without the brackets, printing a list without brackets covers the other options.
Join a list of numbers into a string
join() only accepts strings, so a list of numbers fails straight away:
order_ids = [1042, 1043, 1044]
try:
", ".join(order_ids)
except TypeError as err:
print("TypeError:", err)
print(", ".join(map(str, order_ids)))
print(", ".join(str(n) for n in order_ids))
print()
prices = [19.99, 5, 42.5]
print(", ".join(f"${p:.2f}" for p in prices))
Output:
TypeError: sequence item 0: expected str instance, int found
1042, 1043, 1044
1042, 1043, 1044
$19.99, $5.00, $42.50

map(str, ...) converts them all.The message tells you exactly what went wrong: sequence item 0: expected str instance, int found. The index points at the first item that isn’t a string.
Join a list of digits into one number
A related trick: turning a list of digits into a single number. Join them as text, then convert:
digits = [2, 0, 2, 6]
year_text = "".join(map(str, digits))
print(repr(year_text), "->", int(year_text) + 1)
Output:
'2026' -> 2027
"".join(map(str, digits)) builds the text "2026", and int() turns it into a number you can do math with.
map(str, order_ids) converts every item on the way in, and a generator expression does the same. I use the f-string version when the numbers need formatting, like the dollar amounts in the last line.
Join with a separator: comma, space, newline or none
The separator can be any string at all, which covers most of the formats people ask for:
states = ["Texas", "Ohio", "Utah"]
print(repr(", ".join(states))) # comma and space
print(repr(",".join(states))) # comma only, CSV-style
print(repr(" | ".join(states))) # any text works
print(repr("".join(states))) # no separator at all
print("\n".join(states)) # one per line
Output:
'Texas, Ohio, Utah'
'Texas,Ohio,Utah'
'Texas | Ohio | Utah'
'TexasOhioUtah'
Texas
Ohio
Utah
A comma with no space gives CSV-style output, and an empty string removes the separator completely. "\n".join() puts each item on its own line.
There’s more on newlines in the Python newline guide.
Join lines and write them to a file
"\n".join() is the quick way to write a list of lines to a file. Just remember the last line:
lines = ["order 1", "order 2", "order 3"]
with open("orders.txt", "w") as f:
f.write("\n".join(lines) + "\n") # join adds no newline after the last line
print(repr(open("orders.txt").read()))
Output:
'order 1\norder 2\norder 3\n'
join() only puts the newline between items, so I add one more at the end. Many tools expect a text file to finish with a newline, and appending to it later then starts on a fresh line.
Put quotes around each item
Sometimes each item needs quotes, for a message or a config value. Wrap each one on the way into join():
states = ["Texas", "Ohio", "Utah"]
print(", ".join(f'"{s}"' for s in states))
print(", ".join(repr(s) for s in states))
Output:
"Texas", "Ohio", "Utah"
'Texas', 'Ohio', 'Utah'
The f-string version always uses double quotes, while repr() uses Python’s own quoting. If you’re tempted to build a SQL IN (...) list this way, don’t; pass the values as query parameters instead, so quotes inside the data can’t break the query.
Split the string back into a list
split() is the reverse of join(), as long as you use the same separator:
states = ["Texas", "Ohio", "Utah"]
text = ",".join(states)
back = text.split(",")
print(repr(text))
print(back)
print("same list again?", back == states)
Output:
'Texas,Ohio,Utah'
['Texas', 'Ohio', 'Utah']
same list again? True
The round trip only works if no item contains the separator itself. For the details of the reverse direction, see splitting a string by comma.
Join a list of characters into a word
A list of single characters joins into a word with an empty separator:
letters = ["P", "y", "t", "h", "o", "n"]
word = "".join(letters)
print(repr(word))
print(list(word) == letters, "- list() splits it back into characters")
Output:
'Python'
True - list() splits it back into characters
list(word) goes the other way, splitting a string into its characters. That pair is common in puzzles and in code that edits one character at a time.
Join only the first few items
For previews, show a few items and summarize the rest. Slicing does the work before join():
states = ["Texas", "Ohio", "Utah", "Iowa", "Maine", "Idaho"]
shown = 3
preview = ", ".join(states[:shown])
extra = len(states) - shown
print(preview + (f" and {extra} more" if extra > 0 else ""))
Output:
Texas, Ohio, Utah and 3 more
states[:shown] takes the first three, and the count of the rest turns into “and 3 more”. It keeps a long list from flooding a message or a table cell.
Why is there no list.join() in Python?
If you’re coming from JavaScript, where it’s array.join(", "), this trips you up at first:
fruits = ["apple", "pear"]
try:
fruits.join(", ") # the JavaScript habit
except AttributeError as err:
print("AttributeError:", err)
print(", ".join(fruits)) # separator first, list inside
Output:
AttributeError: 'list' object has no attribute 'join'
apple, pear
In Python, join belongs to the separator string, not the list. It reads backwards at first, but it lets join accept any iterable, not just lists, as the next sections show.
Java’s String.join(", ", list) is actually the closest match: separator first, then the items.
Join a list with None or mixed types
Real data often mixes strings with numbers and missing values. Here’s what happens:
values = ["Austin", None, "Denver", 2026]
try:
", ".join(values)
except TypeError as err:
print("TypeError:", err)
print("map(str): ", ", ".join(map(str, values)))
print("skip None: ", ", ".join(str(v) for v in values if v is not None))
Output:
TypeError: sequence item 1: expected str instance, NoneType found
map(str): Austin, None, Denver, 2026
skip None: Austin, Denver, 2026

map(str, ...) turns None into the word None; filtering removes it.map(str, values) fixes the error, but look at the result: None became the text None. That’s easy to miss and embarrassing in a report.
Decide what a missing value should become. Filtering it out with if v is not None is usually right, or swap in a placeholder like "N/A".
Join a pandas column into one string
With a pandas column, drop the missing values first or let pandas handle them:
import pandas as pd
orders = pd.DataFrame({"state": ["TX", "OH", None, "UT"]})
print(repr(", ".join(orders["state"].dropna())))
print(repr(orders["state"].str.cat(sep=", ")))
print(repr(orders["state"].str.cat(sep=", ", na_rep="N/A")))
Output:
'TX, OH, UT'
'TX, OH, UT'
'TX, OH, N/A, UT'
.dropna() removes the gap before joining. .str.cat(sep=", ") skips missing values on its own, and na_rep fills them with a placeholder instead.
join() works on tuples, sets, dictionaries and strings
Anything you can loop over works with join(), as long as the items are strings:
print(repr("-".join(("2026", "09", "30")))) # tuple
print(repr(", ".join(sorted({"blue", "red"})))) # set: sort it for a stable order
print(repr(", ".join({"x": 1, "y": 2}))) # dict: joins the KEYS
print(repr(", ".join(f"{k}={v}" for k, v in {"x": 1, "y": 2}.items())))
print(repr("-".join("abc"))) # a string is an iterable of characters
print(repr(", ".join([]))) # empty list -> empty string
Output:
'2026-09-30'
'blue, red'
'x, y'
'x=1, y=2'
'a-b-c'
''

A set has no fixed order, so I sort it first to get the same string every run. A dictionary joins its keys; use .items() when you need the values too.
The surprising one is a plain string. "-".join("abc") treats it as a sequence of characters and gives a-b-c, which is handy for formatting codes.
Join a list into a sentence with commas and “and”
For text people will read, a bare comma list isn’t quite right. American English style puts “and” before the last item, usually with a serial comma:
def human_list(items):
if not items:
return ""
if len(items) == 1:
return items[0]
if len(items) == 2:
return f"{items[0]} and {items[1]}"
return ", ".join(items[:-1]) + ", and " + items[-1]
for colors in (["red"], ["red", "white"], ["red", "white", "blue"]):
print(human_list(colors))
Output:
red
red and white
red, white, and blue
The function handles one and two items separately, since “red and white” shouldn’t get a comma. For three or more, it joins all but the last with commas and adds “, and”.
join() vs + vs sum(): which is fastest?
You can build a string with += in a loop, but it’s the slow way:
import timeit
setup = "parts = [str(i) for i in range(100000)]"
join_time = min(timeit.repeat("''.join(parts)", setup=setup, number=50, repeat=3))
loop_time = min(timeit.repeat("s = ''\nfor p in parts: s += p", setup=setup, number=50, repeat=3))
print(f"''.join(parts): {join_time:.3f} s")
print(f"+= in a loop: {loop_time:.3f} s")
print(f"the loop was {loop_time / join_time:.0f}x slower here")
print()
try:
sum(["a", "b", "c"], "")
except TypeError as err:
print("sum() ->", err)
Output:
''.join(parts): 0.033 s
+= in a loop: 0.621 s
the loop was 19x slower here
sum() -> sum() can't sum strings [use ''.join(seq) instead]

join() beat the += loop by a wide margin, and sum() points you to join().join() works out the final size once and copies each piece a single time. The loop builds a new string on every step, so it falls further behind as the list grows.
sum() won’t even try: its error message tells you to use ''.join(seq) instead. When Python’s own error suggests a function, that’s a strong hint.
Convert a list of lists to a string
Nested lists need two joins, one inside the other:
cities = [["New", "York"], ["San", "Diego"], ["Des", "Moines"]]
print(", ".join(" ".join(parts) for parts in cities))
print()
rows = [["name", "state"], ["Ava", "TX"], ["Liam", "OH"]]
print("\n".join(",".join(row) for row in rows))
Output:
New York, San Diego, Des Moines
name,state
Ava,TX
Liam,OH
The inner " ".join(parts) builds each city name, and the outer join puts commas between them. The second example builds a small CSV the same way, one row per line.
Be careful not to confuse this with combining lists themselves. Concatenating multiple lists is about merging lists into one list, not into a string.
If you work with lists and strings a lot, these are worth a look next:
- Concatenate strings in Python
- Convert a comma-separated string to a list
- Split a string by space
- Python list comprehension
Frequently asked questions
How do I convert a list to a string in Python?
Use join(): ", ".join(my_list). The string before .join is placed between the items.
How do I join a list of numbers?
Convert the items first: ", ".join(map(str, numbers)). Joining numbers directly raises TypeError.
Why does str(list) keep the brackets?
str() returns the list’s printed form, brackets and quotes included. Use join() for readable text.
How do I join a list without commas or spaces?
Join with an empty string: "".join(my_list).
Why is there no list.join() method?
join is a string method, so you call it on the separator: ", ".join(my_list). That lets it accept any iterable.
Is join() faster than using + in a loop?
Yes. In my test a += loop over 100,000 strings was many times slower than a single join().
What does join() do with None values?
It raises TypeError. map(str, ...) turns None into the text None, so filter missing values out first. The details are in the Python str.join() documentation.

Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn