Skip to main content

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
Understanding Slice Notation
  • 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
Safe Index Handling

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
Avoid Repeated Concatenation in Loops

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​

ApproachBest ForPerformance
Slicing + concatenationSingle insertionsO(n) per insertion
F-stringsReadable single insertions with formattingO(n) per insertion
Reusable functionRepeated use with edge case handlingO(n) per insertion
Reverse-sorted multiple insertionsSeveral insertions at known positionsO(n) per insertion
List conversion + join()Many insertions in one stringO(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.