TypeError: 'str' object does not support item assignment - 12 Causes and Fixes
TypeError: 'str' object does not support item assignment means you tried to write into a Python string: s[0] = "H", s[0:4] = "....", or data["key"] = value where data turned out to be text instead of a dict. Strings are immutable, so Python never exposes a way to store into one - str simply has no __setitem__.
That single message covers two very different bugs. Either you genuinely want to edit characters and have to rebuild the string instead of patching it, or - far more often in real code - the name you indexed is a str when you expected a dict, list, or parsed JSON object.
Below are 12 causes ordered by how often they show up in production code, each with a reproduction, the corrected lines, and a way to confirm the fix. If your case matches none of them, work the diagnostic checklist - it separates the "edit text" bug from the "wrong type" bug in two steps.
| Error | TypeError: 'str' object does not support item assignment |
|---|---|
| Where it happens | Python 3.x (and 2.x) - any runtime, framework, or notebook. Raised at the moment of assignment, not at import or parse time. |
| What it means | You used indexed or sliced assignment on an object that is a string, and strings cannot be modified in place. |
The Fast Fix
You cannot assign into a string. Rebuild it by slicing and concatenating, or rebind the name:
s = "hello world"
# s[0] = "H" # TypeError: 'str' object does not support item assignment
s = "H" + s[1:] # new string, same variable name
If you expected a container rather than text, the real bug is upstream - print the type first:
print(type(data), repr(data)[:120]) # <class 'str'> ? then parse it
data = resp.json() # not resp.text
data = json.load(f) # not f.read()
What Is Actually Causing It
Jump to your case
- 1. Changing one character by index
- 2. The JSON was never parsed - you hold raw text
- 3. Slice assignment on a string
- 4. Building or padding a string in a loop by index
- 5. Iterating a dict yields its keys, not its values
- 6. File lines treated as parsed rows
- 7. Double-encoded JSON: the nested field is still text
- 8. The nested config value is a scalar, not a mapping
- 9. The name was rebound to a string earlier
- 10. A helper returns serialized JSON instead of the object
- 11. defaultdict(str) where you wanted defaultdict(dict)
- 12. Mutating a string held in a pandas cell
1. Changing one character by index
Reproduce it
name = "jane"
name[0] = "J"
# TypeError: 'str' object does not support item assignment
Why it happens — str implements __getitem__ but not __setitem__, so reading name[0] works and writing it does not. Every "change" to a string must produce a new string object.
The fix
name = "jane"
name = "J" + name[1:] # rebuild, then rebind the name
# single known character anywhere, first occurrence only:
name = name.replace("j", "J", 1)
Changed: assignment moved from name[0] to name itself.
Confirm it worked — print(name) prints Jane, and id(name) differs before and after - proof you got a new object instead of mutating the old one.
2. The JSON was never parsed - you hold raw text
Reproduce it
import requests
resp = requests.get("https://api.example.com/users/1")
user = resp.text # str, not dict
user["name"] = "Jane"
# TypeError: 'str' object does not support item assignment
Why it happens — resp.text and open(...).read() both hand back the serialized payload as a string. Indexing it with "name" is a string index, which fails on assignment (and would fail with a different error on read).
The fix
user = resp.json() # decode the body
user["name"] = "Jane"
# file equivalent
import json
with open("user.json") as f:
user = json.load(f) # not f.read()
Changed: one decode call at the I/O boundary.
Confirm it worked — print(type(user)) prints <class 'dict'> immediately after the parse call.
3. Slice assignment on a string
Reproduce it
card = "4111111111111111"
card[0:12] = "*" * 12
# TypeError: 'str' object does not support item assignment
Why it happens — In Python 3, obj[a:b] = v also routes through __setitem__ with a slice object. Because str has no __setitem__ at all, you get the identical item-assignment message even though you touched a range rather than one index.
The fix
card = "4111111111111111"
card = "*" * 12 + card[12:] # concatenate the masked head with the kept tail
Changed: slice target replaced by slice read plus concatenation.
Confirm it worked — len(card) == 16 and card.endswith("1111") is True and the first twelve characters are *.
4. Building or padding a string in a loop by index
Reproduce it
out = " " * 5 # pre-sized buffer, C style
for i, ch in enumerate("abcde"):
out[i] = ch.upper()
# TypeError: 'str' object does not support item assignment
Why it happens — A pre-sized string is not a writable buffer. Python has no in-place string builder; the mutable equivalents are list (of characters) and bytearray (of bytes).
The fix
chars = [""] * 5
for i, ch in enumerate("abcde"):
chars[i] = ch.upper()
out = "".join(chars)
# byte-level work, where a real mutable buffer helps:
buf = bytearray(b"abcde")
buf[0] = ord("A")
Changed: the buffer is a list (or bytearray); the string is created once at the end.
Confirm it worked — out == "ABCDE" is True, and type(chars) is list inside the loop.
5. Iterating a dict yields its keys, not its values
Reproduce it
users = {"u1": {"name": "jane"}, "u2": {"name": "amir"}}
for user in users:
user["name"] = user["name"].title()
# TypeError: 'str' object does not support item assignment
Why it happens — for user in users binds each key - here the strings "u1" and "u2". The loop body then indexes into a key instead of the nested dict you meant.
The fix
for user in users.values():
user["name"] = user["name"].title()
# when you need the key too:
for uid, user in users.items():
user["name"] = user["name"].title()
Changed: users became users.values() / users.items().
Confirm it worked — print(users["u1"]["name"]) prints Jane; add print(type(user)) in the loop and confirm dict.
6. File lines treated as parsed rows
Reproduce it
with open("users.csv") as f:
rows = f.readlines()
for row in rows:
row[1] = row[1].strip().upper()
# TypeError: 'str' object does not support item assignment
Why it happens — readlines() returns a list of whole lines, each a str. row[1] is the second character of the line, and assigning to it hits the immutable string.
The fix
import csv
with open("users.csv", newline="") as f:
rows = list(csv.reader(f)) # each row is a mutable list of fields
for row in rows:
row[1] = row[1].strip().upper()
Changed: csv.reader splits each line into a list before the loop.
Confirm it worked — print(type(rows[0]), rows[0]) prints <class 'list'> followed by the separated fields.
More causes (6 remaining)
7. Double-encoded JSON: the nested field is still text
Reproduce it
import json
raw = r'{"id": 7, "payload": "{\"retries\": 0}"}'
event = json.loads(raw)
event["payload"]["retries"] = 1
# TypeError: 'str' object does not support item assignment
Why it happens — The producer serialized payload twice (or stored JSON in a text column), so a single json.loads leaves the inner object as a string. The outer decode succeeded, which is why the failure only appears at the nested write.
The fix
event = json.loads(raw)
if isinstance(event["payload"], str):
event["payload"] = json.loads(event["payload"]) # unwrap the second layer
event["payload"]["retries"] = 1
Changed: an explicit guarded second decode before the write.
Confirm it worked — print(type(event["payload"])) prints <class 'dict'>; event["payload"]["retries"] == 1.
8. The nested config value is a scalar, not a mapping
Reproduce it
config = {"db": "postgresql://localhost/app", "debug": False}
config["db"]["host"] = "127.0.0.1"
# TypeError: 'str' object does not support item assignment
Why it happens — Your code assumes config["db"] is a section, but the loaded file stores it as a single connection string. The index lands on that string.
The fix
config = {"db": {"url": "postgresql://localhost/app", "host": "localhost"}}
config["db"]["host"] = "127.0.0.1"
# keeping the flat string? edit it as a URL, not as a dict:
from urllib.parse import urlsplit, urlunsplit
parts = urlsplit("postgresql://localhost/app")
config_url = urlunsplit(parts._replace(netloc="127.0.0.1"))
Changed: either the config gains a real nested section, or the string is parsed before being rewritten.
Confirm it worked — print(type(config["db"])) prints <class 'dict'> for the restructured version; for the URL path, print(config_url) shows the new host.
9. The name was rebound to a string earlier
Reproduce it
tags = ["python", "errors"]
# ... 40 lines later, for a log line:
tags = ", ".join(tags) # same name, now a str
tags[0] = "Python"
# TypeError: 'str' object does not support item assignment
Why it happens — join, format, f-strings, str(), and ", ".join(...) all return text. Reusing the original name silently changes the variable's type for the rest of the scope.
The fix
tags = ["python", "errors"]
tags_label = ", ".join(tags) # separate name for the serialized form
tags[0] = "Python"
Changed: the string form gets its own name, so the list stays a list.
Confirm it worked — Grep every binding of the name before the failing line - grep -n "tags *=" app.py - and confirm only one type is ever assigned.
10. A helper returns serialized JSON instead of the object
Reproduce it
import json
def load_settings(path):
with open(path) as f:
return json.dumps(json.load(f)) # dumps, not the parsed object
settings = load_settings("settings.json")
settings["theme"] = "dark"
# TypeError: 'str' object does not support item assignment
Why it happens — The function round-trips the file and hands back text. Callers that treat the return value as a mapping fail at the first write, far from the line that caused it.
The fix
def load_settings(path) -> dict:
with open(path) as f:
return json.load(f) # return the object
settings = load_settings("settings.json")
settings["theme"] = "dark"
# serialize only at the exit boundary:
open("settings.json", "w").write(json.dumps(settings))
Changed: json.dumps removed from the loader; serialization moved to the write path.
Confirm it worked — Run mypy on the file with the -> dict annotation in place - it reports the mismatch at the return line - or assert isinstance(settings, dict) right after the call.
11. defaultdict(str) where you wanted defaultdict(dict)
Reproduce it
from collections import defaultdict
index = defaultdict(str)
index["errors"]["TypeError"] = 1
# TypeError: 'str' object does not support item assignment
Why it happens — The default factory manufactures an empty string "" for the missing key, so the second index immediately writes into a str. The code never raises KeyError, which hides the wrong factory.
The fix
index = defaultdict(dict)
index["errors"]["TypeError"] = 1
# counters nested one level deeper:
from collections import Counter
index = defaultdict(Counter)
index["errors"]["TypeError"] += 1
Changed: the factory produces a mutable mapping instead of a string.
Confirm it worked — print(type(index["anything"])) prints <class 'dict'> (note that this probe itself creates the key).
12. Mutating a string held in a pandas cell
Reproduce it
import pandas as pd
df = pd.DataFrame({"name": ["jane", "amir"]})
df.loc[0, "name"][0] = "J"
# TypeError: 'str' object does not support item assignment
Why it happens — df.loc[0, "name"] returns the Python string stored in the cell, not a view into the frame. The trailing [0] = ... therefore targets an ordinary immutable str.
The fix
df.loc[0, "name"] = "J" + df.loc[0, "name"][1:] # assign a whole new cell value
# whole column at once:
df["name"] = df["name"].str.capitalize()
Changed: the assignment target is the cell or column, and the string is rebuilt before being stored.
Confirm it worked — print(df.loc[0, "name"]) prints Jane; df["name"].tolist() shows the updated column.
## None of Those? Narrow It Down
- Print the type and a truncated repr immediately before the failing line:
print(type(target), repr(target)[:200]). Confirming<class 'str'>rules out every "it is actually a dict/list" theory and tells you which half of this page applies. - Decide what you intended. If you meant to change characters in text, only causes 1, 3, and 4 apply and no form of assignment will ever work - you must rebuild. If you meant to set a field in a container, the bug is upstream of this line.
- Look at the repr. Text that starts with
{or[is unparsed or double-encoded JSON (causes 2 and 7). A connection string, path, or URL points at a flat config value (cause 8). A whole CSV line points atreadlines()(cause 6). - Walk the name backwards: grep every assignment to it in the scope. A right-hand side containing
join,format, an f-string,.read(),.text,str(...), orjson.dumpsis your culprit and rules in cause 9 or 10. - If the failing line sits inside a
for, print the type of the first element of the iterable.strelements from a plainfor x in some_dictmeans you are iterating keys (cause 5);strelements fromreadlines()means unsplit rows (cause 6). - If the container is a
defaultdictor came fromdict.get(key, default), check the factory and the default value. Astrdefault silently supplies""instead of a mapping (cause 11). - Run
mypyon the module. It flags the line withUnsupported target for indexed assignment ("str")and usually also points at the wrong annotation or return type that let the string travel this far. - Still unmatched? Reduce to three lines in a REPL: bind the value exactly as production does, print its type, then attempt the assignment. If the three-line version passes, the real value differs from the one you assume - log it at the source.
Why This Error Exists At All
Python strings are immutable by design, and the error is the direct consequence: str implements __getitem__ but deliberately omits __setitem__, so the interpreter falls back to a generic message naming the type that refused the write. Immutability buys three things the language depends on. Strings are hashable, which is what makes them usable as dict keys and set members - a key that could change under you would corrupt the hash table holding it. CPython can intern and share string objects freely, so two identical literals may be the same object, and mutating one would silently rewrite the other. And an immutable object is safe to pass across functions and threads without defensive copying.
There is a layout reason too. Under the compact representation introduced by PEP 393, a str object stores its character data inline, sized once at creation, alongside a cached hash and a width tag chosen from the widest code point present. There is nowhere to grow, and writing a wider character into a narrower buffer would require reallocating the object - which is exactly what "return a new string" means. So Python does not pretend: the mutable counterparts are separate types, list for sequences of anything and bytearray for raw bytes.
Read the message as a type assertion failure, not a syntax problem. It names the type you have, never the type you wanted, and the same sentence appears for tuple, bytes, and range - every type without __setitem__. Its close relatives tell you the same thing from other angles: AttributeError: 'str' object has no attribute 'append' is the method-call version, and TypeError: string indices must be integers is the read-side version of indexing a string with a key. Whenever you see any of them, ask one question - where did this value become a string? - and the fix is almost always a missing parse at the boundary rather than anything at the line that raised.
Stop It From Coming Back
- Annotate function returns and config structures, then run
mypy(or Pyright/Pylance in the editor) in CI. Indexed assignment into astris caught statically, before the code reaches a request handler. - Parse at the boundary:
resp.json()andjson.load(f)on the way in,json.dumpsonly on the way out. No function should return serialized text to a caller that treats it as data. - Validate external payloads with Pydantic models or
typing.TypedDictso a string arriving where an object belongs fails at the parse step with a field name, instead of 200 lines later as an item-assignment TypeError. - Never reuse a variable name across types. Keep a suffix convention for serialized forms (
tagsvstags_label,configvsconfig_json) so ajoinor f-string cannot quietly retype a name. - Read tabular files with
csv.readerorcsv.DictReader, neverreadlines()plus manual indexing - the former hands you mutable lists, the latter hands you immutable lines. - Check default factories and fallbacks:
defaultdict(dict)/defaultdict(Counter)for nested structures, anddict.get(key, {})rather thandict.get(key, "")when the value is meant to be a mapping. - Build text with
"".join(parts),io.StringIO, orbytearrayfor byte-level work, and add a regression test that asserts the type of anything crossing a module boundary.
Related Guides
-
TypeError: Cannot read properties of undefined (reading 'map')
-
TS2345: Argument of type is not assignable to parameter of type
Errors You Will Probably Hit Next
AttributeError: 'str' object has no attribute 'append'TypeError: string indices must be integersTypeError: 'tuple' object does not support item assignment
The message names the type you have, not the type you wanted. So before touching the failing line, print type() on the target: if it should be text, rebuild it with slicing, replace, or join, because no assignment will ever work; if it should be a container, the real bug is the missing json.loads, .values(), csv.reader, or stray join somewhere upstream. Fix it where the value became a string, not where it finally refused to be written.
댓글
댓글 쓰기