Consider the two modules (in the same folder):
firstly, person.py
from typing import List
from .pet import Pet
class Person:
def __init__(self, name: str):
self.name = name
self.pets: List[Pet] = []
def adopt_a_pet(self, pet_name: str):
self.pets.append(Pet(pet_name, self))
and then pet.py
from .person import Person
class Pet:
def __init__(self, name: str, owner: Person):
self.name = name
self.owner = owner
the code above will not work, because of circular dependency. You’ll get an error:
ImportError: cannot import name 'Person'
Some ways to make it work:
- keep the definition of the classes Person and Pet in the same file.
- do away with the pet.owner attribute (which is there as a convenient pointer)
- don’t use type-hinting / annotation where it would cause circular references:
e.g. just have:
class Pet:
def __init__(self, name: str, owner):
I see some drawback in all the options I’ve listed so far.
Is there another way?
One that allows me to
- split classes into different files
- use type annotation in combined with pointers such as shown
Or: is there very good reason to instead follow one of the solutions I’ve already listed?
7
I ran into similar problems recently and solved it by using the following method:
import typing
if typing.TYPE_CHECKING:
from .person import Person
class Pet:
def __init__(self, name: str, owner: 'Person'):
self.name = name
self.owner = owner
There is a second solution described here, which requires Python >= 3.7.
from __future__ import annotations # <-- Additional import.
import typing
if typing.TYPE_CHECKING:
from .person import Person
class Pet:
def __init__(self, name: str, owner: Person): # <-- No more quotes.
self.name = name
self.owner = owner
The __future__
import was set to no longer be required as of 3.10, but that has been delayed.
4
After some more learning, I realized there is a right way to do this: Inheritance:
First I define Person, without [pets] or the method in the OP.
Then I define Pets, with an owner of class Person.
Then I define
from typing import List
from .person import Person
from .pet import Pet
class PetOwner(Person):
def __init__(self, name: str):
super().__init__(name)
self.pets = [] # type: List[Pet]
def adopt_a_pet(self, pet_name: str):
self.pets.append(Pet(pet_name))
All methods in Person that needs to refer to Pet should now be defined in PetOwner and all methods/attributes of Person that are used in Pet need to be defined in Person. If the need arises to use methods/attributes in Pet that are only present in PetOwner, a new child class of Pet, e.g. OwnedPet should be defined.
Of course, if the naming bothers me, I could change from Person and PetOwner to respectively BasePerson and Person or something like that.
1
I had a similar use case of circular dependency error because of type annotation. Consider, the following structure of the project:
my_module
|- __init__.py (empty file)
|- exceptions.py
|- helper.py
Contents:
# exceptions.py
from .helper import log
class BaseException(Exception):
def __init__(self):
log(self)
class CustomException(BaseException):
pass
# helper.py
import logging
from .exceptions import BaseException
def log(exception_obj: BaseException):
logging.error('Exception of type {} occurred'.format(type(exception_obj)))
I solved it by using the technique similar to the one described here
Now, the updated content of helper.py
looks like the following:
# helper.py
import logging
def log(exception_obj: 'BaseException'):
logging.error('Exception of type {} occurred'.format(type(exception_obj)))
Note the added quotes in type annotation of exception_obj
parameter. This helped me to safely remove the import statement which was causing the circular dependency.
Caution: If you’re using IDE (like PyCharm), you still might get suggestion of importing the class and the type hinting by the IDE would not work as expected. But the code runs without any issue. This would be helpful when you want to keep the code annotated for other developers to understand.