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. Laptop displaying code with reflection, perfect for tech and programming themes.

ErrorTypeError: 'str' object does not support item assignment
Where it happensPython 3.x (and 2.x) - any runtime, framework, or notebook. Raised at the moment of assignment, not at import or parse time.
What it meansYou 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

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.

A close-up of a person typing on a keyboard in a modern tech workspace with gadgets and a monitor. ## None of Those? Narrow It Down
  1. 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.
  2. 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.
  3. 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 at readlines() (cause 6).
  4. 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(...), or json.dumps is your culprit and rules in cause 9 or 10.
  5. If the failing line sits inside a for, print the type of the first element of the iterable. str elements from a plain for x in some_dict means you are iterating keys (cause 5); str elements from readlines() means unsplit rows (cause 6).
  6. If the container is a defaultdict or came from dict.get(key, default), check the factory and the default value. A str default silently supplies "" instead of a mapping (cause 11).
  7. Run mypy on the module. It flags the line with Unsupported target for indexed assignment ("str") and usually also points at the wrong annotation or return type that let the string travel this far.
  8. 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. Creative young man working on a strategy plan on a whiteboard at the office.

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 a str is caught statically, before the code reaches a request handler.
  • Parse at the boundary: resp.json() and json.load(f) on the way in, json.dumps only 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.TypedDict so 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 (tags vs tags_label, config vs config_json) so a join or f-string cannot quietly retype a name.
  • Read tabular files with csv.reader or csv.DictReader, never readlines() 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, and dict.get(key, {}) rather than dict.get(key, "") when the value is meant to be a mapping.
  • Build text with "".join(parts), io.StringIO, or bytearray for byte-level work, and add a regression test that asserts the type of anything crossing a module boundary.

Related Guides

Errors You Will Probably Hit Next

  • AttributeError: 'str' object has no attribute 'append'
  • TypeError: string indices must be integers
  • TypeError: '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.

댓글

이 블로그의 인기 게시물

TypeError: Cannot read properties of undefined (reading 'map') - 11 Causes and Fixes

psql: FATAL: password authentication failed for user - 10 Causes and Fixes

Error: ERR_MODULE_NOT_FOUND: Cannot find package - 10 Causes and Fixes