2025, Oct 17 07:00

Understanding Python sum(): _Addable, SupportsAdd, and why strings are rejected (use join instead)

Understand Python's sum() with _Addable and SupportsAdd from typeshed: why strings raise at runtime, how typing treats them, and when to prefer ''.join()

When digging into Python’s sum() in an IDE, it’s easy to stumble over mysterious type names like _Addable. The function clearly adds numbers and can concatenate lists with a provided start value, but it refuses to work with strings even though strings support +. What does “addable” actually mean here, and why does sum(['a', 'b'], '') explode at runtime?

Reproducing the situation

First, the behavior that feels intuitive: summing numbers and concatenating lists via a start value.

a_numbers = list(range(1, 4))
b_numbers = list(range(4, 7))
sum(a_numbers)  # 6
nested_lists = [a_numbers, b_numbers]
sum(nested_lists, [])  # [1, 2, 3, 4, 5, 6]

Now the surprising case. Strings do implement addition, but sum rejects them even if a start value is provided.

sum(["x", "y"], "")

TypeError: sum() can't sum strings [use ''.join(seq) instead]

Where _Addable comes from

The clue is not in Python’s runtime, but in its typing information. The type hint you saw is tied to a protocol named SupportsAdd defined in a stubs-only helper module called _typeshed. This module is part of typeshed, which is the shared source of type information for standard library and common packages that type checkers and language servers use. It ships as .pyi stubs and is not importable at runtime. In other words, import _typeshed will not work when your program runs; it exists for static analysis only.

SupportsAdd is very simple. It’s a protocol that says “this type has __add__”.

class AddProto(Protocol[_U_contra, _V_co]):
    def __add__(self, other: _U_contra, /) -> _V_co: ...

In this shape, SupportsAdd[Any, Any] just means “any class that defines __add__”. The _Addable type variables you saw are bound to that protocol.

AddableA = TypeVar("AddableA", bound=AddProto[Any, Any])
AddableB = TypeVar("AddableB", bound=AddProto[Any, Any])

How sum() is typed

The typeshed stub for sum is overloaded to capture a few common scenarios. Among them is a special-case for booleans and literal integers, and a general case for addable types.

AddableA = TypeVar("AddableA", bound=AddProto[Any, Any])
AddableB = TypeVar("AddableB", bound=AddProto[Any, Any])
@type_check_only
class _SumNoDefaultProto(AddProto[Any, Any], SupportsRAdd[int, Any], Protocol): ...
_SumNoDefaultT = TypeVar("_SumNoDefaultT", bound=_SumNoDefaultProto)
@overload
def sum(items: Iterable[bool | _LiteralInteger], /, start: int = 0) -> int: ...
@overload
def sum(items: Iterable[_SumNoDefaultT], /) -> _SumNoDefaultT | Literal[0]: ...
@overload
def sum(items: Iterable[AddableA], /, start: AddableB) -> AddableA | AddableB: ...

This encodes that sum works over iterables of addable things, optionally with a start value. It also encodes that, if you don’t provide a start value, sum behaves as if it begins from 0, which is why using a type that can’t be added to int will fail unless your type supports that operation. Static type checkers surface this via the overloads.

So why does sum() reject strings?

Here is the crux: Python’s static typing does not aim to encode every runtime semantic detail. The restriction against summing strings is a runtime rule enforced by the implementation. It is not modeled in typeshed’s types for sum, and type checkers do not have to warn about it. That’s why sum(["x", "y"], "") type-checks cleanly in many tools yet fails when executed.

In short, “addable” in this context means “has __add__” for the purpose of typing, not “sum will always accept it.” Strings are a notable exception that sum explicitly disallows at runtime.

What to do instead for strings

The runtime error message already points to the intended pattern for strings: use join.

"".join(["x", "y"])  # "xy"

Custom types and sum()

Any user-defined class that implements __add__ fits the SupportsAdd protocol and is therefore acceptable to static type checkers as an element type for sum. If you rely on the form without a start value, remember that sum operates as if starting from 0, so your type would need to support 0 + instance to work. If it doesn’t, pass a start value of your own type so the first addition is well-defined.

Why this matters

This is a good example of the boundary between Python’s runtime semantics and its static type system. The stdlib’s typeshed stubs give powerful guidance to editors and type checkers, but they don’t encode every special case enforced by CPython. Knowing that _typeshed is a type-checking resource only, that SupportsAdd is about the presence of __add__, and that special runtime rules like the string restriction in sum aren’t necessarily represented at the type level helps avoid surprises and makes type hints more useful day to day.

Takeaways

When you see _Addable in sum’s type hints, read it as “anything with __add__,” as defined by the SupportsAdd protocol in _typeshed. Expect static type checkers to accept sum over any such type, but remember that the runtime can still impose extra rules, like prohibiting strings. Use ''.join(...) for string concatenation, and when using sum with your own classes, provide a start value of the same type if adding to integers isn’t meaningful for your objects.

The article is based on a question from StackOverflow by tarheeljks and an answer by STerliakov.