Insert a Character Into a String in Python (at Any Index)

To insert a character into a Python string at a specific index, slice around the position and rebuild:

text = "HelloWorld"

text[:5] + " " + text[5:]      # 'Hello World' 

There is no str.insert(). Strings cannot be modified, so every method here produces a new string rather than changing the original.

All output below comes from real runs on Python 3.12.5.

Inserting a character at a specific index

Three pieces: the part before, the new text, and the part after:

text = "HelloWorld"
i = 5

# everything before the index, the new character, then the rest
result = text[:i] + " " + text[i:]

print("original:", repr(text))
print("result  :", repr(result))

print()
# the same pattern inserts a whole substring, not just one character
print("substring:", repr(text[:5] + ", cruel " + text[5:]))

Output:

original: 'HelloWorld'
result  : 'Hello World'

substring: 'Hello, cruel World'
Command Prompt showing a character inserted into a Python string at index five using slicing
The same pattern inserts a single character or a whole phrase.

Nothing about this is limited to one character. text[:5] + ", cruel " + text[5:] inserts seven characters just as happily.

The index is the position the new text will occupy. Inserting at 5 means the new character becomes character number 5, counting from zero.

Why str object has no attribute insert

Coming from a list, insert is the obvious thing to reach for, and it does not exist:

text = "HelloWorld"

# strings have no insert method, because they cannot be changed
try:
    text.insert(5, " ")
except AttributeError as err:
    print("text.insert(5, ' ') ->", type(err).__name__ + ":", err)

print()
print("list has insert:", hasattr(list, "insert"))
print("str  has insert:", hasattr(str, "insert"))

print()
# the list route, when you prefer it
chars = list(text)
chars.insert(5, " ")
print("via a list:", repr("".join(chars)))

Output:

text.insert(5, ' ') -> AttributeError: 'str' object has no attribute 'insert'

list has insert: True
str  has insert: False

via a list: 'Hello World'
Command Prompt showing an AttributeError because a Python string has no insert method
Lists have insert. Strings are immutable, so they do not.

Converting to a list gives you insert back, and "".join() turns it into a string again.

That route is slower for a single edit, but it reads well when you are making several changes. The same immutability governs removing a character by index.

Appending and prepending to a string

If the position is the very start or the very end, you do not need an index at all:

text = "Python"

# the two easiest cases need no index at all
print("append  :", repr(text + "!"))
print("prepend :", repr(">>> " + text))

print()
# building up in a loop: join is the right tool
parts = ["a", "b", "c"]
print("join    :", repr("-".join(parts)))

print()
# and f-strings for readable assembly
name, version = "Python", 3.12
print("f-string:", repr(f"{name} {version} released"))

Output:

append  : 'Python!'
prepend : '>>> Python'

join    : 'a-b-c'

f-string: 'Python 3.12 released'
GoalCode
Add to the endtext + "!"
Add to the start">" + text
Insert at index itext[:i] + ch + text[i:]
Join a list of parts"-".join(parts)
Build from variablesf"{name} {version}"

Building a string by repeated += in a loop is the one pattern to avoid. Each step copies everything so far, which makes the loop quadratic.

Collect the pieces in a list and join them once at the end instead.

What happens with an out-of-range index

Slicing clamps, so a bad index appends rather than raising:

text = "HelloWorld"

def insert_at(s, i, ch):
    return s[:i] + ch + s[i:]

print("index 0   :", repr(insert_at(text, 0, ">")))
print("index 5   :", repr(insert_at(text, 5, " ")))
print("index -3  :", repr(insert_at(text, -3, "-")), "  counts from the end")
print("index 99  :", repr(insert_at(text, 99, "!")), " <- lands at the end, no error")

print()
print("an out-of-range index does not raise, it just appends")
print("check it yourself if that would be a bug:")
i = 99
print(f"  0 <= {i} <= {len(text)} ?", 0 <= i <= len(text))

Output:

index 0   : '>HelloWorld'
index 5   : 'Hello World'
index -3  : 'HelloWo-rld'   counts from the end
index 99  : 'HelloWorld!'  <- lands at the end, no error

