Edit: I'm not trying to import from different folders, they're all in the same folder.
This has been asked countless times and there's always contradicting answers.
In my case I have this file structure:
import_test
├── custom_hashing
│   ├── __init__.py
│   ├── hashing_type_a.py
│   ├── hashing_type_b.py
│   └── utils.py
└── main.py
And these are the contents in order:
init.py
from hashing_type_a import hashing_function as hashing_function_a
from hashing_type_b import hashing_function as hashing_function_b
# Only "expose" this function in case we need tu update the real hashing functions in the future
def hash_this(string_to_hash, type='a') -> str:
    if type == 'a':
        return hashing_function_a(string_to_hash)
    if type == 'b':
        return hashing_function_b(string_to_hash)
hashing_type_a.py
from utils import split_and_switch
def hashing_function(string) -> str:
    prepared_string = split_and_switch(string)
    hashed_string = ''.join([chr(ord(character)+2)
                            for character in prepared_string])
    return hashed_string
def main():
    print("Testing hashing_type_a")
    hashed_string = hashing_function("abcdefghijk")
    print(f"{hashed_string=}")
if __name__ == '__main__':
    main()
hashing_type_b.py
from utils import split_and_switch
def hashing_function(string) -> str:
    prepared_string = split_and_switch(string)
    hashed_string = ''.join([chr(ord(character)-2)
                            for character in prepared_string])
    return hashed_string
def main():
    print("Testing hashing_type_b")
    hashed_string = hashing_function("abcdefghijk")
    print(f"{hashed_string=}")
if __name__ == '__main__':
    main()
utils.py
def split_and_switch(string) -> str:
    length = len(string)
    half = int(length/2)
    start_bit = string[:half]
    last_bit = string[half:]
    return last_bit+start_bit
main.py
from custom_hashing import hash_this
def main():
    hashed_string = hash_this("Lorem ipsum dolr sit amet", type="a")
    
    print(f"Hashed string type a is {hashed_string}")
    
    hashed_string = hash_this("Lorem ipsum dolr sit amet", type="b")
    
    print(f"Hashed string type b is {hashed_string}")
if __name__ == '__main__':
    main()
Now, from a terminal window on import_test I can run:
- python.exe .\custom_hashing\hashing_type_a.py
- python.exe .\custom_hashing\hashing_type_b.py
And I get the expected output, however if I try to run
- python.exe .\main.py
I get the error
Traceback (most recent call last):
  File ".\main.py", line 1, in <module>
    from custom_hashing import hash_this
  File "C:\Users\David\import_test\custom_hashing\__init__.py", line 1, in <module>
    from hashing_type_a import hashing_function as hashing_function_a       
ModuleNotFoundError: No module named 'hashing_type_a'
I've been told that, since I'm importing from the same folder, I need to change
from hashing_type_a import hashing_function as hashing_function_a
from hashing_type_b import hashing_function as hashing_function_b
to
from .hashing_type_a import hashing_function as hashing_function_a
from .hashing_type_b import hashing_function as hashing_function_b
#    ^ Added these periods
And sure enough, I get past the first error, but I run into a "new" one
Traceback (most recent call last):
  File ".\main.py", line 1, in <module>
    from custom_hashing import hash_this
  File "C:\Users\David\import_test\custom_hashing\__init__.py", line 1, in <module>
    from .hashing_type_a import hashing_function as hashing_function_a
  File "C:\Users\David\import_test\custom_hashing\hashing_type_a.py", line 1, in <module>
    from utils import split_and_switch
ModuleNotFoundError: No module named 'utils'
which seems like we can fix the same way
from .utils import split_and_switch
#    ^ Added the period on both files
Okay, fixed all the errors, now the program runs:
PS C:\Users\David\import_test> python.exe .\main.py
Hashed string type a is fqnt"ukv"cogvNqtgo"kruwo"
Hashed string type b is bmjpqgr_kcrJmpckgnqsk
But now, if we go back to testing individual modules:
PS C:\Users\David\import_test> python.exe .\custom_hashing\hashing_type_a.py
Traceback (most recent call last):
  File ".\custom_hashing\hashing_type_a.py", line 1, in <module>   
    from .utils import split_and_switch
ImportError: attempted relative import with no known parent package
I guess I could put something like
try:
    from .utils import split_and_switch
except ImportError:
    from utils import split_and_switch
but I feel like there has to be a better way that I just don't know about.
Here's the folder structure with files if anyone doesn't feel like copy&pasting everything
