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 returnedNone. 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:
| Expression | Message |
|---|---|
1 + '2' | unsupported operand type(s) for +: 'int' and 'str' |
'2' + 1 | can 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 None — list.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
| Expression | Result |
|---|---|
datetime - datetime | ✅ a timedelta |
time - time | ❌ unsupported operand type(s) for -: 'datetime.time' and 'datetime.time' |
date + int | ❌ unsupported 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 message | What 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
- ✓ Read all three parts — operator, left type, right type — before changing anything
- ✓ The types are in source order; that tells you which side to convert
- ✓ Convert at the boundary (
input(), JSON,os.environ, CSV), not at the error - ✓
'NoneType'? Find the function that returned nothing - ✓
'builtin_function_or_method'? Add the missing parentheses - ✓
sum()failing on strings? Its start value is theintin the message - ✓ Bad value hiding in a dataset? Loop with
enumerateand catch per item - ✓ Remember
1 * '2'and1 + Truesucceed — absence of an error isn't proof of correct types
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
- Emulating numeric types — __add__ and __radd__ (Python Language Reference)
- sum() (Python Standard Library)
- PEP 604 — Allow writing union types as X | Y (Python 3.10)
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.