Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PyCharm: 'Function Doesn't Return Anything'

I just started working with PyCharm Community Edition 2016.3.2 today. Every time I assign a value from my function at_square, it warns me that 'Function at_square doesn't return anything,' but it definitely does in every instance unless an error is raised during execution, and every use of the function is behaving as expected. I want to know why PyCharm thinks it doesn't and if there's anything I can do to correct it. (I know there is an option to suppress the warning for that particular function, but it does so by inserting a commented line in my code above the function, and I find it just as annoying to have to remember to take that out at the end of the project.)

This is the function in question:

def at_square(self, square):
    """ Return the value at the given square """
    if type(square) == str:
        file, rank = Board.tup_from_an(square)
    elif type(square) == tuple:
        file, rank = square
    else:
        raise ValueError("Expected tuple or AN str, got " + str(type(square)))

    if not 0 <= file <= 7:
        raise ValueError("File out of range: " + str(file))
    if not 0 <= rank <= 7:
        raise ValueError("Rank out of range: " + str(rank))

    return self.board[file][rank]

If it matters, this is more precisely a method of an object. I stuck with the term 'function' because that is the language PyCharm is using.

My only thought is that my use of error raising might be confusing PyCharm, but that seems too simple. (Please feel free to critique my error raising, as I'm not sure this is the idiomatic way to do it.)

Update: Humorously, if I remove the return line altogether, the warning goes away and returns immediately when I put it back. It also goes away if I replace self.board[file][rank] with a constant value like 8. Changing file or rank to constant values does not remove the warning, so I gather that PyCharm is somehow confused about the nature of self.board, which is a list of 8 other lists.

Update: Per the suggestion of @StephenRauch, I created a minimal example that reflects everything relevant to data assignment done by at_square:

class Obj:
    def __init__(self):
        self.nested_list = [[0],[1]]

    @staticmethod
    def tup_method(data):
        return tuple(data)

    def method(self,data):
        x,y = Obj.tup_method(data)
        return self.nested_list[x][y]

    def other_method(self,data):
        value = self.method(data)
        print(value)

x = Obj()
x.other_method([1,2])

PyCharm doesn't give any warnings for this. In at_square, I've tried commenting out every single line down to the two following:

def at_square(self, square):
    file, rank = Board.tup_from_an(square)
    return self.board[file][rank]

PyCharm gives the same warning. If I leave only the return line, then and only then does the warning disappear. PyCharm appears to be confused by the simultaneous assignment of file and rank via tup_from_an. Here is the code for that method:

@staticmethod
def tup_from_an(an):
    """ Convert a square in algebraic notation into a coordinate tuple """
    if an[0] in Board.a_file_dict:
        file = Board.a_file_dict[an[0]]
    else:
        raise ValueError("Invalid an syntax (file out of range a-h): " + str(an))

    if not an[1].isnumeric():
        raise ValueError("Invalid an syntax (rank out of range 1-8): " + str(an))
    elif int(an[1]) - 1 in Board.n_file_dict:
        rank = int(an[1]) - 1
    else:
        raise ValueError("Invalid an syntax (rank out of range 1-8): " + str(an))

    return file, rank

Update: In its constructor, the class Board (which is the parent class for all these methods) saves a reference to the instance in a static variable instance. self.at_square(square) gives the warning, while Board.instance.at_square(square) does not. I'm still going to use the former where appropriate, but that could shed some light on what PyCharm is thinking.

like image 808
Rammschnev Avatar asked Feb 09 '17 02:02

Rammschnev


People also ask

Why is my Python function returning nothing?

Python Function without return statement Every function in Python returns something. If the function doesn't have any return statement, then it returns None .

What happens if a function does not return anything?

Not using return statement in void return type function: When a function does not return anything, the void return type is used. So if there is a void return type in the function definition, then there will be no return statement inside that function (generally).

Can a function not return any value?

Some functions don't return any value.

What do you call a function that doesn't return?

Void (NonValue-Returning) functions: Void functions are created and used just like value-returning functions except they do not return a value after the function executes.


1 Answers

PyCharm assumes a missing return value if the return value statically evaluates to None. This can happen if initialising values using None, and changing their type later on.

class Foo:
    def __init__(self):
        self.qux = [None]  # infers type for Foo().qux as List[None]

    def bar(self):
        return self.qux[0]  # infers return type as None

At this point, Foo.bar is statically inferred as (self: Foo) -> None. Dynamically changing the type of qux via side-effects does not update this:

foo = Foo()
foo.qux = [2]  # *dynamic* type of foo.bar() is now ``(self: Foo) -> int``
foo_bar = foo.bar()  # Function 'bar' still has same *static* type

The problem is that you are overwriting a statically inferred class attribute by means of a dynamically assigned instance attribute. That is simply not feasible for static analysis to catch in general.

You can fix this with an explicit type hint.

import typing


class Foo:
    def __init__(self):
        self.qux = [None]  # type: typing.List[int]

    def bar(self):
        return self.qux[0]  # infers return type as int

Since Python 3.5, you can also use inline type hints. These are especially useful for return types.

import typing


class Foo:
    def __init__(self):
        # initial type hint to enable inference
        self.qux: typing.List[int] = [None]

    # explicit return type hint to override inference
    def bar(self) -> int:
        return self.qux[0]  # infers return type as int

Note that it is still a good idea to rely on inference where it works! Annotating only self.qux makes it easier to change the type later on. Annotating bar is mostly useful for documentation and to override incorrect inference.

If you need to support pre-3.5, you can also use stub files. Say your class is in foomodule.py, create a file called foomodule.pyi. Inside, just add the annotated fields and function signatures; you can (and should) leave out the bodies.

import typing


class Foo:
    # type hint for fields
    qux: typing.List[int]

    # explicit return type hint to override inference
    def bar(self) -> int:
        ...
like image 145
MisterMiyagi Avatar answered Nov 15 '22 05:11

MisterMiyagi