Check out our new GPU Glossary! Read now

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:

  1. 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.

  2. 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 or copy_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:

  1. Specify local files and directories through Mount objects.
  2. Include local Python modules with the function Mount.from_local_python_packages().
  3. Refactor your directory structure so that the relevant files and directories are automounted as part of packages.