pyright
pyright copied to clipboard
Strict Setting for Return Types
Is your feature request related to a problem? Please describe. Can we have a config option for a library to require return type annotations? So that code like,
def f(x: int):
return x
is an error on strict for missing return type.
Describe the solution you'd like A configuration setting for missing return type annotations. May optionally be included in strict.
Additional context While pyright can infer return types, I would like return type for explicitness/documentation and also to be friendly to other type checkers. Also inferred return type may be valid but not intended one by the author.
It would also make strict check vs verifytypes more similar. verifytypes rightly detects missing return types, but I'd rather not run verifytypes for every pre-commit/ci check while running pyright there is fine to me.
I presume you mean emitting an error in the library source file. Pyright doesn't emit errors for library code when analyzing code that consumes libraries. It emits errors only for files that it has been asked to type check.
If you want an error emitted at the call site, there's already a set of options that covers this case. Set useTypesFromLibraryCode to false (which the default value for this setting in pyright). Then turn on strict type checking options. This will disable type inference for library code, so return types will be evaluated as Unknown, and any use of an Unknown will be flagged by the strict type checking options.
I mean for my own code/files that pyright checks. I'm calling it a library since I mainly work on code for others to use as a library. Sorry for making that confusing. If I make a file foo.py with contents,
def f(x: int):
return x
and run pyright foo.py then I get no errors even on strict mode. So I want files I ask pyright to type check are annotated with return types. I agree that errors from libraries my code uses should not be shown.
In cases where the function type can be inferred, there's no reason to provide a return type annotation unless the function is part of a library's public interface. In that case, the return type should be explicitly annotated. That can already be checked with the "pyright --verifytypes" command. Outside of the "--verifytypes" command, there's no way for pyright to know whether any particular function or method is public, and I don't think it makes sense to provide a setting that requires return type annotations for internal functions and methods.
I'd be happy with just a rule for all functions. My primary reasons for using types are readability and detection of errors. My goal for return types is clarity/documentation to other people working in the codebase. I find return types help readability. So I want private functions to have return types. Also if we want our codebase to mypy strict check, mypy has no inference of return types and this is largest inconsistency between strict mypy vs pyright errors. We're currently leaning to drop mypy for other reasons and just use pyright for other reasons but will miss having return types documented.
If you'd prefer a public interface rule a simple one would be underscore convention. I don't think it's necessary to distinguish though if this an optional strict rule. The simplest rule of all functions have return types would be easier and fit happily for me.
As for verifytypes, verifytypes does not show errors while using pylance. I mostly fix type errors by relying on pylance to show me them and use pyright for CI/debugging.
We wouldn't be able to include such a feature in the general "strict" category, so it would likely go undiscovered and unused by pretty much everyone but you. We generally don't add new features that are so specific that only a handful of pyright users want them.
I'm going to close this for now. We might reconsider the decision in the future if we receive signal of interest from other users in the form of upvotes (thumbs up).
@erictraut Can you re-open this issue? I think this option is required
@sawaca96, I'm going to leave the issue closed for now. The issue has received some thumbs up but not enough to justify adding it. We may revisit this in the future if there is sufficient interest, but I'm still unconvinced that this is a good feature to add for all of the reasons I mentioned above.
I think this issue has received enough upvotes for us to open it up for discussion again.
I'm still not 100% convinced that we should include such a feature. To summarize the arguments against adding this:
- If we add the feature, it will be off by default, even in strict mode, so it will likely be discovered and used by a very small percentage of pyright and pylance users.
- It will do little or nothing to catch bugs in your code that are not already caught by pyright today. It effectively just creates a bunch of extra unnecessary work for little or no benefit.
- Inferred return types already appear in hover text and signature help.
- Pylance also recently added support for "inlayed" type annotations, so the inferred return types appear within the code.
The arguments I've heard in favor of this feature:
- Mypy doesn't provide return type inference, so return type annotations are always needed for mypy. This sounds to me like a problem in mypy, not in pyright ;). If you run mypy in addition to pyright, you can ask mypy to report missing return type annotations, right?
- For library authors who use "--verifytypes", this command requires that all public functions and methods (other than
__init__) have explicit return types, and it would be useful for pyright to report a similar error when used in interactive editing mode. The problem with this argument is that when pyright is used in normal mode, it can't tell what functions make up the public interface for a library. So consistency here isn't possible. - Inferred return types might be accurate but not what was intended by the author. I think this is the strongest argument in favor of this feature, but it's still pretty weak IMO. Authors can always override the inferred type by adding an (optional) return type annotation. This is consistent with how variable type annotations work.
I'm interested in hearing from those of you who upvoted this feature. How and why would you use it?
There is 4th reason,
- Useful as documentation to others reading the code and in generated documentation that extract type hints. Sphinx-autodoc-typehints is one tool that can generate api documentation including type signatures it finds.
With your three that does cover all my reasons.
The recent support for inlayed type hints and adding them definitely helps and lowers my want for this.
I am a bit confused by 2. Why can’t single file mode recognize public interface? Is rule more complex then leading underscore functions are private?
The rule for "what is the public interface" is a bit more complicated, at least for modules whose names start with an underscore or are located within a directory whose name starts with an underscore. In those cases, it's necessary to understand whether that symbol is imported and re-exported from a publicly-visible module. The --verifytypes logic performs this cross-module analysis.
After further deliberation and discussion with the pylance team, we've concluded that this feature is not sufficiently compelling for the reasons discussed above. As such, I'm going to close it again.
The Pylance team is planning to add a "fix all" code action that adds explicit return types for all functions and methods within a file based on inferred types.
Not very constructive, but I'm quite surprised this is not an option coming from mypy
Would also like to see this
As discussed above, enforcing the presence of return type annotations will do little or nothing to improve the type safety of your code when you're using pyright.
If you're using mypy as a type checker, return type annotations are important because mypy does not perform return type inference. It assumes an implied return type of Any if a return type isn't provided.
If you're using pyright as a type checker, return type annotations are really more an issue of "code style" — something that falls more in the domain of a linter. If you want to enforce a rule like this, I recommend running ruff or pylint on your code. They are perfectly capable of flagging functions without return type annotations. They don't need to perform any static type analysis to enforce this sort of a code style rule.
enforcing the presence of return type annotations will do little or nothing to improve the type safety of your code when you're using pyright
return type annotations are important because mypy does not perform return type inference
I wanted to add a dissenting voice, I think adding explicit return type annotations everywhere does improve type safety and that relying on type inference is not ideal. I think it should be an option supported by pyright.
For one, return types are good documentation and allow one to read and understand a function in isolation. It's clear in the definition what goes in and what goes out, you don't have to understand the whole codebase. E.g. what does foo() return here?
def foo():
return bar()
More importantly, it can be very easy to change the return type of a function inadvertently and transitively. Changing the return type of bar() also changes the return type of foo() and all places it's used. Especially easy in practice to return a None where before the type was T -- now this becomes T | None. Say you just serialize this return type, this is a newly introduced silent bug (happened to us multiple times in both Python and Typescript and have since switched to enforcing explicit return types).
You can argue this is a feature, that all the places where the function is used will continue to work as long as the new return type works in the place. I'd argue that if I modify a function, I shouldn't have to reason whether the new return type makes sense everywhere the function is used. This is a decision that should be made explicitly at the call site. Explicit is better than implicit.
enforcing the presence of return type annotations will do little or nothing to improve the type safety of your code when you're using pyright.
weirdly specific counter-example:
async def baz(arg: int) -> None:
# do stuff
pass
async def bar(big: bool): # no type annotation
if big:
# missing await
return baz(1000) # Awaitable[None]
else:
return await baz(1) # None
async def main():
awaitable = bar(big=True) # type: Awaitable[Awaitable[None] | None]
await awaitable
This is an obvious bug (baz might not get awaited) that I would very much like the typechecker to catch! And it does catch it when a type annotation on await_bar exists. But if we allow type inference, it suddenly doesn't matter whether the function is returning a None or an Awaitable[None].
As others have noted, the assertion:
enforcing the presence of return type annotations will do little or nothing to improve the type safety of your code when you're using pyright.
Is just plain incorrect. Inference can't know author intent and can easily hide bugs (as others have called out) and infer types which are either too specific or too broad, depending on the function implementation. Additionally, explicit return types can aid readers of both "internal" and "public" APIs, so limiting enforcement to the latter is unreasonably limiting.
It has been a few years since we last considered this enhancement request. Since then, it has racked up more upvotes, so I think it's time to reopen it for consideration.
Let me start by saying that I try to be principled and consistent when making feature decisions for pyright. One principle that I've tried to follow is that pyright is a type checker and not a linter. (I violated this principle a few times early in pyright's development, and I've regretted it.) As a type checker, pyright should stick to enforcing type consistency rules. It should not enforce coding convention rules. The latter is the domain of a linter. Linters and type checkers both have their place in the programmer's tool chest, but it's best if their roles are distinct. From my perspective, the requested feature falls under the domain of a linter, not a type checker.
Let me address a few of the specific points that were posted above.
Inference ... can infer types which are either too specific or too broad, depending on the function implementation
I agree that inference can infer a return type that is narrower or wider than what the author intended. You are free to add return type annotations in such cases if you want to be explicit. However, I stand by my assertion that if you omit a return type annotation — even if the inferred type is narrower or wider than what you intended, type safety is still guaranteed if you use pyright to type check both the caller and the callee. If you make a change in either location (caller or callee) that results in a type violation, pyright will inform you of that violation.
There are cases where code can be made more robust if a return type annotation is provided. A few examples are provided above where bugs would be caught with the addition of a return type annotation. However, there are many situations (more often than not, in my experience) where return type annotations provide little or no value in terms of type safety, readability or maintainability. They become pure maintenance overhead in these cases. Simple accessor methods or property getters are good examples. Determining when a return type annotation is going to be useful is easy in most cases. If you find that you're unsure, then it's a good idea to add an explicit return type annotation.
One could make the same argument for variable type annotations. There are situations where adding an explicit type annotation for a local variable can improve code robustness and readability. Yet, I haven't heard anyone suggesting that there should be a type checker feature that enforces type annotations for all local variables. Just as type inference works fine most of the time for local variables, it also works fine for most return types.
Explicit return types can aid readers of both "internal" and "public" APIs
I agree that explicit return types can provide value in terms of readability and documentation. Similarly, docstrings can aid readers in understanding an API. However, one would probably not expect a type checker to enforce that every method has a docstring. This is what I mean when I say that this feels like the domain of "coding conventions" — the responsibility of a linter, not a type checker.
I'm going to leave this issue (re)opened for a little while to gather additional input and feedback. So far, I've found the arguments above to be less than convincing, but I'm open to new arguments and perspectives. If you feel strongly that this should be a feature in a type checker (as opposed to a linter), please explain why.
I like explicit return types for the code readibility and they also allow me to create a constraint for the function implementation, similar to how usual tests work but in terms of typing, warning you when you stepped out of the way. I even had an idea to propagate them even deeper into function body to ensure typing integrity.
I've started reading this thread looking for such feature in pyright, it seemed logical - type checker = warnings for missing type annotations, but now I strongly agree that it's indeed just a linter feature, not type checker's. And it's very simple one, since it's not connected to type checker features in any way, e.g. something like ANN201 and ruff check test.py --select ANN201 should cover it, unless someone is strongly prefers using just pyright as a tool for the codebase.
In my opinion, return types in pyright is just a tool. Sometimes they can be useful, sometimes they just create noise. And if pyright would implement return types requirement, it may also suggest to users that "at the end of the day providing return type annotations is the most type safe way", which would contradict with strong reliance on type inference that pyright does have.
Docstring correctness isn't reasonably enforced by a checker; type annotations are and generally serve as a more reliable signal of intent than a docstring or comment, either of which may get out of date.
Local variables have substantially more limited scope than return values, the latter of which generally comprise API boundaries while the former do not and thus have a much broader surface area if an intended implementation detail accidentally spills out into surrounding code. Mandatory return value annotations makes it much less likely for such details to leak out in the first place (e.g. return filter(....) will be inferred as returning the filter type, which is more likely to be an implementation detail than an intention API choice and an easy mistake to overlook.)
Thanks for your perspectives.
I don't find these arguments compelling enough to justify a change, so I'm going to once again close this issue.
If you want to enforce that all functions in your code base have explicit return type annotations, you can enable that rule (or rules) in your linter of choice. For example, flake8 and ruff offer missing-return-type-undocumented-public-function, missing-return-type-private-function/, etc.