How to Insert Substrings at Specific Indexes in Python
String manipulation is a core skill in Python programming, and inserting text at specific positions is a common requirement for data formatting, template generation, and text processing. Since Python strings are immutable, meaning they cannot be modified after creation, inserting a substring requires constructing a new string from the original parts.
In this guide, you will learn how to insert text at any position within a string using slicing, f-strings, reusable functions, and techniques for handling multiple insertions efficiently.
String Slicing and Concatenationā
The most straightforward approach uses slicing to split the original string at the target index, then concatenates the pieces with the new content in between:
original = "HelloWorld"
insert_text = " "
position = 5
result = original[:position] + insert_text + original[position:]
print(result)
Output:
Hello World
The pattern string[:index] + new_content + string[index:] works for any insertion point:
sentence = "Python is great"
word = "really "
index = 10
modified = sentence[:index] + word + sentence[index:]
print(modified)
Output:
Python is really great
string[:index]extracts characters from the start up to (but not including) the index.string[index:]extracts characters from the index to the end.- Together, they capture the entire original string split at your chosen point, with the new content placed between them.
Using F-Strings for Readabilityā
F-strings provide a cleaner syntax for the same operation, especially when combining insertion with other formatting:
text = "ProgrammingFun"
insertion = " is "
pos = 11
result = f"{text[:pos]}{insertion}{text[pos:]}"
print(result)
Output:
Programming is Fun
This is particularly useful when the insertion is part of a larger formatted string:
filename = "reportpdf"
pos = 6
formatted = f"Document: {filename[:pos]}.{filename[pos:]}"
print(formatted)
Output:
Document: report.pdf
Reusable Insertion Functionā
Encapsulating the logic in a function makes it easy to reuse and handles edge cases cleanly:
def insert_at(original, substring, index):
"""
Insert a substring at the specified index.
Args:
original: The source string.
substring: Text to insert.
index: Position for insertion (0-based).
Returns:
New string with the substring inserted.
"""
if index < 0:
index = max(0, len(original) + index + 1)
return original[:index] + substring + original[index:]
print(insert_at("HelloWorld", " ", 5))
print(insert_at("Python", "3.", 0))
print(insert_at("Hello", "!", -1))
print(insert_at("Test", " Case", 100))
Output:
Hello World
3.Python
Hello!
Test Case
Python's slicing is forgiving. Indices beyond the string length simply return what is available without raising an error. "abc"[:100] returns "abc", making slicing inherently safer than direct index access with [].
Multiple Insertionsā
When inserting at several positions in the same string, you must work from the highest index to the lowest. Otherwise, each insertion shifts the positions of all subsequent characters, causing later insertions to land in the wrong place.
A Common Mistake: Inserting in Forward Orderā
text = "ABCDEF"
# Wrong: inserting at index 2 shifts everything right,
# so the second insertion at index 4 is now off by one
result = text[:2] + "-" + text[2:] # "AB-CDEF"
result = result[:4] + "-" + result[4:] # "AB-C-DEF" (wanted "AB-CD-EF")
print(result)
Output:
AB-C-DEF
The second dash ended up after C instead of after D because the first insertion shifted all positions by one.
The Correct Approach: Reverse Orderā
def insert_multiple(text, insertions):
"""
Insert multiple substrings at specified positions.
Args:
text: Original string.
insertions: List of (index, substring) tuples.
Returns:
Modified string with all insertions applied.
"""
# Sort by index in descending order to preserve positions
sorted_insertions = sorted(insertions, key=lambda x: x[0], reverse=True)
for index, substring in sorted_insertions:
text = text[:index] + substring + text[index:]
return text
original = "ABCDEF"
changes = [(2, "-"), (4, "-")]
result = insert_multiple(original, changes)
print(result)
Output:
AB-CD-EF
By processing the highest index first, earlier positions remain accurate because the insertions do not affect characters before them.
List Conversion for Heavy Modificationsā
When performing many insertions, converting the string to a list of characters can be more efficient because list.insert() modifies the list in place rather than creating a new string each time:
def insert_many_via_list(text, insertions):
"""Efficient approach for numerous insertions."""
chars = list(text)
# Sort by position descending to maintain correct indices
for index, substring in sorted(insertions, reverse=True):
chars.insert(index, substring)
return "".join(chars)
text = "ABCDEFGH"
insertions = [(2, "-"), (4, "-"), (6, "-")]
result = insert_many_via_list(text, insertions)
print(result)
Output:
AB-CD-EF-GH
Building strings with += in a loop creates a new string object on each iteration, resulting in O(n squared) complexity for many insertions. For multiple modifications, use list operations and str.join() to assemble the final result once.
# Slow: O(n²) - creates a new string every iteration
result = ""
for char in characters:
result += char
# Fast: O(n) - builds a list and joins once
result = "".join(characters)
Inserting at the Beginning and Endā
Two special cases deserve explicit mention since they are the most common insertion positions:
def insert_at(original, substring, index):
if index < 0:
index = max(0, len(original) + index + 1)
return original[:index] + substring + original[index:]
text = "World"
# Insert at the beginning (index 0)
result = "Hello " + text
print(result)
# Insert at the end (index equals length, or just concatenate)
result = text + "!"
print(result)
# Using the function for consistency
print(insert_at(text, "Hello ", 0))
print(insert_at(text, "!", len(text)))
Output:
Hello World
World!
Hello World
World!
For these simple cases, direct concatenation is the clearest approach. The insert_at function is more valuable when the index is computed dynamically at runtime.
Method Comparisonā
| Approach | Best For | Performance |
|---|---|---|
| Slicing + concatenation | Single insertions | O(n) per insertion |
| F-strings | Readable single insertions with formatting | O(n) per insertion |
| Reusable function | Repeated use with edge case handling | O(n) per insertion |
| Reverse-sorted multiple insertions | Several insertions at known positions | O(n) per insertion |
List conversion + join() | Many insertions in one string | O(n) total |
Conclusionā
String insertion in Python is fundamentally an assembly operation: split the original string at the target index and join the pieces with the new content in between.
- For single insertions, slicing with concatenation (
original[:i] + new + original[i:]) is the simplest and most readable approach. - For multiple insertions, always process positions in descending order to prevent index shifting from corrupting later insertions.
When performing many modifications, convert to a list and use "".join() to avoid the quadratic cost of repeated string concatenation.