Mounting local files and directories
When you run your code on Modal, it executes in a containerized environment separate from your local machine.
There are two ways to make local files available to your Modal app:
Mounting: Mounting is the process of making local files and directories accessible to your Modal function or application during runtime. Mounting is intended for files that change frequently during development. It allows you to modify your code locally and rerun it on Modal without needing to rebuild the container image each time. This can significantly speed up your development iteration cycle.
Adding files to the container image: For files that don’t change often, you can add them directly to your Modal container image during the build process with
copy_local_file
orcopy_local_dir
. This is suitable for dependencies and static assets that remain relatively constant throughout your development process.
This page is concerned with mounting. To use local files and packages within your Modal app via mounting, they either need to be automounted or explicitly mounted.
Automount
By default, with
automount=True
,
Modal mounts local Python packages that you have used (imported) in your code
but are not installed globally on your system (like those in site-packages
,
where globally installed packages reside).
For example, if you have a local module deps.py
that contains a function you
would like to import, dependency
. You import it as follows:
from deps import dependency
app = modal.App()
Modal will automatically mount deps.py
, and all of its dependencies not in
site-packages
.
All Python packages that are installed in site-packages
will be excluded from
automounting. This includes packages installed in virtual environments.
Non-Python files will be automounted only if they are located in the same
directory or subdirectory of a Python package. Note that the directory where
your Modal entrypoint is located is considered a package if it contains a
__init__.py
file and is being called as a package.
Absolute vs. Relative Imports
In Modal, you can import dependencies using either absolute or relative imports.
The method you choose affects how you should structure your modal run
or
modal deploy
command, as these commands mirror Python’s execution modes:
- Script mode: Python executes the file directly as a standalone script
- Module mode: Python executes the file as part of a package, enabling relative imports
Your choice of import style determines which mode to use when executing your code in Modal:
1. Absolute Imports: Use Script Mode
When using absolute imports, Python looks for modules in the Python path.
Example:
Given this project structure:
project/
├── src/
│ ├── main.py
│ └── helper.py
If main.py
uses absolute imports:
# src/main.py
from helper import * # Absolute import
Run it in Modal using script mode:
modal run src/main.py
Modal will automatically mount the necessary files, and Python will resolve the imports based on the Python path.
2. Relative Imports: Use Module Mode
Relative imports use dots (.
) to specify module locations relative to the
current file’s position in the package hierarchy.
Example:
Using the same project structure, if main.py
uses relative imports:
# src/main.py
from .helper import * # Relative import
Run it in Modal using module mode:
modal run src.main # Use dots instead of slashes
This tells Python to treat src
as a package, enabling it to resolve the
relative imports correctly.
Note: When using relative imports, ensure your project directory contains an
__init__.py
file to mark it as a Python package:
project/
├── src/
│ ├── __init__.py
│ ├── main.py
│ └── helper.py
Editable-mode exclusion
If local packages that you thought were installed in site-packages
are being
automounted, it’s possible that those packages were installed in editable-mode.
When you install a package in editable-mode (also known as “development mode”),
instead of copying the package files to the site-packages
directory, a link
(symbolic link or .egg-link file) is placed there. This link points to the
actual location of the package files, which are typically in your project
directory or a separate source directory. As a result, they may be automounted.
Automounts take precedence over PyPI packages
Automounts take precedence over PyPI packages, so in the case where you
pip_install
or otherwise include a package by building it into your image, the
automount will still trigger and shadow the site-packages
installed version.
An example of when this would happen is if you have binary parts of local
modules such that they need to be built as part of the image build.
Example #1: Simple directory structure
Let’s look at an example directory structure:
mountingexample1
├── __init__.py
├── data
│ └── my_data.jsonl
└── entrypoint.py
And let’s say your entrypoint.py
code looks like this:
import modal
app = modal.App()
@app.function()
def app_function():
print("app function")
When you run modal run entrypoint.py
from inside the mountingexample1
directory, you will see the following items mounted:
✓ Created objects.
├── 🔨 Created mount /Users/yirenlu/modal-scrap/mountingexample1/entrypoint.py
└── 🔨 Created app_function.
The data
directory is not auto-mounted, because mountingexample1
is not
being treated like a package in this case.
Now, let’s say you run cd .. && modal run mountingexample1.entrypoint
. You
should see the following items mounted:
✓ Created objects.
├── 🔨 Created mount PythonPackage:mountingexample1.entrypoint
├── 🔨 Created mount PythonPackage:mountingexample1
└── 🔨 Created app_function.
The entire mountingexample1
package is mounted, including the data
subdirectory.
This is because the mountingexample1
directory is being treated as a package.
Example #2: Global scope imports
Oftentimes when you are building on Modal, you will be migrating an existing codebase that is spread across multiple files and packages. Let’s say your directory looks like this:
mountingexample2
├── __init__.py
├── data
│ └── my_data.jsonl
├── entrypoint.py
└── package
├── __init__.py
├── package_data
│ └── library_data.jsonl
└── package_function.py
And your entrypoint code looks like this:
import modal
from package.package_function import package_dependency
app = modal.App()
@app.function()
def app_function():
package_dependency()
When you run the entrypoint code with modal run mountingexample2.entrypoint
,
you will see the following items mounted:
✓ Created objects.
├── 🔨 Created mount PythonPackage:mountingexample2.entrypoint
├── 🔨 Created mount PythonPackage:mountingexample2
└── 🔨 Created app_function.
The entire contents of mountingexample2
is mounted, including the /data
directory and the package
package inside of it.
Finally, let’s check what happens when you remove the package
import from your
entrypoint code and run it with modal run entrypoint.py
.
✓ Created objects.
├── 🔨 Created mount /Users/yirenlu/modal-scrap/mountingexample2/entrypoint.py
└── 🔨 Created app_function.
Only the entrypoint file is mounted, and nothing else.
Mounting files manually
If something that you want to have mounted is not included in an automount, you have a few options:
- Specify local files and directories through
Mount
objects. - Include local Python modules with the function
Mount.from_local_python_packages()
. - Refactor your directory structure so that the relevant files and directories are automounted as part of packages.