an out-of-range index does not raise, it just appends
check it yourself if that would be a bug:
  0 <= 99 <= 10 ? False
Command Prompt showing that inserting at an index beyond the string length appends instead of raising an error
Index 99 on a ten-character string quietly lands at the end.

That silence is worth guarding against. If the index comes from a calculation, check 0 <= i <= len(text) before you use it.

Negative indexes work as you would expect, counting back from the end. -3 inserts three characters from the right.

Inserting separators every n characters

Formatting jobs are the most common real use, and they don’t need a loop over indexes:

text = "HelloWorld"

# a separator every 3 characters
chunks = [text[i:i + 3] for i in range(0, len(text), 3)]
print("chunks   :", chunks)
print("joined   :", repr("-".join(chunks)))

print()
# the classic: formatting a phone number
digits = "5551234567"
print("formatted:", f"({digits[:3]}) {digits[3:6]}-{digits[6:]}")

print()
# inserting at several known positions, from the back so indexes stay valid
positions = [2, 5]
out = text
for i in sorted(positions, reverse=True):
    out = out[:i] + "|" + out[i:]
print("multi    :", repr(out))

Output:

chunks   : ['Hel', 'loW', 'orl', 'd']
joined   : 'Hel-loW-orl-d'

formatted: (555) 123-4567

multi    : 'He|llo|World'
Command Prompt showing separators inserted every three characters and a phone number formatted with slices
Chunk with a range step, then join.

Splitting into chunks and joining is far clearer than tracking shifting positions, and it handles a trailing partial chunk for free.

When you do need several specific positions, insert from the back. Working forwards shifts every later index by one and the result drifts.

Which insertion method is faster?

For a single insertion into a long string, the slice wins comfortably:

import timeit

text = "x" * 5000

def slice_insert():
    return text[:2500] + "Y" + text[2500:]

def list_insert():
    chars = list(text)
    chars.insert(2500, "Y")
    return "".join(chars)

a = timeit.timeit(slice_insert, number=20_000)
b = timeit.timeit(list_insert, number=20_000)

print(f"slice insert       {a * 1000:>8.0f} ms")
print(f"list insert + join {b * 1000:>8.0f} ms")
print(f"\nthe slice is about {b / a:.0f}x faster for a single insertion")
print("a list is only worth it when you are making many edits in a row")

Output:

slice insert              6 ms
list insert + join      447 ms

the slice is about 69x faster for a single insertion
a list is only worth it when you are making many edits in a row

The slice copies two blocks of memory. The list version allocates one Python object per character, inserts, then reassembles.

If you’re making many edits to the same text, convert to a list once, make all the changes, and join at the end. That amortises the conversion.

Common string insertion mistakes

SymptomCauseFix
AttributeError: no attribute 'insert'Strings are immutableSlice and rebuild
Nothing changedResult not assignedtext = text[:i] + ch + text[i:]
Character landed at the endIndex beyond the lengthCheck against len(text)
Off by one positionIndex is where the new text goesInserting at 5 makes it character 5
Multiple inserts driftEarlier inserts shift later indexesInsert from the back
Loop is slowRepeated +=Collect a list, then join

Other Python string editing guides worth having open:

Frequently asked questions

How do I insert a character into a string at a specific index in Python?

Slice around the position: text[:i] + ch + text[i:]. Slicing is covered in the Python sequence operations reference.

Why is there no str.insert() in Python?

Strings are immutable, so there is nothing to insert into. Every string operation returns a new string.

How do I add a character to the end of a string?

text + "!". For the start, put the new text first: ">" + text.

What happens if the index is larger than the string?

Slicing clamps to the length, so the character is appended at the end and no error is raised.

How do I insert a character at multiple positions?

Work from the highest index down, so earlier insertions do not shift the positions you have not reached yet.

How do I add a separator every n characters?

Split into chunks with range(0, len(text), n) and join them with your separator.

Is slicing or a list faster for inserting?

Slicing, for a single insertion. A list is worth the conversion only when you are making many edits in a row.