1

In this question How can I separate functions of class into multiple files? the top answer suggests to use

from method_file import method

Inside a class definition to have class methods defined in separate files. However, for a class like this

my_number.py

class MyNumber:
    def __init__(self):
        self.x = 5

    from my_method import my_method

my_method.py

def my_method(self):
    print(self.x)

It would not be clear to the IDE that self refers to a MyNumber object. As a consequence code completion (for e.g. self.x) is not available in my_method. A type hint for self could solve this, i.e.

my_method.py

from my_number import MyNumber

def my_method(self: MyNumber):
    print(self.x)

but this leads to a circular import.

Is there any workaround or best practice for such a situation?

desap
  • 169
  • 1
  • 15
Michael
  • 878
  • 5
  • 17
  • 1
    Idk how your IDE will handle it in this scenario, but `def my_method(self: "MyNumber"):` may work. – Carcigenicate Aug 18 '21 at 17:13
  • 2
    I would think the best practice would be to not split the definition of a class across multiple modules like this. `MyNumber.my_method` will see a different global scope than `MyNumber.__init__`, for example. – chepner Aug 18 '21 at 17:38
  • Also note that the top answer was never accepted. Besides, putting `import`s in the middle of a class definition is a poor programming practice. – martineau Aug 18 '21 at 17:40
  • If `my_method` works *only* for `self: MyNumber`, why do you want to split it off in the first place? – MisterMiyagi Aug 18 '21 at 17:47
  • Thanks for the remarks. The reason for the splitting would be for two reasons. First, there would be about 15 such class methods, each using some helper functions resulting in quite a lot of code for one file. Second, we work as a team on these different methods and it would thus be convenient if people could work in different files. I'm using Studio Code with the Python extension and unfortunately `self: "MyNumber"` isn't recognized. – Michael Aug 18 '21 at 17:57
  • While there are means to do this (see the answers) I think what you should actually do is revisit your design and work flow. VCS and IDE should have no problem with multiple people working on the same file; if the methods responsibilities are so distinct they don't logically should be worked on together, they should not be part of a single class in the first place. – MisterMiyagi Aug 18 '21 at 18:06
  • 1
    What is your concern with "a lot of code" in one file? Proper source control should make multiple people working on a single file a non-issue. – chepner Aug 18 '21 at 18:09
  • 1
    I believe OP's actual question could be rephrased as "how do I avoid circular imports when using type hints + type checkers". Of course "rewrite your code so that it avoids circular imports" is a solution, but I think it's valuable to have an alternative to that. Discussing whether or not it makes sense to bind a function to a class at runtime seems tangencial to that, since the same issue of circular imports + type hints can appear in different contexts. – jfaccioni Aug 18 '21 at 18:26

4 Answers4

1

There is an approach that combines a __future__ import to disregard type annotations at runtime, with a if TYPE_CHECKING clause that "imports" the code from your IDE's point of view only, so that code completion is available.

Example:

my_number.py

class MyNumber:
    def __init__(self):
        self.x = 5

    from my_method import my_method

my_method.py

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from my_number import MyNumber

def my_method(self: MyNumber):
    print(self.x)

With the from __future__ import annotations, we postpone the evaluation of type hints - in other words, we can type hint my_method even if we don't actually import MyNumber. This behavior was planned to be the default in Python 3.10, but it got postponed, so we need this import for now.

Now depending on your editor/IDE, you will still get a warning complaining that MyNumber isn't defined, and its methods/attributes may not show up on the autocomplete. Here's where the TYPE_CHECKING comes into play: this is simply a constant False value, which means that our clause is:

if False:
    from my_number import MyNumber

In other words, we're "tricking" the IDE into thinking we're importing MyNumber, but in reality that line never executes. Thus we avoid the circular import altogether.

This might feel a little hacky, but it works :-) the whole point of the TYPE_CHECKING constant is to allow type checkers to do their job, while not actually importing code at runtime, and to do so in a clear way (Zen of Python: There should be one-- and preferably only one --obvious way to do it).

This approach has worked for me consistently in PyCharm, not sure about other IDEs/editors.

