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

Docs: confirm that use of None-typed function return value is disallowed #3226

Closed
pkch opened this issue Apr 23, 2017 · 8 comments · Fixed by #15876
Closed

Docs: confirm that use of None-typed function return value is disallowed #3226

pkch opened this issue Apr 23, 2017 · 8 comments · Fixed by #15876

Comments

@pkch
Copy link
Contributor

pkch commented Apr 23, 2017

Regarding an example provided by @Jdban:

from typing import List

def get_first_elem(the_list):  # type:(List[int]) -> int
    return the_list[0]

myList = [1, 2, 3]  # type: List[int]

print get_first_elem(myList.append(0) or myList)  # mypy: error: "append" of "list" does not return a value

There are a few issues:

  1. Mypy rejects safe (albeit slightly unconventional) code
  2. This behavior is not documented (either in PEP 484 or mypy docs)
  3. The error message is confusing

@elazarg

using expression in a conditional only for the side effect is less common (and desired) than forgetting that the function doesn't actually return self. IIUC catching this kind of errors is the whole point of having the Void type in mypy's codebase.

I understand, but I think 2) and 3) are worth fixing. I don't have an opinion on whether special-casing 1) for this particular use case is worth it.

@gvanrossum
Copy link
Member

The example is a horrible hack and I'm glad mypy rejects it. Agreed that (2) and (3) are worth fixing.

@bwo
Copy link
Contributor

bwo commented Jun 22, 2018

I think this behavior is incorrect, regardless of how one feels about the hackiness of the example. Consider (apologies for the length of the code below, but I wanted to work up to a relatively sane example that mypy rejects):

from typing import Set, Union, Callable, Optional, TypeVar

T = TypeVar('T')


def hack(i):
    # type: (int) -> Set[int]
    return reduce(lambda s, i :s.update([i]) or s, range(i), set())


def pointless():
    # type: () -> None
    return None


def higherorder(f):
    # type: (Callable[[], Optional[T]]) -> Optional[T]
    return f()


def pointy():
    # type: () -> Optional[int]
    return None


def highestorder(somecondition):
    # type: (bool) -> Optional[int]
    if somecondition:
        return higherorder(pointless)  # error!
    else:
        return higherorder(pointy)


pointless() is None  # error
higherorder(pointless) is None  # error
higherorder(pointy) is None  # no error (as expected)

hack() I personally do not think is horrible but I'll cop to its being not exactly wonderful (nevertheless, I've written substantially that code); the fact is, though, that set.update does return a value, namely, None. I'm not sure what the intended fix to the error message might be; it might be bad style to use the return value like that but as a matter of types it's not incorrect.

pointless() also clearly does return a value, namely None; there's even an explicit return statement. Nevertheless, if you try to make use of it (in pointless() is None), mypy complains, saying it does not return a value.

What's really surprising to me is that this error comes up for both higherorder(pointless) and highestorder(). It seems reasonable to me that you might have a "just always immediately fail" function (like pointless) and depending on some condition pass it rather than one that might return a non-None result around to other functions. None is a subtype of Optional[T] so this should be hunky-dory. However, both higherorder(pointless) is None at the end of the snippet and return higherorder(pointless) in the definition of highestorder get flagged as not returning a value. Again, I'm not sure what the intended fix to the error message might be, but it surely can't even be a matter of style, in this case! Can it?

@pbatko
Copy link

pbatko commented Feb 14, 2019

I also think this should be allowed since None is actually value and as far as I know mypy is a type checker, not a code style checker.

@ilevkivskyi
Copy link
Member

@pbatko Well, this is your opinion, @gvanrossum expressed a different opinion above. I also think mypy should flag this as error. Here are some points:

  • First of all, the example with generics proposed by @bwo is fixed and now works correctly, mypy only complains if a function is annotated as returning None, not for inferred None returns etc.
  • Second, the fact that functions/methods in Python that mutate the value also return None is on purpose, to emphasize the mutation. Compare for example sorted([1, 2, 3]) and [1, 2, 3].sort(). This way people will not mix procedural and functional (pure) styles, that can cause hard to track bugs.
  • If someone really wants to use functional style, one should just define a pure wrapper. Again using an example by @bwo:
    def update_pure(s: Set[T], items: Iterable[T]) -> Set[T]:
        s = s.copy()  # This is up to the user, depending on safety vs performance.
        s.update(items)
        return s
    
    reduce(lambda s, i : update_pure(s, [i]), range(i), set())
    mypy will be perfectly happy with this, and similar for list.append().
  • The error message is indeed confusing and should be replaced with something more descriptive/actionable (and it also should be documented).

@gvanrossum
Copy link
Member

Yeah, let’s just update the docs and the error message.

@MSK61
Copy link

MSK61 commented Jul 1, 2021

I'm on the side of allowing functions that explicitly return None to be allowed as a return value by mypy. Maybe we just need some way to tell mypy that "Yes, we indeed intend to return `None' and allow it to be used as a value from within the caller's context." as opposed to "No, we're assuming this function shouldn't return anything that can be inadvertently used within the caller's context." My Thoughts go into a direction like this:

import typing

def func_that_doesnt_return_anything() -> None:
    ...


def func_that_explicitly_returns_none() -> typing.Literal[None]:
    return None


l = []
l.append(func_that_explicitly_returns_none())  # mypy should allow this.
l.append(func_that_doesnt_return_anything())  # mypy should still flag an error here.

There may be other smarter ways but the point is that we can cater for a way to explicitly allow the return values from None-returning functions to be used as valid values within the caller's context.

@carlosgmartin
Copy link

May I suggest changing the error message from does not return a value to something like does not return a value (or returns None), to assist users confused by the fact that their function does return something (that just so happens to be None)?

@JukkaL
Copy link
Collaborator

JukkaL commented Apr 23, 2023

@carlosgmartin I like the idea.

hauntsaninja added a commit that referenced this issue Sep 2, 2023
Fixes #3226.

Aims to provide better assistance to users who may be confused when
their void functions technically return None.

Co-authored-by: Ilya Priven <ilya.konstantinov@gmail.com>
Co-authored-by: hauntsaninja <hauntsaninja@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants