os.path works in a funny way. It looks like os should be a package with a submodule path, but in reality os is a normal module that does magic with sys.modules to inject os.path. Here’s what happens:
-
When Python starts up, it loads a bunch of modules into
sys.modules. They aren’t bound to any names in your script, but you can access the already-created modules when you import them in some way.sys.modulesis a dict in which modules are cached. When you import a module, if it already has been imported somewhere, it gets the instance stored insys.modules.
-
osis among the modules that are loaded when Python starts up. It assigns itspathattribute to an os-specific path module. -
It injects
sys.modules['os.path'] = pathso that you’re able to do “import os.path” as though it was a submodule.
I tend to think of os.path as a module I want to use rather than a thing in the os module, so even though it’s not really a submodule of a package called os, I import it sort of like it is one and I always do import os.path. This is consistent with how os.path is documented.
Incidentally, this sort of structure leads to a lot of Python programmers’ early confusion about modules and packages and code organization, I think. This is really for two reasons
-
If you think of
osas a package and know that you can doimport osand have access to the submoduleos.path, you may be surprised later when you can’t doimport twistedand automatically accesstwisted.spreadwithout importing it. -
It is confusing that
os.nameis a normal thing, a string, andos.pathis a module. I always structure my packages with empty__init__.pyfiles so that at the same level I always have one type of thing: a module/package or other stuff. Several big Python projects take this approach, which tends to make more structured code.