jfaccioni
  • 7,099
  • 1
  • 9
  • 25
  • If you are using `annotations`, there should be no need for an import. `MyNumber` is treated as a literal string, not an expression to be evaluated. (Or is that solely for the IDE's benefit?) – chepner Aug 18 '21 at 18:11
  • 1
    @chepner but then (in most IDEs) code completion for `MyNumber` does not become available in `my_method.py`, which is what OP was complaining about. – jfaccioni Aug 18 '21 at 18:12
  • @chepner The type checker still must know in which namespace MyNumber was defined. Otherwise, it would be ambiguous if it had to guess. – MisterMiyagi Aug 18 '21 at 18:17
  • Right; I overlooked the IDE-autocompletion aspect (as it's not something I typically use). – chepner Aug 18 '21 at 18:18
1

Is there any workaround or best practice for such a situation?

The best practice is not to do this. If a method implementation is specific to a class, it should be part of the class definition.


If a method is not specific to a class, it should be defined across all valid types. A Protocol is appropriate to express this :

from typing import Protocol, Any

class HasX(Protocol):
    x: Any  # might need a TypeVar for complex cases

def my_method(self: HasX):
    print(self.x)

If a method extends a class separate of its definition, it should not be patched in. Use functools.singledispatch to externally define single dispatch functions, which are logically similar to methods:

from functools import singledispatch
from my_number import MyNumber

# not imported into MyNumber
@singledispatch
def my_method(self):
    raise NotImplementedError(f"no dispatch for {type(self}") 

@my_method.register
def _(self: MyNumber):
    print(self.x)
MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
-1

Typically, the self keyword is used to represent an instance of the class. It doesn't make sense to type hint self. Second, you can't access the instance variable x via self if your function is not a method to a class that is a MyNumber object.

I would suggest two options to accomplish what you want to. You can accept a MyNumber object as a parameter to the my_method() function or you can create a new class and inherit the MyNumber class. Make sure the files are in the same directory, otherwise update the import statement in File 2.

Option #1

class MyNumber:
    def __init__(self):
        self.x = 5

def my_method(my_number: MyNumber):
    print(my_number.x)

my_method(MyNumber())

Option #2

#my_number.py
class MyNumber:
    def __init__(self):
        self.x = 5

#my_method.py
from my_number import MyNumber

class MyMethod(MyNumber):
    def __init__(self):
        super().__init__()
        
    def my_method(self):
        print(self.x)

MyMethod().my_method()
desap
  • 169
  • 1
  • 15
  • This answer seems misleading. The code shown in the question does define a method of `MyNumber`, which can access x via self. While not common, there are many viable cases in which `self` should be annotated, either to refer to or restrict the type of `self`. – MisterMiyagi Aug 18 '21 at 18:13
  • What do you mean? there is no method MyNumber in the question...There is a class called MyNumber and that is present in my answer – desap Aug 18 '21 at 18:26
  • The code in the question defines a method *of* `MyNumber`, namely `MyNumber.my_method`. – MisterMiyagi Aug 18 '21 at 18:46
-2

I think you are having problems regarding object-oriented concepts in python. Your "my_method" function doesn't need the "self: MyNumber" as a parameter, in fact, you need to create an object of the MyNumber class and consequently this class will have an attribute that is the "x" since you defined the "x" in the constructor of the MyNumber class. It would look something like this:

#my_number.py
class MyNumber:
  def __init__(self):
    self.x = 5

#my_method.py
from my_number import MyNumber
def my_method():
  mm = MyNumber()
  print(mm.x)
  • Thanks for the answer. Your suggestion would be a more functional programming approach which might indeed be the more suitable one in this case. My question was however on modular class definitions, i.e. defining `my_method(self)` as a class method in a different file. – Michael Aug 18 '21 at 18:00
  • 1
    This answer seems misleading. The code shown in the question does define a method that is passed self as usual. Replacing it by a function that cannot use a preexisting instance is not at all equivalent. – MisterMiyagi Aug 18 '21 at 18:10
  • Sorry, I thought it would help but I ended up misunderstanding your proposal. – Marcos Gôlo Aug 23 '21 at 12:43