How to Join Strings in Python

How to Join Strings in Python

by Martin Breuss Feb 10, 2025 0 Comments basics python

Python’s built-in string method .join() lets you combine string elements from an iterable into a single string, using a separator that you specify. You call .join() on the separator, passing the iterable of strings to join.

By the end of this tutorial, you’ll understand that:

  • You use .join() in Python to combine string elements with a specified separator.
  • A separator is the piece of text you want inserted between each substring.
  • To join list elements, you call .join() on a separator string, passing the list as the argument.
  • .join() inserts the separator between each list element to form a single string.
  • The .join() method returns a new string that is the concatenation of the elements in the iterable, separated by the specified string.
  • For smaller string concatenation tasks, you can use the concatenation operator (+) or f-strings instead of .join().

Python’s built-in str.join() method gives you a quick and reliable way to combine multiple strings into a single string. Whether you need to format output or assemble data for storage, .join() provides a clean and efficient approach for joining strings from an iterable.

In the upcoming sections, you’ll learn the basic usage of .join() to concatenate strings effectively. You’ll then apply that knowledge to real-world scenarios, from building CSV files to constructing custom log outputs. You’ll also discover some surprising pitfalls and learn how to avoid them.

Take the Quiz: Test your knowledge with our interactive “How to Join Strings in Python” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

How to Join Strings in Python

Test your understanding of Python's .join() string method for combining strings, handling edge cases, and optimizing performance.

How to Join Strings in Python Using .join()

To use the string method .join(), you call .join() on a separator string and pass an iterable of other strings as the argument. The method returns a single string, where it has inserted the separator string between each element of the iterable:

Python
>>> words = ["Time", "flies", "like", "an", "arrow!"]
>>> " ".join(words)
'Time flies like an arrow!'

In this example, you joined a list of words into one sentence, separated by spaces.

At first glance, this usage might look a little backward. In many other string operations, you call the method on the main string that you want to manipulate. However, with .join(), you call the method on the separator string, then pass the iterable of strings that you want to combine:

Python
>>> separator = " "
>>> separator.join(words)
'Time flies like an arrow!'

This example achieves the same result as the earlier one but splits the process into two steps. Defining separator separately makes the code more readable and avoids the potentially odd-looking syntax of calling .join() directly on a short string literal.

You rarely see code that’s written in multiple steps where you assign the separator string to a variable, like you did in the example above.

In typical usage, you call .join() directly on the separator string, all in one line. This approach is more concise and highlights that any valid string can be your separator, whether it’s whitespace, a dash, or a multicharacter substring.

Join With an Empty String

What if you don’t want any separator at all, but just want to concatenate the items? One valid approach is to use an empty string ("") as the separator:

Python
>>> letters = ["A", "B", "C", "D"]
>>> "".join(letters)
'ABCD'

This code snippet concatenates the letters in the list, forming a single string "ABCD". Using an empty string as the separator is a great way to assemble strings without a delimiter between them.

Combine Strings of Characters

Since .join() can take any iterable of strings—not just lists—you can even pass a string as an argument. Because strings are iterable, Python iterates over each character in that string, considering each character as a separate element:

Python
>>> characters = "ABCD"
>>> ",".join(characters)
'A,B,C,D'

By calling .join() on "," and passing the string characters, you effectively place a comma between every single character in "ABCD". This might not always be what you intend, but it’s a neat trick to keep in mind if you ever need to treat each character as a separate element.

Next, you’ll continue to explore using different separators when joining strings with .join().

Join Elements of an Iterable With Specific Separators

In many real-world scenarios, you’ll want to join data with a specific delimiter. For example, when creating rows of CSV data, you could join all row elements using a single comma. Or, when you’re preparing data for display, you may want to use a tab character to make it more readable.

Create Comma-Delimited Output

Suppose you have a list of fruit names that you want to pack into a single string with each fruit separated by a comma. You can handle this in one line:

Python
>>> fruits = ["apple", "orange", "lemon", "kiwi"]
>>> csv_line = ",".join(fruits)
>>> print(csv_line)
apple,orange,lemon,kiwi

If you save or print this data to a file, you can handle it as a line in a basic CSV format. This approach is quick and efficient. However, keep in mind that if you need to handle quotes or complex CSV structures, then you should use Python’s dedicated csv library.

Collapse a List of URL Parts

Another use case for .join() is constructing or normalizing URL paths. Often, you’ll have a base domain and a list of subpaths that you want to concatenate with a slash (/):

Python url_builder.py
base_url = "https://example.com/"
subpaths = ["blog", "2025", "02", "join-string"]

