Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loss of typing information on composed functions #569

Open
muxator opened this issue Aug 17, 2023 · 2 comments
Open

Loss of typing information on composed functions #569

muxator opened this issue Aug 17, 2023 · 2 comments

Comments

@muxator
Copy link

muxator commented Aug 17, 2023

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
Copy link
Author

muxator commented Aug 17, 2023

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
Copy link

mentalisttraceur commented Aug 18, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants