Python: TypeError: unsupported operand type(s) for +: 'int' and 'str'

Quick answer

  • The message names three things — the operator, the left type, the right type, in source order. All three matter.
  • Numbers meant to be numbers? Convert where the string entered (input(), JSON, os.environ), not at the error.
  • Says 'NoneType'? Nothing to convert — a function returned None. Trace it back.
  • Says 'builtin_function_or_method'? You forgot the parentheses on a call.

The exact error string

Traceback (most recent call last):
  File "app.py", line 2, in <module>
    total = count + suffix
TypeError: unsupported operand type(s) for +: 'int' and 'str'

# the same error shape, other operators and types you'll see in the wild:
TypeError: unsupported operand type(s) for -: 'str' and 'int'
TypeError: unsupported operand type(s) for -: 'float' and 'NoneType'
TypeError: unsupported operand type(s) for +=: 'int' and 'builtin_function_or_method'
TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'

Python raises this when an operator has no implementation for that pair of types. It is not saying either value is invalid — both are perfectly good objects. It is saying the combination has no defined meaning, which is deliberate: unlike JavaScript or PHP, Python refuses to silently guess whether you meant arithmetic or concatenation.

Everything below was verified on CPython 3.14.6.

Anatomy of the message

unsupported operand type(s) for  +:  'int'  and  'str'
                                 ^    ^^^^^       ^^^^^
                                 |    |           |
                                 |    |           +-- type of the RIGHT operand
                                 |    +-------------- type of the LEFT operand
                                 +------------------- the operator that failed

The two type names appear in source order: left operand first. That is more useful than it sounds, because it tells you which side to convert without having to reason about the expression. If you see 'int' and 'str', the string is on the right; 'str' and 'int' means it is on the left.

Swapping the operands changes the message

This trips people up constantly, and no other guide covers it. The same conceptual mistake produces two entirely different errors depending on operand order:

ExpressionMessage
1 + '2'unsupported operand type(s) for +: 'int' and 'str'
'2' + 1can only concatenate str (not "int") to str
[1] + 'a'can only concatenate list (not "str") to list
'a' + b'b'can only concatenate str (not "bytes") to str

The reason is the protocol described in the data model: Python asks the left operand's __add__ first. With 1 + '2', int.__add__ returns NotImplemented, Python then tries str.__radd__, that declines too, and only then does the interpreter emit the generic message. With '2' + 1 the str handles the call itself and raises its own, more specific complaint before the fallback is ever reached.

Both messages mean the same thing — you mixed a number and a string — so if you searched for one and landed here, the fixes below apply either way.

Fix 1: find where the string came from

Converting at the point of the error is a patch. The durable fix is to convert at the boundary, because these four sources hand you strings whether you expect them or not:

# 1. input() ALWAYS returns str, even when the user types a number
age = input("Age: ")          # '30'
# age + 1  ->  can only concatenate str (not "int") to str
age = int(input("Age: "))     # convert here, once

# 2. JSON keeps whatever type the producer sent — "3" stays a string
data = json.loads('{"qty": "3", "price": 2}')
# data["qty"] + data["price"]  ->  can only concatenate str (not "int") to str
qty = int(data["qty"])

# 3. environment variables are always str
port = int(os.environ["PORT"])       # never os.environ["PORT"] + 1

# 4. CSV columns are str unless you convert (pandas: dtype 'object')
df["qty"] = pd.to_numeric(df["qty"], errors="coerce")

The JSON case is worth dwelling on because it is invisible until it isn't: an API that returns {"qty": "3"} parses without complaint, and the failure surfaces much later at the first arithmetic. If you are unsure what types a payload actually contains, run it through the JSON Formatter — quoted values are strings, bare ones are numbers. The same mismatch in Go produces json: cannot unmarshal string into Go value of type int, which at least fails at parse time instead of later.

Fix 2: sum() hides an int + str

sum() starts its running total at 0. The first element is therefore added to an integer, and a list of strings fails on the very first step:

# ❌ sum(['1', '2'])
#    TypeError: unsupported operand type(s) for +: 'int' and 'str'

# ✅ convert as you go
sum(int(x) for x in ['1', '2'])        # 3

# joining strings? sum() refuses outright, with its own message:
# sum(['a', 'b'], '')
#    TypeError: sum() can't sum strings [use ''.join(seq) instead]
''.join(['a', 'b'])                    # 'ab'

Note that the error names 'int' and 'str' even though your list contains no integers at all — the int is sum()'s own start value. That mismatch between the message and your data is why this case is so often misread.

Fix 3: 'NoneType' means something returned nothing

When one of the type names is 'NoneType', there is nothing to convert. A value you assumed existed is None:

# TypeError: unsupported operand type(s) for -: 'float' and 'NoneType'

def get_discount(user):
    if user.is_premium:
        return 0.2
    # ❌ falls off the end -> returns None implicitly for everyone else

price - get_discount(user)      # float - None

The usual sources are a function with a conditional return that misses a branch, dict.get() with no default, a regex search() that found nothing, or a method that mutates in place and returns Nonelist.sort() and list.append() both do. Fix the origin rather than defaulting to zero, or you will hide a real bug. The related 'NoneType' object has no attribute comes from the same class of mistake reached through attribute access instead of an operator.

Fix 4: 'builtin_function_or_method' means missing parentheses

This type name only ever appears for one reason — you referenced a function without calling it:

# ❌ total += len
#    TypeError: unsupported operand type(s) for +=: 'int' and 'builtin_function_or_method'

total += len(items)      # ✅ call it

Whenever 'function', 'builtin_function_or_method' or 'method' shows up as an operand type, go to the line in the traceback and look for a name that should have been followed by ().