full_url = base_url + "/".join(subpaths)
print(full_url)

In this snippet, you start with a base URL, then use "/".join() to handle the rest. When you run the script, you’ll get a fully-formed URL as output:

Shell
$ python url_builder.py
https://example.com/blog/2025/02/join-string

If you need to handle more advanced URL operations, then you can also use Python’s urllib library, specifically the parse module, for robust URL manipulation.

Use Multicharacter Separators

Your separator doesn’t have to be a single character. You can also call .join() on a longer string if the delimiter you want has more than one character:

Python
>>> colors = ["red", "green", "blue"]
>>> " | ".join(colors)
'red | green | blue'

This code snippet uses the three-character string " | " as a delimiter.

Such a use case might make your output more visually appealing or help you parse the string quicker by picking a unique delimiter.

Because .join() doesn’t place any restrictions on the separator’s length or content, you’re free to choose any delimiter that best suits your data and user-facing output:

Python
>>> running_instructions = ("ready", "set", "go")
>>> "...".join(running_instructions)
'ready...set...go'

Joining values with .join() is one of the cleanest ways to convert a list of strings into a single output string. By calling .join() on the delimiter that you need—whether it’s a single character, a multicharacter substring, or even an empty string—you can unify your data in a single step.

Detangle Surprising Errors When Using .join()

You may occasionally run into surprising issues when attempting to join strings with .join(). Some common mistakes include mixing data types, or calling .join() on the main string instead of the separator. In this section, you’ll take a look at these pitfalls to save you time debugging.

Non-String Values in the Iterable

The most common error you’ll encounter when using .join() is a TypeError caused by trying to join an iterable that contains non-string values, such as integers or floats:

Python
>>> countdown = [3, 2, 1]
>>> "-".join(countdown)
Traceback (most recent call last):
  ...
TypeError: sequence item 0: expected str instance, int found

Python complains that the first element is an integer, not a string. The .join() method only works with strings, and it won’t automatically convert values to strings for you.

If you’re confronted with a situation where you want to join objects that aren’t strings, you can explicitly convert each element to a string beforehand using a list comprehension or a generator expression and str():

Python
>>> countdown = [3, 2, 1]
>>> "-".join(str(number) for number in countdown)
'3-2-1'

Alternatively, you can use a more functional style with Python’s map() function:

Python
>>> countdown = [3, 2, 1]
>>> "-".join(map(str, countdown))
'3-2-1'

Both approaches ensure that the resulting iterable you pass to .join() consists only of strings. Without these explicit conversions, Python will raise a TypeError because .join() doesn’t transform objects into strings automatically.

Non-Iterable Types

You can only pass an iterable object as an argument to .join(). If you pass a non-iterable object, such as None or an integer, then Python will raise an error:

Python
>>> "-".join(42)
Traceback (most recent call last):
  ...
TypeError: can only join an iterable

Here, Python complains that 42 isn’t an iterable, so .join() can’t loop over it and Python raises a TypeError.

If you’ve successfully done something like ",".join("ABCD") in the past—passing a single string to .join()—then you might assume that passing just one integer or float would work the same way. A string may look like a single value, but Python treats it as a sequence of characters, which means that it’s iterable.

Numbers, on the other hand, don’t implement the iterable protocol. As a result, passing them as arguments to .join() causes Python to raise a TypeError.

.join() Called on the Wrong Object

When you’re used to calling methods like .split() on the main string, it can feel counterintuitive that you need to call .join() on the separator. When you accidentally reverse that order, you’ll get an unexpected result:

Python
>>> "Hello World!".join(",")
','

In this snippet, you’re calling .join() on "Hello World!", but passing just "," as the iterable. A string is an iterable of its characters, so .join() will attempt to place the separator string "Hello World!" in between every character of the "," string.

However, that string only consists of a single character, so there’s no place to insert the separator. Therefore, .join() returns a new string that only consists of ",". This result may be surprising!

If you want to insert a single comma between each of the characters that make up the string "Hello, World!", then you need to swap the order of the strings involved:

Python
>>> ",".join("Hello World!")
'H,e,l,l,o, ,W,o,r,l,d,!'

This confusion is especially common when you’re passing a single string as the argument to .join(), like in the example above. It’s a good idea to double-check that you always use "sep".join(iterable_of_strings) rather than accidentally reversing the order.

By understanding these pitfalls, you can avoid common mistakes when using .join(). Whether it’s converting non-string types or passing the correct iterable, a little awareness can save you time and frustration while debugging.

Consider the + Operator or F-Strings for Simple Scenarios

In some cases, you may not need to use .join(), like when you’re only concatenating a small number of strings. For example, using the plus operator (+) or f-strings might do just fine:

