Differences in bound=object vs bound=Any

I’m trying to understand the difference between these three situations:

T = TypeVar('T')  # which I believe is effectively bound=None per CPython source
T = TypeVar('T', bound=Any)
T = TypeVar('T', bound=object)

In typing — Support for type hints — Python 3.11.3 documentation , it says that not specifying a bound means it can be anything, which makes me think 1 and 2 are basically the same. But isn’t it the case that everything in Python is an object, which would also make 3 the same as 1 and 2? The SQLAlchemy codebase uses #2 and #3 and seems to do so intentionally (per some inline comments), so there must be a difference I’m not grokking.

Thanks!

I think the distinction is that bound=Any means that any operation is compatible with the type, so it kind of turning off type checking on that type. For example, it would be valid to do T.upper().

bound=object however says that only things which object can do are valid, so doing T.upper() will not work as object.upper() is not a thing.

I tried out a few things and could not observe a difference between the two type var bounds in mypy or pyright:

from typing import Any, Callable, Generic, Literal, Mapping, TypeVar

T1 = TypeVar("T1", bound=object)
class C1(Generic[T1]): ...

T2 = TypeVar("T2", bound=Any)
class C2(Generic[T2]): ...

C1[type]
C2[type]

C1[type[int]]
C2[type[int]]

C1[Callable[[str], None]]
C2[Callable[[str], None]]

C1[Literal[3]]
C2[Literal[3]]

C1[Mapping[str, int]]
C2[Mapping[str, int]]

class P(Protocol):
    def f(self, x: int) -> None: ...

C1[P]
C2[P]

All were accepted without complaint.

So, I’m guessing the SQLAlchemy devs do it for documentation reasons?

With the code:

from typing import Any, TypeVar

A = TypeVar('A', bound=Any)
O = TypeVar('O', bound=object)


def thing1(a: A):
    a.upper()


def thing2(a: O):
    a.upper()

If I run mypy over it, I get the error:

typetest.py:10: error: "O" has no attribute "upper"  [attr-defined]
Found 1 error in 1 file (checked 1 source file)
1 Like

Type checkers treat the following two equivalently:

T = TypeVar('T')
T = TypeVar('T', bound=object)

Using bound=Any is a strange thing to do. I do not recommend it, like most uses of Any, it is unsound:

from typing import Any, TypeVar

T1 = TypeVar("T1", bound=object)
T2 = TypeVar("T2", bound=Any)

def f(t1: T1, t2: T2):
    reveal_type(t1.asdf)  # error: "T1" has no attribute "asdf"
    reveal_type(t2.asdf)  # no error, despite no guarantee that attribute exists

Type checkers treat bound=None differently from no bound / bound=object. In general, the CPython source code isn’t going to be a good indicator of what static type checkers do.

from typing import Any, TypeVar

T1 = TypeVar("T1", bound=object)
T3 = TypeVar("T3", bound=None)

def takes_t1(t1: T1): ...
def takes_t3(t3: T3): ...

takes_t1(0)  # no error
takes_t3(0)  # error: Value of type variable "T3" of "takes_t3" cannot be "int" 
takes_t1(None)
takes_t3(None)

Edit: my page was stale, sorry for repeating what Matt said!

3 Likes

Type systems often look at types as though they exist in a hierarchy from top to bottom. Types can be silently converted or assigned up the tree but not down.

In Python object is what is known as a top type: you can assign anything to a variable of type object but the only operations you can perform on that variable are the operations valid on all objects.

The Any type is both a top type (so you can assign anything to Any) and a bottom type (so you can assign Any to anything). In effect it turns off the type system whenever you use Any.

If you know any typescript there the equivalents are object and unknown are top types (but unknown has no operations at all defined on it), never is a bottom type (so no other types can be assigned to a never) and any is both top and bottom so again effectively breaks the type system.

As of Python 3.11 we also have a proper bottom type in typing.Never. I’m not aware if we have an equivalent unknown type but for most purposes using object will do (and object is almost always preferable in places people use typing.Any).

So to answer the question, bound=Any is removing all the safety harnesses you want if you’re doing typing at all in Python, bound=object keeps the safety but does mean you will have to explicitly check the actual type if you want to do anything much with the object.

1 Like

Yet sometimes necessary to remain sane :sweat_smile:

Thank you all for the very helpful explanations and examples. I did also end up asking the more specific question in the sqlalchemy forum about the choice to use bound=Any vs bound=object, if you’re curious to see their response there: Why does the SQLAlchemy code distinguish between bound=object and bound=Any? · sqlalchemy/sqlalchemy · Discussion #9835 · GitHub
(I’ll also post a link there to here, back references!)

1 Like