Fix 5: | between types needs Python 3.10+

The X | Y union syntax was introduced by PEP 604 in Python 3.10. On an older interpreter it fails as an operand error, which is confusing because the code looks like a type annotation rather than an expression:

# on Python 3.9 and earlier:
#   def f(x: int | None): ...
#   TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'

from typing import Optional
def f(x: Optional[int]): ...          # ✅ works on every supported version

# or defer annotation evaluation entirely
from __future__ import annotations     # ✅ then `int | None` is fine as an annotation

The __future__ import works because annotations then stay unevaluated strings at runtime; it does not help if you use int | None as a real expression, such as in a TypeAlias assignment or an isinstance() check.

Fix 6: dates and times don't all support arithmetic

ExpressionResult
datetime - datetime✅ a timedelta
time - timeunsupported operand type(s) for -: 'datetime.time' and 'datetime.time'
date + intunsupported operand type(s) for +: 'datetime.date' and 'int'
date + timedelta(days=1)✅ a date

A time is a wall-clock reading with no date attached, so subtracting two of them has no well-defined answer across a midnight boundary — combine them with a date first (datetime.combine). And date arithmetic takes a timedelta, never a bare integer, because "plus 1" is ambiguous about its unit.

Locate the offending value, don't hunt for it

When the bad value is one row in a large dataset, the traceback tells you the line but not the item. Catch per iteration instead:

total = 0
for i, row in enumerate(rows):
    try:
        total += row
    except TypeError as e:
        print(f"row {i} = {row!r} ({type(row).__name__}) -> {e}")
        # row 2 = '3' (str) -> unsupported operand type(s) for +=: 'int' and 'str'

# or find every offender at once, before doing any arithmetic
bad = [(i, v) for i, v in enumerate(rows) if not isinstance(v, (int, float))]

Two combinations that don't raise

Worth knowing, because both silently produce wrong results rather than an error:

1 * '2'      # '2'   — int * str REPEATS the string, it doesn't multiply
3 * 'ab'     # 'ababab'
1 + True     # 2     — bool is a subclass of int, so this is valid arithmetic
sum([True, True, False])   # 2  — sometimes useful, sometimes a lurking bug

So a stray string that would fail under + may sail through * and corrupt your output instead. If you are validating input, check the types rather than relying on an exception to catch the mistake.

Reading any variant of this message

Type name in the messageWhat to do
'str'Convert with int()/float(), or convert the other side with str()
'NoneType'Don't convert — find what returned None (Fix 3)
'builtin_function_or_method', 'function'Add the missing () (Fix 4)
'list', 'dict', 'tuple'You're operating on the container, not an element — index or iterate
'type'You're operating on a class object itself — usually the | union case (Fix 5)
'Decimal' with 'float'Convert the float: Decimal(str(x)) — never mix the two

Paste a raw traceback into the Error Log Analyzer if you want it routed to the matching reference automatically.

Debugging checklist

Frequently Asked Questions

How do I fix TypeError: unsupported operand type(s) for +: 'int' and 'str'?

Decide which type you actually want, then convert at the source rather than at the point of the error. If both values are meant to be numbers, wrap the string one in int() or float(). If you are building a message, convert the number with str() or use an f-string. The more durable fix is to convert where the string entered your program — the input() call, the JSON field, the environment variable — so the rest of the code works with real numbers.

Why do 1 + '2' and '2' + 1 give different error messages?

Because Python tries the left operand's __add__ first. With 1 + '2' the int does not know how to add a str, the str's __radd__ also declines, and you get the generic unsupported operand type(s) message. With '2' + 1 the str's own __add__ handles it and raises a more specific complaint: can only concatenate str (not "int") to str. Same underlying mistake, two different messages, decided purely by which value is on the left.

Why does sum() raise this error on a list of strings?

Because sum() starts from 0, an integer, and then adds each element to that running total. The very first addition is therefore int plus str, which is exactly the unsupported combination. Convert the elements first with sum(int(x) for x in values), and note that sum() refuses strings even with a string start value — it raises a dedicated message telling you to use ''.join(seq) instead.

What does it mean when the message says 'NoneType'?

It means one operand is None, so a function returned nothing where you expected a value, a dictionary lookup produced a default of None, or an attribute was never assigned. The fix is not to convert None but to trace where it came from — a function that falls off the end without a return statement returns None implicitly, and that is by far the most common source.

Why does the message mention builtin_function_or_method?

Because you referenced a function without calling it. Writing total += len instead of total += len(items) adds the function object itself, and Python reports its type as builtin_function_or_method. Any time that type name appears in an operand error, look for a missing pair of parentheses on the line the traceback points to.

Why does int | None fail on my machine but work elsewhere?

Because the X | Y union syntax was introduced by PEP 604 in Python 3.10. On an earlier interpreter the pipe operator between types raises unsupported operand type(s) for |: 'type' and 'NoneType'. Either upgrade, or use typing.Optional[int] and typing.Union, which work on older versions. Adding from __future__ import annotations also helps when the union only appears in annotations rather than at runtime.

How do I find which value in a list is the wrong type?

Loop with enumerate and catch the TypeError per item, printing the index, the repr and the type. That turns one unhelpful traceback into the exact position and value that broke — far quicker than inspecting a large dataset by eye. For data that arrives from JSON or CSV, validating types at the boundary is better still, since one stray string in a thousand rows will otherwise fail only at the moment it is used.

References

Check what types your JSON actually contains

Quoted values are strings, bare ones are numbers — the formatter makes the difference obvious before it reaches your arithmetic.

JSON Formatter Error Log Analyzer All Error References
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.