Python
 1>>> first_name = "Kai"
 2>>> last_name = "Nguyen"
 3
 4>>> first_name + " " + last_name
 5'Kai Nguyen'
 6>>> f"{first_name} {last_name}"
 7'Kai Nguyen'

The example above shows two different ways to create a new, larger string from the strings first_name and last_name:

For short examples like this, both approaches are often more readable than using .join():

Python
>>> " ".join([first_name, last_name])
'Kai Nguyen'

All three ways work and produce the same output, but if you’re only joining a few strings, then it’s best to focus on readability and use an f-string.

For large tasks or in loops, .join() remains more efficient and expressive. Its true power becomes evident when you have a list or other iterable of unknown length, or when you’re dealing with hundreds or thousands of strings. In these scenarios, .join() is the tool of choice for writing Pythonic code.

Create an Event Log String: Practical Example

At this point, you’ve learned how to use .join() with short toy examples. Now it’s time to tie everything together with a small real-world use case.

Imagine that you have access to JSON event logs from an application. Each key is a timestamp and the value is a list of event names with a varying amount of elements. You want to build a single, human-readable string from it that contains all your events in chronological order:

JSON event_log.json
{
    "2025-01-24 10:00": ["click", "add_to_cart", "purchase"],
    "2025-01-24 10:05": ["click", "page_view"],
    "2025-01-24 10:10": ["page_view", "click", "add_to_cart"]
}

You decide to create an output string with one line per timestamp. On each line, you want a comma-separated list of event names, prefixed by the timestamp. Using .join(), you can elegantly handle the formatting:

Python format_event_log.py
 1import json
 2
 3def load_log_file(file_path):
 4    with open(file_path, mode="r", encoding="utf-8") as file:
 5        return json.load(file)
 6
 7def format_event_log(event_log):
 8    lines = []
 9    for timestamp, events in event_log.items():
10        # Convert the events list to a string separated by commas
11        event_list_str = ", ".join(events)
12        # Create a single line string
13        line = f"{timestamp} => {event_list_str}"
14        lines.append(line)
15
16    # Join all lines with a newline separator
17    return "\n".join(lines)
18
19if __name__ == "__main__":
20    log_file_path = "event_log.json"
21    event_log = load_log_file(log_file_path)
22    output = format_event_log(event_log)
23    print(output)

Running format_event_log.py produces readable output, formatted to your liking:

Shell
$ python format_event_log.py
2025-01-24 10:00 => click, add_to_cart, purchase
2025-01-24 10:05 => click, page_view
2025-01-24 10:10 => page_view, click, add_to_cart

This example demonstrates how .join() helps you at multiple stages of the process—and also when it’s better to opt for a different approach when you need to concatenate strings:

  • Line 11 uses .join() to combine individual events with a comma separator.

  • Line 13 uses an f-string to assemble each line of the output. Because there are always only two variables to combine, the f-string is more readable than using .join().

  • Line 17 uses .join() another time to unify all lines into one final string with a newline separator between each line.

Your data remains readable and it’s straightforward to parse. Using .join() for string concatenation helps keep your code flexible and explicit about how each part of the output is structured. It also allows the implementation to grow and stay performant, even when handling a large number of log events per timestamp.

If you want to store the output in a text file for further analysis, then you can adapt the final step by writing the output to a file:

Python format_event_log.py
# ...

if __name__ == "__main__":
    log_file_path = "event_log.json"
    event_log = load_log_file(log_file_path)
    output = format_event_log(event_log)
    with open("event_log.txt", mode="w", encoding="utf-8") as file:
        file.write(output)

That’s it! You’ve built a readable string from multiple parts using .join(). This example highlights how beneficial .join() can be for real-world text processing tasks.

If you’re curious about how .join() manages to do its work so efficiently, and you don’t mind peeking at CPython’s source code, then keep reading the next section.

Understand How str.join() Optimizes String Concatenation

So far, you’ve learned that .join() is efficient and useful when you need to join a large amount of strings. But why is that, and why are the other approaches that you’ve seen less performant?

When you build a string by repeatedly adding to it—such as using += in a loop—each operation creates a new string and copies the existing data. This can lead to a quadratic-time cost if you have many strings or large data. In contrast, .join() makes two passes over your data and does only a single allocation. This makes it faster and more memory-friendly when you’re combining many substrings.

In this section, you’ll take a dip into CPython’s source code to better understand how .join() manages to join strings without much overhead.

Pre-Pass: Check Types and Calculate Needed Space

