17. Organizing code with modules and packages
Large codebases can become difficult to maintain and navigate if all the code is in a single file. To address this issue, we typically split the code into multiple files. In Mojo, we can use modules and packages to organize our code.
17.1. Modules
In Mojo, a module is a file that contains code. You can define functions, classes, and variables in a module. To use the code from a module, you can import it using the import statement.
def print_module_message():
print("Hello from my_module!")
Usage:
import my_module
my_module.print_module_message()
In the above example, we have a module named my_module that contains a function print_module_message. We import this module in our main.mojo file and call the function to print a message.
17.2. Packages
A package is a collection of modules (and therefore files) organized in a directory. You can import modules from a package using the import statement or the from … import … statement. You can also nest packages within packages to create a hierarchical structure.
What separates a normal directory from a package is the presence of an init.mojo file. This file can be empty, but it must be present for the directory to be recognized as a package.
def print_packaged_module_message():
print("Hello from mypackagedmodule!")
# This is empty on purpose. It allows us to treat the "mypackage" directory as a package, enabling us to import modules from it.
Usage:
import mypackage.mypackagedmodule as mpm
mpm.print_packaged_module_message()
In the above code, we have a directory named mypackage that contains a file named mypackagedmodule, along with init.mojo. We import this module in our main.mojo file and call the function to print a message.
As you can see, the module and package system in Mojo is rather simple and straightforward. It allows you to organize your code in a way that is easy to maintain and navigate, especially as your codebase grows larger.
17.2.1. init.mojo file
The init.mojo file need not be empty. You can use it to import modules or functions that you want to be available when the package is imported. This can be useful for creating a cleaner and more convenient API for your package.
def print_packaged_module_message2():
print("Hello from mypackagedmodule2!")
from .mypackagedmodule2 import print_packaged_module_message2
Usage:
import mypackage2
mypackage2.print_packaged_module_message2()
In the above code, you can see that we are not mentioning the mypackagedmodule2 module in our main.mojo file. Instead, we are importing it in the init.mojo file of the package. This allows us to call the function from mypackagedmodule2 directly after importing the package, without needing to specify the module name.
This allows us to keep the internal structure of the package hidden from the users of the package, and provides a cleaner and more convenient API. It also allows us to provide some level of backwards compatibility if we decide to change the internal structure of the package in the future.
17.3. Compiled packages
Sometimes you do not want to distribute the source code of your package, but still want others to be able to use it. In such cases, you can distribute a compiled version of your package. This is similar to how you might distribute a compiled library in other programming languages. To create a compiled package, you can use the mojo precompile command to compile your package into a binary format. You can then distribute this compiled package to others, who can import and use it just like they would with a regular package.
Usage:
pixi run mojo precompile mypackage