Parameterized functions

A single Modal function can be parameterized by a set of arguments, so that each unique combination of arguments will behave like an individual Modal function with its own auto-scaling and lifecycle logic.

For example, you might want to have a separate pool of containers for each unique user that invokes your function. In this scenario, you would parameterize your function by a user ID.

To parameterize a Modal function, you need to use Modal’s class syntax and the @app.cls decorator. Specifically, you’ll need to:

  1. Convert your function to a method by making it a member of a class.
  2. Decorate the class with @app.cls(...) with the same arguments you previously had for @app.function(...) or your web endpoint decorator.
  3. If you previously used the @app.function() decorator on your function, replace it with @modal.method().
  4. Define dataclass-style, type-annotated instance attributes with modal.parameter() and optionally set default values:
import modal

app = modal.App()

@app.cls()
class MyClass:

    foo: str = modal.parameter()
    bar: int = modal.parameter(default=10)

    @modal.method()
    def baz(self, qux: str = "default") -> str:
        return f"This code is running in container pool ({self.foo}, {self.bar}), with input qux={qux}"

The parameters create a keyword-only constructor for your class, and the methods can be called as follows:

@app.local_entrypoint()
def main():
    m1 = MyClass(foo="hedgehog", bar=7)
    m1.baz.remote()

    m2 = MyClass(foo="fox")
    m2.baz.remote(qux="override")

Function calls for each unique combination of values for foo and bar will run in their own separate container pools. If you re-constructed a MyClass with the same arguments in a different context, the calls to baz would be routed to the same set of containers as before.

Some things to note:

  • The total size of the arguments is limited to 16 KiB.
  • Modal classes can still annotate typed of regular class attributes, which are independent of parameterization, by either omitting = modal.parameter() or using = modal.parameter(init=False) to satisfy type checkers.
  • Only int and str types for parameters are supported. Support for other types will be added in the future. If you need to pass a different type of argument, you can use the legacy method of specifying parameters, via __init__ and optional type hints:
@app.cls()
class MyClass:

    def __init__(self, foo, bar: float = 10.3) -> None:
        self.foo = foo
        self.bar = bar

    @modal.method()
    def baz(self, qux: str = "default") -> str:
        ...

This legacy method will not work with parameterized web endpoints.

Looking up a parameterized function

If you want to call your parameterized function from a Python script running anywhere, you can use Cls.lookup:

import modal

MyClass = modal.Cls.lookup("parameterized-function-app", "MyClass")  # returns a class-like object
m = MyClass(foo="snake", bar=12)
m.baz.remote()

Parameterized web endpoints

Modal web endpoints can also be parameterized by passing the arguments as URL query parameter values.

Given the same example as above but with a web endpoint:

@app.cls()
class MyClass():

    foo: str = modal.parameter()
    bar: int = modal.parameter(default=10)

    @modal.web_endpoint()
    def baz(self, qux: str = "default") -> str:
        ...

Here are some cURL commands that would work:

curl "https://parameterized-function-app.modal.run?foo=hedgehog&bar=7&qux=override"
curl "https://parameterized-function-app.modal.run?foo=hedgehog&qux=override"
curl "https://parameterized-function-app.modal.run?foo=hedgehog&bar=7"
curl "https://parameterized-function-app.modal.run?foo=hedgehog"

Using parameterized functions with lifecycle functions

Parameterized functions can be used with lifecycle functions. For example, here is how you might parameterize the @enter lifecycle function to load a specific model:

import modal

app = modal.App()

@app.cls()
class Model:

    name: str = modal.parameter()
    size: int = modal.parameter(default=100)

    @modal.enter()
    def load_model(self):
        print(f"Loading model {self.name} with size {self.size}")
        self.model = load_model_util(self.name, self.size)

    @modal.method()
    def generate(self, prompt: str) -> str:
        return self.model.generate(prompt)