Troubleshooting

“Command not found” errors

If you installed Modal but you’re seeing an error like modal: command not found when trying to run the CLI, this means that the installation location of Python package executables (“binaries”) are not present on your system path. This is a common problem; you need to reconfigure your system’s environment variables to fix it.

One workaround is to use python -m modal.cli instead of modal. However, this is just a patch. There’s no single solution for the problem because Python installs dependencies on different locations depending on your environment. See this popular StackOverflow question for pointers on how to resolve your system path issue.

Custom types defined in __main__

Modal currently uses cloudpickle to transfer objects returned or exceptions raised by functions that are executed in Modal. This gives a lot of flexibility and support for custom data types.

However, any types that are declared in your Python entrypoint file (The one you call on the command line) will currently be redeclared if they are returned from Modal functions, and will therefore have the same structure and type name but not maintain class object identity with your local types. This means that you can’t catch specific custom exception classes:

import modal
stub = modal.Stub()

class MyException(Exception):
    pass

@stub.function
def raise_custom():
    raise MyException()

if __name__ == "__main__":
    with stub.run():
        try:
            raise_custom()
        except MyException:  # this will not catch the remote exception
            pass
        except Exception:  # this will catch it instead, as it's still a subclass of Exception
            pass

Nor can you do object equality checks on dataclasses, or isinstance checks:

import modal
import dataclasses

@dataclasses.dataclass
class MyType:
    foo: int

stub = modal.Stub()

@stub.function
def return_custom():
    return MyType(foo=10)

if __name__ == "__main__":
    with stub.run():
        data = return_custom()
        assert data == MyType(foo=10)  # false!
        assert data.foo == 10  # true!, the type still has the same fields etc.
        assert isinstance(data, MyType)  # false!

If this is a problem for you, you can easily solve it by moving your custom type definitions to a separate Python file from the one you trigger to run your Modal code, and import that file instead.

# File: my_types.py
import dataclasses

@dataclasses.dataclass
class MyType:
    foo: int
# File: modal_script.py
import modal
from my_types import MyType

stub = modal.Stub()

@stub.function
def return_custom():
    return MyType(foo=10)

if __name__ == "__main__":
    with stub.run():
        data = return_custom()
        assert data == MyType(foo=10)  # true!
        assert isinstance(data, MyType)  # true!

Function side effects

The same container can be reused for multiple invocations of the same function within an app. This means that if your function has side effects like modifying files on disk, they may or may not be present for subsequent calls to that function. You should not rely on the side effects to be present, but you might have to be careful so they don’t cause problems.

For example, if you create a disk-backed database using sqlite3:

import modal
import sqlite3

stub = modal.Stub()

@stub.function
def db_op():
    db = sqlite3("db_file.sqlite3")
    db.execute("CREATE TABLE example (col_1 TEXT)")
    ...

This function can (but will not necessarily) fail on the second invocation with an

OperationalError: table foo already exists

To get around this, take care to either clean up your side effects (e.g. deleting the db file at the end your function call above) or make your functions take them into consideration (e.g. adding an if os.path.exists("db_file.sqlite") condition or randomize the filename above).