How do we handle circular dependency between Python classes?

In this article we are going to discuss how to handle the circular dependency between Python classes. First of all, let us understand what is circular dependency.

When two or more modules depend on one another, this is known as a circular dependency. This is because each module is defined in terms of the other module.

What is Circular Dependency?

Following is an example of circular dependency ?

functionE():
   functionF()

And

functionF():
   functionE()

The code shown above clearly shows a circular dependency. FunctionE() calls functionF(), which depends on it, and functionF() calls functionE(). There are some apparent issues with this kind of circular dependency, which we'll go over in more detail in the following section.

Function E Function F calls calls

Problems with Circular Dependency

Circular dependencies might result in a variety of issues with your code. For instance, it can lead to a tight coupling between modules, which would restrict code reuse. This aspect also makes long-term code maintenance more challenging.

Circular dependencies can also be the cause of possible problems like memory leaks, infinite recursions, and cascading effects. It can be very challenging to troubleshoot the numerous possible issues it produces when your code contains a circular dependency.

Memory Leak Example

When the classes of any objects involved in the circular reference have a custom __del__ function, a problem arises. Following is an example showing problem occurring with circular dependency ?

class Python:
    def __init__(self):
        print("Object Python is Created")
    def __del__(self):
        print("Object Python is Destroyed")

class Program:
    def __init__(self):
        print("Object Program is Created")
    def __del__(self):
        print("Object Program is Destroyed")

# Create the two objects
py_obj = Python()
pr_obj = Program()

# Set up the circular reference
py_obj.pr_ref = pr_obj
pr_obj.py_ref = py_obj

# Delete the objects
del py_obj
del pr_obj

The output of the above code is ?

Object Python is Created
Object Program is Created

Here, both objects have a custom __del__ function and are holding references to one another. In the end, the __del__ methods were not called when we attempted to manually delete the objects, indicating that the objects had not been destroyed and had instead caused a memory leak.

In this situation, Python's garbage collector is unable to collect objects for memory cleanup because it is unsure of what order to call the __del__ functions in.

Fixing Memory Leak with weakref

Circular references can lead to memory leaks, which can be avoided using the weakref() function in Python. In Python, the weakref function provides a weak reference that is insufficient to maintain the object's life. The object is free to be destroyed by the garbage collector when the only remaining references to an object are weak references.

Example

Following example shows how to fix memory leak in circular dependency ?

import weakref

class Python:
    def __init__(self):
        print("Object Python is Created")
    def __del__(self):
        print("Object Python is Destroyed")

class Program:
    def __init__(self):
        print("Object Program is Created")
    def __del__(self):
        print("Object Program is Destroyed")

# Create the two objects
py_obj = Python()
pr_obj = Program()

# Set up the weak circular reference
py_obj.pr_ref = weakref.ref(pr_obj)
pr_obj.py_ref = weakref.ref(py_obj)

# Delete the objects
del py_obj
del pr_obj

The output of the above code is ?

Object Python is Created
Object Program is Created
Object Python is Destroyed
Object Program is Destroyed

As you can see, both of the __del__ methods were called this time, proving that the objects were successfully removed from memory.

Circular Import Dependencies

The import statement in Python creates circular importing, a type of circular dependency. This happens when two modules try to import each other.

Problematic Example

The following example explains this. Assume we have created 3 python files as shown below ?

module3.py

# module3.py
import module4

def func8():
    module4.func4()

def func9():
    print('Welcome to TutorialsPoint')

module4.py

# module4.py
import module3

def func4():
    print('Thank You!')
    module3.func9()

main.py

# main.py
import module3
module3.func8()

The issue arises when func4() attempts to call func9() in module3. Since module3 was loaded first and loaded module4 before it could complete initialization, func9() may not be accessible yet.

Fixing Circular Import Dependencies

Circular imports are typically the result of poor designs. Here are two common solutions:

Method 1: Combine Modules

Sometimes combining both modules into a single, larger module is a simple option ?

# combined_module.py
def func8():
    func4()

def func4():
    print('Thank You!')
    func9()

def func9():
    print('Welcome to TutorialsPoint')

# Execute the functions
func8()

The output of the above code is ?

Thank You!
Welcome to TutorialsPoint

Method 2: Local Import

Another option is to delay the import and only import it when necessary. Move the import statement inside the function ?

# module3.py
def func8():
    import module4  # Import only when needed
    module4.func4()

def func9():
    print('Welcome to TutorialsPoint')

Python will be able to load all of the functions in module3 and only load module4 when necessary. This method does not violate Python syntax, though it is customary to place import statements at the beginning of a module.

Conclusion

Circular dependencies can cause memory leaks and import errors. Use weakref to break reference cycles between objects, and restructure code to avoid circular imports through better design or local imports.

Updated on: 2026-03-24T19:45:59+05:30

5K+ Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements