I had this problem for the first couple years, but not anymore. What I did:
- I split the core app up into different views and services based on the functionality. For FWD:Everyone this is basically: account_management, admin_apis, content_discovery, organizations, upload_thread, read_thread, comments, and user.
I have a folder for all my views and a folder for all my services, and for each view there is one (or occasionally two) service files.
So e.g. in the views folder there is a file called account_management_views, and then in the services folder there is a file called account_management_service.
For utilities that get re-used across the entire app I have two additional things in the services folder, one is called util.py and the other is io_util.py.
Util is for things like sanitizing XSS that don't require any database access, and io_util is for things like get_user_from_username, get_user_from_email_address, etc.
Then you can import any models into io_util.py, but never into util.py. And you can import anything from util.py into io_util.py, but never the other way around. Then you can import anything from either util or io_util into any service method and shouldn't have any issues. (Although I always just import util or io_util, and then call the methods like util.sanitize_xss.)
Once I structured the services and utilities that way I never had issues again with circular imports or figuring out where to put stuff. Importing with Python is kind of difficult and janky in general, but so far this pattern seems to be working pretty well for both our startup and also for the consulting projects I've done.
Basically for logic that's mostly specific to a single view, put it in the services file for that view. Public functions in that folder can still be re-used in other services, but if they're fairly generic and they're re-used in more than one or two places then consider moving them into util or io_util so as to avoid circular import issues.