Before doing any other work, CPython performs a pre-pass that accomplishes two things:

  1. Type Check: It checks whether all elements are strings.
  2. Space Allocation: It calculates the total size of all items in the iterable, including the cumulative size of the separator, to create a new string with exactly the right size to fit all the pieces.

You can check out the actual implementation in the CPython source code in the unicodeobject.c file. There, you can find a function named _PyUnicode_JoinArray() that handles the low-level logic for PyUnicode_Join().

Below is a simplified snippet that shows the type check, and how the function calculates the total required size for the final joined string:

C
 1sz = 0;
 2seqlen = PySequence_Fast_GET_SIZE(seq);
 3items = PySequence_Fast_ITEMS(seq);
 4
 5for (i = 0; i < seqlen; i++) {
 6    item = items[i];
 7    if (!PyUnicode_Check(item)) {
 8        /* If an element isn't a string, handle the error */
 9    }
10    Py_ssize_t itemlen = PyUnicode_GET_LENGTH(item);
11    sz += itemlen;
12}
13if (seqlen > 1) {
14    sz += seplen * (seqlen - 1);
15}

Remember that this is a simplified version of the original CPython code. You won’t find it just like this in the source code, but you can search for the variable names used above to see how they fit into the actual implementation.

Understanding what this code snippet does, however, can help when reading the actual source code:

  • Line 1 defines the variable sz, short for size, and sets it to 0. You’ll update this variable to store the length of the final string that the join operation will produce.

  • Lines 2 and 3 use specialized CPython functions to determine the size and collect the elements of the iterable seq. Note that seq isn’t defined in the simplified code snippet above, but you’ll find it as a parameter in the original function in the source code.

  • Lines 5 to 12 make up a loop that iterates over the items of seq, checks whether they’re valid strings that CPython can concatenate, calculates the length of each element, and adds that length to sz, incrementing it accordingly.

  • Lines 13 to 15 conditionally add the length of all necessary separators to sz. Because .join() adds the separator in between elements, you’ll need seqlen - 1 separators. This calculation is only relevant if there’s more than one item in seq and if you used a separator that’s larger than 0 characters. Note that the simplified code snippet uses the variable seplen, which exists in the source code but isn’t defined above.

Using a similar but significantly more sophisticated approach, the real _PyUnicode_JoinArray() function determines the size of the string that the join operation will build. And because the runtime knows the total size (sz) beforehand, it can allocate exactly the required space for the final output when creating the initially empty, new string element res:

C
/* ... */

res = PyUnicode_New(sz, maxchar);

Creating a new object with the exact size requirements sets the stage for why .join() can be so efficient when you need to concatenate many items.

Second Pass: Copy Once and Insert Separators

After allocating the memory, CPython copies each string into the final buffer in order, inserting the separator as needed. Because of the setup work done during the pre-pass, it does this without needing to constantly re-create new objects.

Below you’ll find another simplified excerpt of the relevant CPython source code that you can study to better understand the high-level logic in _PyUnicode_JoinArray():

C
 1/* ... */
 2
 3res_data = PyUnicode_1BYTE_DATA(res);
 4sep_data = PyUnicode_1BYTE_DATA(sep);
 5
 6for (i = 0; i < seqlen; i++) {
 7    item = items[i];
 8    Py_ssize_t itemlen = PyUnicode_GET_LENGTH(item);
 9    memcpy(res_data,
10           PyUnicode_DATA(item),
11           itemlen);
12    res_data += itemlen;
13
14    if (i < seqlen - 1) {
15        memcpy(res_data,
16               sep_data,
17               seplen);
18        res_data += seplen;
19    }
20}

Again, you won’t find this simplified code snippet in CPython’s source code, but it can be helpful to better understand the process that makes .join() efficient.

Assume that the variables from the previous step, res, sep, items, seqlen, and seplen are still accessible in this code:

  • Line 3 creates a pointer to the raw data buffer of res. CPython later uses this value to keep track of where to insert the data.

  • Line 4 does the same for the separator, sep. CPython uses this pointer to insert the separator’s data into the new string.

  • Line 6 starts a loop that iterates over each item, line 7 fetches the current item, and line 8 calculates its length again.

  • Lines 9 to 11 use memcpy() to insert the item’s data into the correct location in res, using the res_data pointer as the destination and itemlen to determine the size.

  • Line 12 advances the res_data pointer by the length of the item.

  • Line 14 checks whether you still need a separator, because you don’t need to add a separator after the last item of the iterable.

  • Lines 15 to 19 then insert the separator, again using memcpy(), and advance the pointer accordingly.

After this pass over the elements in the iterable, res contains the data of all the elements interspersed with as many separators as necessary.

