What would the purpose of circular modules like this be? You may as well collapse into a single module and the situation would not be any different would it?
What would the purpose of circular modules like this be? You may as well collapse into a single module and the situation would not be any different would it?
What is the purpose of modules? You may as well collapse into a single script and the situation would not be any different, would it?
I'm not being facetious here. The answer to the second is the answer to the first.
A common example might go like this. You have a module for each kind of thing you have in the database. But now if someone loads a Company, they need to get to Employees. And if someone loads Employee they need to get to Accounting for the salary, payments, etc. And Accounting needs to be able to get to Company.
Those are all large and complicated enough that it makes sense to make them modules. But you just created a circular dependency!
The standard solution is to load a base library that loads all kinds of objects so they all can assume that all the others already exist and don't need a circular dependency. But of course someone won't like wasting all that memory for things you don't need and ...
So parts of the system can be managed independently.
> The answer to the second is the answer to the first.
Clearly not - since circular modules cannot be managed independently!
In the example that I gave, the design described will handle complex edge cases such as a part time employee working for multiple companies. And will do so without programmers having to think through the whole system at all points.
Independence of modules has no importance in a codebase that ships as a whole. But modularity does.
Only if those things not only need to be able to “get to” each other, but also need to know, at compile time, about the concrete implementation of the others.
That's possible to be a real need, but its also something that often happens because of excessive and unnecessary coupling.
However your "compile time" point is important. There is another solution, which is to implement lazy loading of those classes.
So you put your import in the method of each that needs to know the other. This breaks the circular dependency and needs more up front memory. However it can also become a maintenance issue where a forgotten import in one function is masked by a successful import in another, until something changes the call and previously working code mysteriously goes boom.
It's all tradeoffs.
The common approach to solving this is pulling everything that is used by all the modules into leaf libraries, effectively creating a directed acyclic graph, but this is not obvious nor easy to do the first time.
Boom, circular dependency.
Happens in basically all corporate code bases that grow over the years, with varying path lengths.
Throwing all potentially circular types into one big module isn't a great solution.
(In practice, we tend to rely on run-time imports to make it work. Not really great, but better than throwing several 10k or 100k lines of code into a single module).
Suppose you have some method of A which does something special if it gets an instance of B, and vice versa. Now you have a circular import problem; glhf
But why not collapse into a single module at this point if you can’t avoid dependency? What are the separate modules adding at this stage forward?
I ran into this when A and B had many derived classes. I wanted to put A and it’s derived classes in one module, and B and it’s derived classes in another. It was messy.
I wound up putting A and B in a single module and having a separate one for the derived classes. Not ideal.
A and B both need to know about the other's base definition, neither cares about the details about the other's derived classes. Splitting it into three modules shares as little surface area as possible.
from a import A
write import a
and in the code (which is presumably not at module level) check against isinstance(obj, a.A)
?Besides, if you are in a "well, fuck it, deadline is tomorrow" mode, you can always do something horrible like:
if 'classB' in type(obj).__name__: ...But the truth is sometimes it has happened to me and the only solution I found was creating an small module with maybe one or two functions which is not exactly ideal.
While that’s in rare circumstances the right thing to do, it's mostly an anti-pattern—you should be taking an object supporting a protocol, with the behavior difference depending on a field or method of (or actually implemented in a method of) that protocol. If you do that, you don't create a dependency on a concrete class that happens to require the special behavior.
If you keep doing that for a while, a circular dependency will happen.
This is the dumbest thing thing in Python. All other languages I know have solved it.
So put them in the same module? Circular modules don’t give you the benefits of modules, do they? Not an expert in modularity.
In case you didn't know, each source code file is a module in Python.
- protocols and abcs. Depend only on builtins, this sits at the top of the hierarchy and can be used anywhere
- top-level types/models, data structures. Only depends on external libraries
- config and logging. Depends on pydantic, which handles all init config validation, sets up log level. Many things import this
- specific class implementations, pure functions, transforms. imports from all the above
- dependency-injected stuff
- application logic
- routers and endpoints
I've had some close calls since I type check heavily, but protocols (since 3...6? 3.7 for sure) are a fantastic tool for both structuring and appeasing mypy.