PythonMastery
reference 3 min read · lesson 56 of 60 in Errors

AttributeError: 'str' object has no attribute 'X'

1 · The lesson

read

What this error means

You asked a string for a method it doesn't have. The method name in the message tells you what you thought you were holding: append means you expected a list, decode means bytes, get or keys means a dict, is_integer means a number. The variable is a str, and the bug is almost never in the line that crashed — it's wherever that value stayed text when you expected it to become something else.

When you see it

python
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    names.append("Grace")
    ^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'append'

Python 3.13 sometimes adds a hint when a string method has a similar name:

python
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    text = raw.decode("utf-8")
           ^^^^^^^^^^
AttributeError: 'str' object has no attribute 'decode'. Did you mean: 'encode'?

Take that hint with a pinch of salt. encode exists on str, but it's the opposite operation and rarely what you want here.

Why it happens

Four situations cover nearly every case.

1. You wanted a list and started with a string. Someone wrote names = "Ada" meaning "a list with Ada in it". Strings are immutable, so they have no append, remove or insert.

python
names = "Ada"
names.append("Grace")       # AttributeError: no attribute 'append'

2. Python 2 habits: .decode() on text. In Python 3, str is already decoded text. Only bytes has .decode(). Code ported from Python 2, or copied from an old answer, calls .decode("utf-8") on something that's already a str.

3. A number that's still text. input() always returns a string. So does every cell that csv.reader hands you. The value looks like 42, but it's "42".

python
quantity = "3"              # what input() or csv.reader actually gives you
print(quantity.is_integer())   # AttributeError: no attribute 'is_integer'

4. JSON you never parsed. An API body, a message off a queue or a value from a database column comes back as a JSON string. Until you call json.loads, it's characters, not a dict.

python
body = '{"customer": "Hedy", "total": 42.5}'
print(body.get("customer"))    # AttributeError: no attribute 'get'

Try body["customer"] instead and you get a different error — TypeError: string indices must be integers, not 'str' — for exactly the same reason.

How to fix it

Start with the right type. If it's meant to hold several names, make it a list from the beginning:

python
names = ["Ada"]
names.append("Grace")
print(names)                # ['Ada', 'Grace']

Building up a string rather than a list? Use += or, better, collect the pieces in a list and join once at the end:

python
parts = []
for name in ["Ada", "Grace", "Linus"]:
    parts.append(name)
print(", ".join(parts))     # Ada, Grace, Linus

Drop the .decode(). If the value is already a str, there's nothing to decode. If you genuinely might get either, check the type once at the boundary:

python
def as_text(value):
    if isinstance(value, bytes):
        return value.decode("utf-8")
    return value

print(as_text(b"Ada Lovelace"))   # Ada Lovelace
print(as_text("Grace Hopper"))    # Grace Hopper

Convert numbers where they come in. Do it once, at the line that reads the value, not three functions later:

python
import csv
import io

sales_csv = io.StringIO("item,qty,price\nkeyboard,2,49.99\nmouse,5,19.50\n")

total = 0.0
for row in csv.DictReader(sales_csv):
    qty = int(row["qty"])           # "2"     -> 2
    price = float(row["price"])     # "49.99" -> 49.99
    total += qty * price
print(f"Revenue: {total:.2f}")      # Revenue: 197.48

Parse JSON before you use it. json.loads turns the string into dicts and lists:

python
import json

body = '{"customer": "Hedy", "total": 42.5}'
order = json.loads(body)
print(order.get("customer"))   # Hedy
print(type(order))             # <class 'dict'>

When you're not sure what you've got, print type(x) and repr(x). repr shows the quotes, so '42' versus 42 is obvious at a glance.

When you'd actually see this in real code

  • A config value read from an environment variable: os.environ["MAX_RETRIES"] is always a string, so retries.bit_length() or any arithmetic on it fails.
  • A requests call where you used resp.text instead of resp.json() and then tried .get("data") on the result.
  • A JSON column in SQLite or a Redis value: you stored it with json.dumps, read it back, and forgot the json.loads on the way out.
  • A function that accepts "one name or a list of names" and gets passed a bare string — then names.append(...) blows up, or worse, for n in names quietly loops over letters.

See Also

Found this useful? Share it with a friend who’s learning Python.