Modal 1.0 migration guide

We are planning to release version 1.0 of the modal Python SDK in Q1 of 2025. This release will signify an increased commitment to API stability and will imply some changes to our development workflow.

Preceding the 1.0 release, we are making a number of breaking changes based on feedback that we have received from early users. These changes are intended to address pain points and reduce confusion about some aspects of the Modal API. While they will require some changes to existing user code, we believe that they’ll make it easier to use Modal going forward.

Our plan is to gradually roll out changes — with deprecation warnings — across the final sequence of 0.X releases. Once we release 1.0, code that does not issue deprecation warnings can be considered stable API. We will eventually expire the deprecations introduced prior to 1.0 and remove support for the old APIs.

This page outlines the major changes that we’re making as part of the v1.0 releases.

Deprecating Image.copy_* methods

Introduced in: v0.72.11

We recently introduced new Image methods — Image.add_local_dir and Image.add_local_file — to replace the existing Image.copy_local_dir and Image.copy_local_file.

The new methods subsume the functionality of the old ones, but their default behavior is different and more performant. By default, files will be mounted to the container at runtime rather than copied into a new Image layer. This can speed up development substantially when iterating on the contents of the files.

Building a new Image layer should be necessary only when subsequent build steps will use the added files. In that case, you can pass copy=True in Image.add_local_file or Image.add_local_dir.

The Image.add_local_dir method also has an ignore= parameter, which you can use to pass file-matching patterns (using dockerignore rules) or predicate functions to exclude files.

Deprecating Mount as part of the public API

Introduced in: v0.72.4

Currently, local files can be mounted to the container filesystem either by including them in the Image definition or by passing a modal.Mount object directly to the App.function or App.cls decorators. As part of the 1.0 release, we are simplifying the container filesystem configuration to be defined only by the Image used for each Function. This implies deprecation of the following:

  • The mount= parameter of App.function and App.cls
  • The context_mount= parameter of several modal.Image methods
  • The Image.copy_mount method
  • The Mount object

Code that uses the mount= parameter of App.function and App.cls should be migrated to pass those files / directories to the Image used by that Function or Cls, i.e. using the Image.add_local_file, Image.add_local_dir, or Image.add_local_python_source methods:

# Mounting local files

# Old way (deprecated)
mount = modal.Mount.from_local_dir("data").add_local_file("config.yaml")
@app.function(image=image, mount=mount)
def f():
    ...

# New way
image = image.add_local_dir("data").add_local_file("config.yaml")
@app.function(image=image)
def f():
    ...

## Mounting local Python source code

# Old way (deprecated)
mount = modal.Mount.from_local_python_packages("my-lib"))
@app.function(image=image, mount=mount)
def f()
    ...

# New way
image = image.add_local_python_source("my-lib")
@app.function(image=image)
def f(...):
    ...

## Using Image.copy_mount

# Old way (deprecated)
mount = modal.Mount.from_local_dir("data").add_local_file("config.yaml")
image.copy_mount(mount)

# New way
image.add_local_dir("data").add_local_file("config.yaml")

Code that uses the context_mount= parameter of Image.from_dockerfile and Image.dockerfile_commands methods can delete that parameter; we now automatically infer the files that need to be included in the context.

Deprecating the @modal.build decorator

Introduced in: v0.72.17

As part of consolidating the filesystem configuration API, we are also deprecating the modal.build decorator.

For use cases where modal.build would previously have been the suggested approach (e.g., downloading model weights or other large assets to the container filesystem), we now recommend using a modal.Volume instead. The main advantage of storing weights in a Volume instead of an Image is that the weights do not need to be re-downloaded every time you change something else about the Image definition.

Many frameworks, such as Hugging Face, automatically cache downloaded model weights. When using these frameworks, you just need to ensure that you mount a modal.Volume to the expected location of the framework’s cache:

cache_vol = modal.Volume.from_name("hf-hub-cache")
@app.cls(
    image=image.env({"HF_HUB_CACHE": "/cache"}),
    volumes={"/cache": cache_vol},
    ...
)
class Model:
    @modal.enter()
    def load_model(self):
        self.model = ModelClass.from_pretrained(...)

For frameworks that don’t support automatic caching, you could write a separate function to download the weights and write them directly to the Volume, then modal run against this function before you deploy.

In some cases (e.g., if the step runs very quickly), you may wish for the logic currently decorated with @modal.build to continue modifying the Image filesystem. In that case, you can extract the method as a standalone function and pass it to Image.run_function:

def download_weights():
    ...

image = image.run_function(download_weights)

Deprecating the .lookup method on Modal objects

Introduced in: Pending

Most Modal objects can be instantiated through two distinct methods: .from_name and .lookup. The redundancy between these methods is a persistent source of confusion.

The .from_name method is lazy: it operates entirely locally and instantiates only a shell for the object. The local object won’t be associated with its identity on the Modal server until you interact with it. In contrast, the .lookup method is eager; calling it triggers an RPC, and it returns a fully-hydrated object.

Because Modal objects can now be hydrated on-demand, when they are first used,there is rarely a need to eagerly hydrate. Therefore, we’re deprecating .lookup so that there’s only one obvious way to instantiate objects.

In most cases, the migration is a simple find-and-replace of .lookup.from_name.

One exception is when your code needs to access object metadata, such as its ID, or a web endpoint’s URL. In that case, you can explicitly force hydration of the object by calling its .hydrate() method.