Circular Relationships in Models

Learn via video courses
Topics Covered

Overview

Circular dependencies are common in many domain models where related objects are interdependent. However, because of their detrimental effects, circular dependencies between larger software modules are viewed as an anti-pattern in software design.

Despite this, it has been discovered that these circular (or cyclic) dependencies are frequently found in the source files of real-world software. However, mutually recursive modules are relatively typical in functional programming, where inductive and recursive definitions are commonly encouraged.

Introductions

What is a Circular Dependency

When some modules are dependent on one another, this is known as circular dependency. The script may experience issues like tight coupling and possible failure as a result.

The Circular dependence needs to be deleted for the script to execute without any errors. There are various techniques to generate circular dependency. These situations can be prevented with a well-designed program.

Additionally, they are known as mutually recursive modules.

For example:

and,

A pretty straightforward circular dependency is shown in the code above. functionP() depends on functionQ(), which in turn calls functionP(). In the following section, we'll go into more detail about these problems with this kind of circular dependency.

Circular Dependencies: Issues

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

Circular dependencies can also cause potential problems like memory leaks, infinite recursions, and cascading effects. It can be very challenging to troubleshoot the numerous potential issues it causes when your code contains a circular dependency, which can happen if you're not careful.

The Circular dependency must be removed for the script to execute without errors. There are many ways to create circular dependency. These situations can be prevented with a well-designed program.

Circular Import in Python

When using the import statement in Python, circular importing, a type of circular dependency, is produced.

  1. A particular kind of circular dependency is Python Circular Imports. When two or more models import one another in Python, the importing connection is repeated, turning the call into an infinite circle.
  2. The Python script returns an error when Circular Imports are used. It is very challenging to locate and manually remove the Python script because it must be removed in order for it to run.
  3. Logic anomalies in the implementation and poor coding design lead to circular imports.

Let's examine the three separate pieces of code below as an example:

Python checks the module registry when importing a module to see if it has already been imported. Python would use the already-existing object from the cache if the module were already registered.

The module registry is a table of initialized modules indexed using the module name. sys.modules provide access to this table.

If the module wasn't registered, Python locates it, initializes it if necessary, and executes it in the namespace of the new module.

In our example, Python loads and runs import module2 after it reaches that point. But module2 also uses module1, and module1 defines function1().

When function2() attempts to call module1's function3(), an issue arises. function3() cannot be called because it hasn't yet been defined. After all, because module1 had to be loaded first, which loaded module2 before reaching it.

Forward References in Python

A forward Reference in computer programming is the Reference of an identifier without a full definition from the programmer. A compiler must know specific characteristics of an identifier, but not others, such as the particular value or purpose it holds.

Solving Circular Import Errors Using Forward Reference

Circular dependencies can indeed be cleared up in Python typing with a forward reference: Let's see this through this,

mypy will effectively typecheck that.

However, if I separate A and B into distinct files or modules: a.py:

b.py:

And if you use mypy to check the package or the modules, it fails:

Code Example of Solving Circular Import Error

So, here's an alternative to combining both into one file to solve that problem. (Tested on Python 3.8.4) Edit:

Regarding the subject of circular imports, I added a trivial __main__.py:

The alternative you can use, as I recently suggested, is the TYPE_CHECKING variable:

Solving Circular Dependencies by Using Design Patterns

Circular imports are typically the result of poor designs. A more thorough examination of the program might have revealed that the dependency isn't actually necessary or that the dependent functionality can be transferred to other modules without the circular reference. Sometimes combining both modules into a single, larger module is a straightforward solution. The final code from the preceding example would resemble this:

However, if the two modules already contain a lot of code, the merged module may have some unrelated functions (tight coupling) and could grow significantly in size.

In the event that that doesn't work, another option would have been to delay the import of module 2 and only import it when necessary. To accomplish this, incorporate the import of module2 into the definition of function1().

In this scenario, Python can load all of the functions in module 1 and only load module 2 when necessary.

The Python documentation states that this method does not violate the syntax because "It is customary but not required to place all import statements at the beginning of a module (or script, for that matter)."

The Python documentation also states that using import X is preferable to other statements like from module import * or from module import a, b, and c.

Deferred importing is a common practice in many code bases even when there isn't a circular dependency because it speeds up startup time; therefore, it is not at all regarded as bad practice (although it may be bad design, depending on your project).

Other Fixes for Circular Imports

There are numerous ways to prevent circular imports. Among them are:

  1. Merge modules
  2. Avoid Circular Import
  3. Change the Name of the Working python script
  • Merge modules: It is best practice to merge the two modules to prevent circular imports when one module depends on another, and that model depends on the first module.
  • Avoid Circular Import: Many times, the operation of one module depends on the function of another model, which depends on it. Most often, this case results in circular imports.
  • Change the name of the Working Python script: The Circular Imports problem can be avoided by renaming the working file to something other than the module that is imported into the script.

Conclusion

  1. We hope this article has clarified the Circular dependencies and how to handle the Circular Import Issue.
  2. Here, we learned about Circular Dependency, Circular Imports, and different approaches to the issue.
  3. We discussed the Python Forward Reference concept and how to use it to resolve circular import errors.
  4. A particular instance of circular references is circular imports. In most cases, they are fixable with better code design. However, the resulting structure occasionally combines unrelated functionalities or is overly code-heavy (tight coupling).
  5. Circular imports reduce the ability to reuse code and produce endless recursions that result in ineffective programming, memory leaks, and even cascading effects. Avoiding circular imports is a wise practice for programmers.