pytoolz / toolz

A functional standard library for Python.
http://toolz.readthedocs.org/
Other
4.66k stars 259 forks source link

Loss of typing information on composed functions #569

Open muxator opened 1 year ago

muxator commented 1 year ago

Passing any function through compose() seems to strip away all the information about the types of those functions.

Minimal example:

import toolz

def f(x: int) -> str:
    return f"{x}"

# compose f with the identity function
g = toolz.compose(f)

reveal_type(f(1))
reveal_type(g(1))

Testing on python 3.11 yields:

$ mypy --version
mypy 1.5.1 (compiled: yes)

$ mypy test_toolz.py 
test_toolz.py:11: note: Revealed type is "builtins.str"
test_toolz.py:12: note: Revealed type is "Any"
$ pyright --version
pyright 1.1.323

$ pyright test_toolz.py 
<BASE>/test_toolz.py
  <BASE>/test_toolz.py:11:13 - information: Type of "f(1)" is "str"
  <BASE>/test_toolz.py:12:13 - information: Type of "g(1)" is "Unknown | Literal[1]"

I expected that the static type checker would have printed builtins.str even for g(1).

This is a stripped down example of a larger problem I had early on on a project of mine, where I wrongly assumed that composed functions would have brought over the type information about their signature. I ended up introducing a bug in my project because of this assumption.

Is this use case something that toolz could ever support? I have no idea if this is due to a deep limitation in the Python language or is simply something that eventually needs to be worked on.

I also read #496, but I do not know if my problem could be solved by working on that issue.

Thank you for the library.

muxator commented 1 year ago

Additional note:

calling import signature ; print(inspect.signature(g)) prints the correct answer:

(x: int) -> str

But I suspect this is done at runtime, so there is no benefit for my use case. I am curious now: what would a language such as OCaml do?

n.b.: I also tried a different library, more limited in scope (https://pypi.org/project/compose/), which behaves exactly as toolz (mypy does not infer the types of the composed functions, while inspect.signature() does).

mentalisttraceur commented 1 year ago

Update re: last comment: my https://pypi.org/project/compose/ now has optional type hints (available in https://pypi.org/project/compose-stubs/ )

Notes for the toolz community based on my experience type-hinting compose:

  1. Turns out that type hints for a variadic compose are possible, they just have to be brute-forced with an overload for each arity.
  2. I initially did overloads for arities up to 256, but that caused huge delays when checking types. I then dialed it down to 16, which reliably got feels-basically-immediate type-checking even on cold start. (For type-checker implementers, there is low-hanging optimization fruit here which might be worth exploring: instead of iterating through all overloads when checking a call, separate overloads by arity and check only the ones which can match the number of arguments in the call.)
  3. When type-hinting a class with callable instances, the type hint overloads can be plain functions.
  4. Doing a separate type stubs package can be very freeing re: backwards-compatibility, since the needs of running code are different than the needs of static type-checking (when writing code that needs to be backwards compatible, it's usually fine to just do type-checking on newer Pythons so long as it runs the same on older Pythons).