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.
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.
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!
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.