Using this two-step approach, CPython only needs to create one new string, removing the cost of making multiple partial copies. Further, because res is already the right size, CPython can directly copy each piece into the final position.

Compare .join() to Using += in a Loop

Now, consider using the augmented concatenation operator (+=) inside a for loop as another approach to joining strings:

Python plus_loop_concatenation.py
cities = ["Hanoi", "Adelaide", "Odessa", "Vienna"]
separator = "->"

travel_path = ""
for i, city in enumerate(cities):
    travel_path += city
    if i < len(cities) - 1:
        travel_path += separator

print(travel_path)

Here, each iteration needs to reallocate the result string, travel_path, to a new string object, copying previous data again and again. This results in a worst-case O(n²) time complexity.

The .join() method, on the other hand, runs in roughly O(n), because it examines all items first to calculate the total length, allocates once, then copies each substring and separator exactly once. Additionally, the code looks a lot more readable:

Python join_concatenation.py
cities = ["Hanoi", "Adelaide", "Odessa", "Vienna"]
travel_path = "->".join(cities)
print(travel_path)

The code in join_concatenation.py produces the same result as the code in plus_loop_concatenation.py. But it’s also more straightforward and more performant and scalable!

For a few short concatenations, you can use +, or f-strings. But once you’re handling an iterable of unknown length or find yourself joining strings in a loop, .join() remains the preferred and Pythonic approach.

Conclusion

Python’s built-in .join() method is a powerful solution for assembling an iterable of strings into a single string. It’s more expressive and often more efficient than repeatedly using the plus operator, especially when you’re working with many pieces of text. Whether you’re formatting user output, generating CSV lines, or constructing custom logs, .join() offers a clean, Pythonic way to handle string concatenation.

In this tutorial, you learned how to:

  • Use .join() to combine a list of strings into one
  • Join on different delimiters such as commas, spaces, slashes, or even empty strings
  • Handle common pitfalls, such as attempting to join non-string data
  • Write code for a real-world scenario building user-facing logs with .join()
  • Dive deeper into CPython source code to understand how .join() optimizes the concatenation process

With all of this in mind, you’re now well-equipped to bring together all your text data using Python. What a happy reunion! To continue honing your string manipulation skills, you can check out how to split strings in Python with .split() for handling incoming data. Additionally, you can use .strip() to remove unwanted characters, and .replace() to swap out characters as needed.

As you keep working with text data, you’ll discover that .join() is a helpful method for building maintainable code that gracefully handles string concatenation.

Frequently Asked Questions

Now that you have some experience with joining strings using .join() in Python, you can use the questions and answers below to check your understanding and recap what you’ve learned.

These FAQs are related to the most important concepts you’ve covered in this tutorial. Click the Show/Hide toggle beside each question to reveal the answer.

You use .join() to concatenate an iterable of strings into a single string, inserting a separator string between each element of the iterable.

The string method .join() returns a new string element that’s a single string consisting of the elements of the iterable separated by the specified separator.

To join list elements with a specific separator, call .join() on the separator string. You then need to pass the list as an argument to combine the elements with the chosen separator. For example, "...".join(["ready", "set", "go"]) joins the three list elements "ready", "set" and "go" with three dots ("...") in between them, returning the new string element "ready...set...go".

For simple concatenation of a few strings, you can use the concatenation operator (+) or string interpolation with f-strings. However, .join() is more efficient for combining many strings or an iterable with a variable amount of strings.

The string method .join() is designed so that the separator itself calls the method. You then pass the list—or another iterable of strings—as the argument. For example, ",".join(items) tells Python that it should take all elements in items and join them using a comma. This may feel somewhat reversed compared to methods like .split(), but because it’s a string method, you need to call it on a single string object.

In this case, you’ll encounter a TypeError because .join() can only concatenate strings. You can avoid this by converting non-string elements to strings, like with [str(item) for item in my_list] or map(str, my_list).

Yes! If you pass a string as the argument to .join(), then Python iterates over its characters. So "!".join("ABC") results in "A!B!C". This can be handy when you need to insert a separator between every character in a string.

Yes, .join() processes the iterable in the same order that it appears in your data structure. It doesn’t rearrange or sort the elements.

Take the Quiz: Test your knowledge with our interactive “How to Join Strings in Python” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

How to Join Strings in Python

Test your understanding of Python's .join() string method for combining strings, handling edge cases, and optimizing performance.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Martin Breuss

Martin likes automation, goofy jokes, and snakes, all of which fit into the Python community. He enjoys learning and exploring and is up for talking about it, too. He writes and records content for Real Python and CodingNomads.

» More about Martin

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!