Table of Contents

Packaging

FIXME :!: The Python community has some PEPs and a lot of debate over what the 'correct' way to package Python programs is. As of November 2023 I'm not totally sure what the way forward is meant to be for PyPI projects, or if the below information is still true about Python's module handling. Consume appropriate amounts of salt where sus. :!:

In Python, individual source files are referred to as modules. They represent a single collection of code, and can be imported. Packages are directories containing an __init__.py file and other modules. They are comparable to namespaces, and can also be imported. A package with an empty __init__.py file is completely fine; it just won't import any modules when imported.

Here's an example of a package containing modules:

my_package/
    __init__.py
    ui.py
    attribute.py
    model.py
    router.py

There are multiple ways to interact with a package, which are covered below. In every case, it's assumed that the Python interpreter is in the directory above my_package/, or my_package/ is somewhere in Python's path variable. See Python's documentation on sys.path for information on that.

Simple Importing

# Import just the package
import my_package

In this case, importing the package will process everything that's in __init__.py, including any pre-defined imports. It's considered good practice to define which modules get imported by default when importing a package, to ensure only the necessary modules are loaded. Your modules can individually import any part of the package they need to get the job done. To do this, your __init__.py should look somewhat like this:

__init__.py
# Let's say you only need the model and router to get started
from . import (model, router)

The . here means "the current package". If you have sub-packages, you can address them with a dot preceding its name, like .my_subpackage.

Single Module Importing

Sometimes, you only need a single module from a package. That's easy:

# Import only the UI
from my_package import ui

In this example, only the ui module is imported. It gets injected into the current environment as ui, and you can access its members as well, using something like ui.create_box().

Single Member Importing

Lastly, sometimes you need only specific parts of a module. That's easy, too!

# Import only specific things from a given module
from my_package.router import do_route

In the above example, do_route will become available in the current environment, imported from the router module inside the my_package package.

If you find yourself with strange NameErrors, AttributeErrors, or ValueErrors, it may point to a name collision. You can rename your imports with the as keyword:

# Import with a different name
from my_package.router import do_route as make_route

This is often caused by importing two modules with the same member name. If you import two things with the same member name, the latter name will take precedence:

# Same name, different structures
from os import path
print(type(path))
# <class 'module'>
from sys import path
print(type(path))
# <class 'list'>

For this reason, it's generally better to leave imports alone and go one level deeper only as needed. So if you wanted to use path from both os and sys, just import them on their own, so you're using os.path and sys.path. It's more verbose, but also more precise and less likely to break.

References

~~DISCUSSION~~