Type inference for function return types

Fair enough, not all strictly-typed languages… But do they do the kind of type inference being described here? I haven’t written anything like ocaml in a long time but I remember annotating all my variables…

When implemented correctly, inference allows you to avoid handcrafting and the perils associated with complex handcrafted types – and an auto-generated type is indistinguishable from a handcrafted one, which leads right back to the same set of verbosity and correctness questions.

Even if I can see the advantage of this, the fact that inferred-return-types can change implicitly due to small code changes is too great of a problem to allow this, IMHO.

Type checkers and users should be able to look at the signature and docstring only in order to understand the parameters and return types, without having to check the body of the function.

IMHO this inferred return types should not be implemented, as much as it sounds harmless and a good idea at first glance.

Return type inference has been implemented in pyright for several years, and it’s widely used and highly appreciated by pyright users. If you haven’t used a type checker or a language that supports return type inference, I recommend that you try it before you criticize it.

I do not recommend that library authors rely on return type inference for a library’s public interface. A library’s interface must be unambiguous (not depend on inference behaviors, which can vary between type checkers) and stable over time, so explicit return type annotations are important here. Pyright includes a “–verifytypes” option that will identify library symbols that are untyped or ambiguously typed due to reliance on inference.

For non-library code, return type inference works very well in my experience. In code bases type checked with pyright, it’s typical for two-thirds of functions and methods to have no explicit return type annotations. The same is true of code bases written in TypeScript. In my experience, this doesn’t make the code less readable or less type safe. If a change to your code causes the inferred return type to change in a way that violates an assumption elsewhere in your code, the type checker will notify you of that type violation.

Of course, if you want to provide explicit return type annotations in your code, you have that option. And if you want to enforce that every def statement in your code base includes a return type annotation, that’s a coding convention choice that can easily be enforced by a linter. It’s not a rule that should be forced on everyone, however.

6 Likes

Thanks for the thoughtful response.

I appreciate that you acknowledge the distinction between public API and internal code, and how important is to use unambiguous typing in public API. My fear is that with return type inference in place, it will be very easy (perhaps even encouraged) to use this everywhere, without users realizing the consequences of that, but I might wrong.

At this point I would say pyre infer doesn’t work terribly well, it’s probably not a general answer to the need for this kind of tool.


For what it’s worth, we are taking a long look at Pyre’s core algorithm, which in many ways doesn’t work well in many ways. The three most important examples:

  • we never infer lambda types, they are completely unsafe in Pyre
  • we’re never able to type containers initialized to be empty, they just turn into containers of Any
  • even with an explicit annotation, we often force users to suppress variance errors in complex container literals because we too-eagerly specify a narrow invariant type for inner data

The biggest problem here is that Pyre tries to operate mainly as a “naive” type inference engine, eagerly computing the types of expressions, which makes a lot of type inference impossible so we just give up and use Any. We’ll almost certainly need to replace this with either bidirectional inference (which is what MyPy uses, according to their docs) or a wider-scope constraint solver.

We implement this as an abstract interpretation, which allows pyre infer to add a backward pass and propagate more information, but the fundamental limitations of overly eager type inference aren’t fixed in pyre infer and can’t be without a near-complete rewrite.

Regardless of what we decide to do in pyre check we want to make a powerful pyre infer, a function-level constraint solver would likely work pretty well and in many cases could give us function return types. I think it might be hard to always get a high-quality answer because a “principle” type isn’t always available, but I suspect a majority of return annotations have one. I don’t know much about Pyright but I’d guess it’s doing something along these lines under the hood.

1 Like

For what it’s worth, I believe PyType uses a fairly wide-scope constraint solver and has powerful type inference.

I haven’t used it personally but @rchen152 might be able to say more; I know @martindemello was working on using some of the same logic pyre infer uses to inject annotations as a way to spit out the inferred types from PyType.

In my opinion, this is an argument for inferred return types. If the signature of a function changes, I would want to get errors if a caller uses this function incorrectly. I don’t get those if an unannotated return type is equivalent to Any.

I still recommend to explicitly annotate return types, but: Explicitly annotated > Implicitly annotated > Any.

2 Likes

Sure, I agree with that for functions which do not have type annotations currently.

My concern is that the feature will encourage people to stop writing return type annotations, relying on the return type being inferred.

2 Likes

Yeah I think there’s a mix of use-cases in this discussion, as described by @erictraut above: it’s great to have inferred return types while writing or analyzing code for errors. This is really just a consequence of have type inference at all–a function is just a chunk of code, and type analysis should be able to traverse through that code. I think many type checkers do this already.

I interpreted the point of this thread as being about some formalization of inference as a replacement for annotating return types. I think that’s a bad idea for the reasons @nicoddemus outlined and Eric also mentioned–in libraries, the return annotation is part of the API contract, and relying on inference will lead to bugs. The only way to prevent those bugs is through testing of some kind, which is the equivalent of just annotating the type, but more laborious.

Thanks @jamestwebber for the great synopsis of the thread. Indeed there are (at least) two use cases being discussed here, and my concern is regarding public APIs, not internal usage.

1 Like

For what it’s worth, even though pytype can infer function return types, at Google we still settled on recommending that public APIs be explicitly type annotated (styleguide | Style guides for Google-originated open-source projects), for clarity and readability. Where I see users taking advantage of type inference is in non-public code, where, e.g., they might omit a bunch of -> None annotations that are tedious to write and easy to infer.

1 Like