Mojo allows you to access the entire Python ecosystem, but environments can vary depending on how Python was installed. It's worth taking some time to understand exactly how modules and packages work in Python, as there are a few complications to be aware of. If you've had trouble calling into Python code before, this will help you get started.
Let's start with Python, if we have two files in the same directory:
We can also add to sys.path with an env var to give us access to modules elsewhere:
Bash
export PYTHONPATH="/home/j"
If you check the first entry on sys.path it'll now be there:
Python
sys.path[0]
Output
/home/j
You can also edit sys.path directly:
Python
sys.path.append("/tmp")
To convince ourselves this works create a file at /tmp/mod2.py:
Python/tmp/mod2.py
a = 42
You can now access mod2 as a module:
Python
import mod2
mod2
Output
module 'mod2' from '/tmp/mod2.py'
Anything inside that module is accessible:
Python
mod2.a
Output
42
It's considered bad practice to directly modify sys.path, you should use the module system as intended, but it's a good way to see how things work under the hood.
The most important part for our purposes is sys.prefix which is the base where the lib and bin folders are located for the python binary we're using:
Bash
which python
Output
/usr/bin/python
Now notice the prefix is two folders above the python executable, this is the root where sys.path will start searching from:
Python
sys.prefix
Output
/usr
Depending on the system you're running on this will look very different, but you'll always see lib and bin at sys.prefix, and this is where the first few paths like /usr/lib/python3.11 and /usr/lib/python3.11/site-pacakgesare added. There are ways to hack this as per the docs above, but that's the general rule.
In Mojo we can use any Python module in sys.path in the same way. To work out exactly which environment is being used, let's start by checking the prefix:
Mojo
from python import Python
def main():
let sys = Python.import_module("sys")
print(sys.prefix)
Output
/usr
Calling Python could result in an error, so we either need to use def main(): as the entrypoint, fn main() raises: to mark it as a strict Mojo function that can throw an error, or handle the errors in a try except block:
Mojo
fn main():
try:
let x = Python.import_module("fake")
except e:
# mojo 0.3.1
print("Failed to import:", e.value)
# next release
print("Failed to import:", e)
You can also print everything that's on sys.path from Mojo:
Now any modules in the current directly are available, let's access the mod.py we created earlier from Mojo:
Mojo
let mod = Python.import_module("mod")
mod.bar
Output
[5, 10, 15, 20]
What is a Package?
Mojo behaves the same as Python, a package can be a subfolder containing an __init__.🔥 or __init__.mojo with initialization logic. It also allows you to access other modules in the same directory:
A question that often comes up in both Python and Mojo is relative imports, this is possible with the above structure. car, plane and common can access each other because they are sub-packages of the package vehicles:
Mojo./vehicles/common/liquids.🔥
var fuel = 0
fn refuel():
print("refuelling!")
Mojo./vehicles/car/__init__.🔥
from ..common.liquids import fuel, refuel
fuel and refuel are now accessible from ./vehicles/car/drive.🔥:
Mojo./vehicles/car/drive.🔥
fn move_forward():
if fuel == 0:
refuel()
print("moving forward!")
Mojo./main.🔥
from vehicles.car.drive import move_forward
fn main():
move_forward()
Outputpython main.🔥
refuelling!
moving forward!
This is different to other languages where file structure doesn't matter as much, keep this in mind as you build out your code base.
Note that as of Python 3.3 the __init__.py file is not required if you don't have any initialization logic due to PEP 420, however it's still currently a requirement in Mojo to mark a folder as a package.
Creating a virtual environment with venv
venv comes with Python and can be used to generate a virtual environment from the python binary we have on path, for more details you can read a primer here.
First check which python binary you're using to make sure it's the one you want:
The two site-packages folders that were controlled by my system package manager have been removed, and the venvs /home/j/venv/lib/python3.11/site-packages is now in our sys.path. Let's have a look inside:
rich and it's dependencies were installed into the venv so we can now access the module.
Let's check if sys.prefix has changed:
Bash
python -c "import sys; print(sys.prefix)"
Output
/home/j/venv
It has, but how are we still getting all the standard library modules in our sys.path? The base_prefix is what adds the python modules from the base installation:
Bash
python -c "import sys; print(sys.base_prefix)"
Output
/usr
We can access modules from Mojo in the same way so long as the venv is activated, and print where rich is located from Mojo:
Mojo
let rich = Python.import_module("rich")
rich
Output
module 'rich' from '/home/j/venv/lib/python3.11/site-packages/rich/__init__.py'
This works but requires a Python environment with a dynamic libpython in to work correctly, if you don't have a compatible installation you can use conda.
Installing libpython with Conda
To use a specific version of Python with you can install it with conda and link to the libpython that's installed. It not only isolates python dependencies, but also C/C++ system libraries like openssl and cuda that are notorious for causing cross-platform and linux distribution problems.
Now we have an isolated environment with its own system libraries, python packages, and importantly a fresh libpython which Mojo uses directly for python interop.
You can activate it when you want access to these modules with:
Bash
conda activtate py310
Now when you run mojo again you'll see that it's getting modules from the directories in our isolated python3.10 environment:
Great, now it's not a mystery where our Python modules are coming from!
Let's install a python package into our conda instance now:
Bash
conda activate py310
conda install numpy -y
Output
## Package Plan ##
environment location: /home/j/miniconda3
added / updated specs:
- numpy
The following NEW packages will be INSTALLED:
blas pkgs/main/linux-64::blas-1.0-mkl
intel-openmp pkgs/main/linux-64::intel-openmp-2023.1.0-hdb19cb5_46305
mkl pkgs/main/linux-64::mkl-2023.1.0-h213fc3f_46343
mkl-service pkgs/main/linux-64::mkl-service-2.4.0-py311h5eee18b_1
mkl_fft pkgs/main/linux-64::mkl_fft-1.3.8-py311h5eee18b_0
mkl_random pkgs/main/linux-64::mkl_random-1.2.4-py311hdb19cb5_0
numpy pkgs/main/linux-64::numpy-1.26.0-py311h08b1b3b_0
numpy-base pkgs/main/linux-64::numpy-base-1.26.0-py311hf175353_0
tbb pkgs/main/linux-64::tbb-2021.8.0-hdb19cb5_0
The following packages will be UPDATED:
ca-certificates 2023.05.30-h06a4308_0 --> 2023.08.22-h06a4308_0
certifi 2023.5.7-py311h06a4308_0 --> 2023.7.22-py311h06a4308_0
conda 23.5.2-py311h06a4308_0 --> 23.7.4-py311h06a4308_0
openssl 3.0.9-h7f8727e_0 --> 3.0.11-h7f8727e_2
numpy is complicated underneath and requires many system libraries for fortran routines, linear algebra, and hardware acceleration. All the system libraries above are all installed to the isolated environment. System libs are checked for compatibility against the version of python and all the other packages you're installing. Having an older or newer distribution won't break it, and we don't have to mess around with our system package manager to install system dependencies. Installing numpy on Apple Silicon uses entirely different libraries to take advantage of different hardware:
Output
## Package Plan ##
environment location: /opt/homebrew/Caskroom/miniconda/base/envs/py310
added / updated specs:
- numpy
The following NEW packages will be INSTALLED:
blas conda-forge/osx-arm64::blas-2.118-openblas
blas-devel conda-forge/osx-arm64::blas-devel-3.9.0-18_osxarm64_openblas
libblas conda-forge/osx-arm64::libblas-3.9.0-18_osxarm64_openblas
libcblas conda-forge/osx-arm64::libcblas-3.9.0-18_osxarm64_openblas
libgfortran conda-forge/osx-arm64::libgfortran-5.0.0-13_2_0_hd922786_1
libgfortran5 conda-forge/osx-arm64::libgfortran5-13.2.0-hf226fd6_1
liblapack conda-forge/osx-arm64::liblapack-3.9.0-18_osxarm64_openblas
liblapacke conda-forge/osx-arm64::liblapacke-3.9.0-18_osxarm64_openblas
libopenblas conda-forge/osx-arm64::libopenblas-0.3.24-openmp_hd76b1f2_0
llvm-openmp conda-forge/osx-arm64::llvm-openmp-16.0.6-h1c12783_0
numpy anaconda/osx-arm64::numpy-1.22.3-py310hdb36b11_0
numpy-base anaconda/osx-arm64::numpy-base-1.22.3-py310h5e3e9f0_0
openblas conda-forge/osx-arm64::openblas-0.3.24-openmp_hce3e5ba_0
Now we can access numpy from inside Python and print the path to the module:
Mojo
let numpy = Python.import_module("numpy")
numpy
Output
module 'numpy' from '/home/j/miniconda3/base/envs/py310/lib/python3.10/site-packages/numpy/__init__.py'
If we can't find something in conda simply install it with `pip`:
This works as a lockfile for the specific arch and os which you're running on, we're including system libraries that are specific to Linux. If you're building something cross-platform, let conda resolve all the dependencies, and just specify what you need:
Just be careful, as pip dependencies have to be added back in manually when using this technique. It's best to edit it manually, remove the prefix, and set minimum versions as required so that it ends up looking like this:
Make sure to always put -n <env-name>, so it installs to your <conda-install>/base/envs/ folder and ignores any hard coded prefix.
Conclusion
The aim of this post was to make it clear about how you can access python modules from Mojo, so that you can troubleshoot anything that goes wrong yourself. And to demonstrate the two most common methods of creating virtual environments. There are other solutions like poetry and pdm that have nice features, but conda is the most foolproof way as it installs any version of python with all the required system libraries to an isolated environment. This mitigates the huge amount of system configuration problems and library conflicts that can arise when distributing Python applications.
In my next post we'll be creating a GUI app using Python libraries that call performant Mojo functions, stay tuned!
Jack started his career optimizing autonomous truck software for leading mining companies, including BHP and Caterpillar. Most recently he was designing computer vision software, putting AI inference pipelines into production for IDVerse. He is enormously passionate about the developer community, having been a Rust, Go, Python and C++ developer for over a decade. Jack enjoys making complicated topics simple and fun to learn, and he’s dedicated to teaching the world about Mojo 🔥.