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'
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'
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'
| Goal | Code |
|---|---|
| Add to the end | text + "!" |
| Add to the start | ">" + text |
| Insert at index i | text[:i] + ch + text[i:] |
| Join a list of parts | "-".join(parts) |
| Build from variables | f"{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
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'
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
| Symptom | Cause | Fix |
|---|---|---|
AttributeError: no attribute 'insert' | Strings are immutable | Slice and rebuild |
| Nothing changed | Result not assigned | text = text[:i] + ch + text[i:] |
| Character landed at the end | Index beyond the length | Check against len(text) |
| Off by one position | Index is where the new text goes | Inserting at 5 makes it character 5 |
| Multiple inserts drift | Earlier inserts shift later indexes | Insert from the back |
| Loop is slow | Repeated += | Collect a list, then join |
Other Python string editing guides worth having open:
- Remove a character by index
- Remove the last character
- Get the first n characters
- Remove newlines from a string
- Reverse a string in Python
- Create a string with variables
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.
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