Answers you can trust, from Codeables
Every page on Codeables is structured and verified — built so people and the AI agents they rely on can trust it. Explore more from the source behind this answer.
Explore CodeablesIs there a way to run my Python code in the cloud with reproducible environments and logs/metrics without writing a bunch of YAML?
Quick Answer: Yes. With Modal, you can run Python code in the cloud with fully reproducible environments, structured logs, and metrics—defined entirely in Python instead of YAML. You package your environment as a Modal Image, decorate functions, and get autoscaled, observable workloads in minutes.
Why This Matters
If you care about latency, correctness, and debugging in production, “just throw it on a VM” isn’t enough. You need:
- Reproducible environments so your code behaves the same on your laptop and in the cloud.
- Centralized logs and metrics so you can understand failures and performance.
- Autoscaling infrastructure that doesn’t require maintaining Terraform, Kubernetes, or a zoo of YAML files.
Modal’s bet is that all of this should live in Python, right next to your application logic. You describe the environment, hardware, scaling, and endpoints as code, then let Modal handle provisioning, orchestration, and observability.
Key Benefits:
- Reproducible environments: Pin Python packages and system dependencies in a Modal Image for deterministic runs across dev and prod.
- Logs and metrics by default: Every function call, container, and app has integrated logs and execution metadata in the Modal UI and API.
- No YAML, just Python: Define infrastructure using decorators and a simple Python API instead of writing and maintaining config files.
Core Concepts & Key Points
| Concept | Definition | Why it's important |
|---|---|---|
| Modal Image | A Python-defined container image that packages your code, dependencies, and system libraries. | Makes environments reproducible and shareable; you know exactly what runs in production. |
| Modal Function / App | A Python function or class wrapped with @app.function, @app.cls, etc., that runs in a container on demand. | Turns plain Python into scalable cloud workloads (endpoints, batch jobs, workers) with no extra services to manage. |
| Unified observability | Integrated logging and execution metadata for every function call, container, and app, exposed in the Modal dashboard and API. | Gives you visibility into performance, failures, and resource usage without bolting on a logging stack. |
How It Works (Step-by-Step)
Instead of writing YAML or spinning up Kubernetes, you describe your environment and workloads in a single Python file. Then you run it locally, deploy it, and let Modal autoscale it across CPUs/GPUs.
Here’s the basic workflow.
-
Define your environment as a Modal Image
You start by declaring a reproducible environment in Python:
import modal image = ( modal.Image.debian_slim() .pip_install( "numpy==1.26.4", "pandas==2.2.2", "requests==2.32.3", ) ) app = modal.App("reproducible-python-example")This replaces Dockerfiles and random “pip install” instructions. You pin versions tightly so every run—local test or production—uses the same stack.
-
Turn a function into a cloud workload
Take plain Python and annotate it with Modal decorators. For example, a batch job that processes data and logs metrics:
import time import logging logger = logging.getLogger(__name__) @app.function(image=image, timeout=600) def process_record(record_id: int) -> dict: start = time.time() logger.info("Starting processing", extra={"record_id": record_id}) # Your actual business logic result = {"record_id": record_id, "value": record_id ** 2} duration = time.time() - start logger.info( "Finished processing", extra={"record_id": record_id, "duration_sec": duration}, ) return resultYou can run this locally for tight feedback:
modal run script.py::process_record --record-id 42Modal spins up a container with the
image, runs the function, and streams logs back to your terminal and the UI. -
Scale it out with autoscaling and observability
When you want to fan out across many inputs, you use
.map()or.spawn():@app.local_entrypoint() def main(): record_ids = range(1_000) # Executes in parallel across autoscaled containers results = list(process_record.map(record_ids)) print("Processed", len(results), "records")To deploy for recurring use:
modal deploy script.pyAfter deployment:
- Each call is tracked as a job with logs in the Modal dashboard.
- You see execution time, retries, failures, and resource usage per call.
- Autoscaling kicks in to run jobs in parallel and scale back to zero when idle.
For APIs, you’d expose a FastAPI app directly:
from fastapi import FastAPI
web_app = FastAPI()
@web_app.get("/square/{x}")
def square(x: int):
return {"x": x, "x2": x * x}
@app.fastapi_endpoint("/api")
def fastapi_app():
return web_app
Deploy with modal deploy, and you now have a web endpoint with the same reproducible environment and logging.
Common Mistakes to Avoid
-
Leaving dependencies unpinned:
If you just call.pip_install("pandas")without a version, your environment can change under you.
How to avoid it: Always pin versions ("pandas==2.2.2"), especially for anything that touches data, models, or serialization formats. -
Treating logging as an afterthought:
Printing random strings makes it hard to debug production incidents.
How to avoid it: Useloggingwith structured fields; treat each function call as a unit of work whose logs you’ll read in the Modal UI. Log inputs (carefully), key decisions, and timings.
Real-World Example
Let’s say you’re running nightly evaluations on an LLM with thousands of prompts. You want:
- A reproducible environment with a specific
transformersand CUDA stack. - Parallel execution across GPUs when load spikes.
- Logs per prompt so you can understand failures and latency.
Here’s a sketch using Modal:
import modal
import logging
from transformers import AutoModelForCausalLM, AutoTokenizer
logger = logging.getLogger(__name__)
image = (
modal.Image.debian_slim()
.pip_install(
"torch==2.2.2",
"transformers==4.40.0",
)
)
app = modal.App("llm-evals")
@app.cls(image=image, gpu="A10G", timeout=600)
class ModelServer:
@modal.enter()
def load(self):
logger.info("Loading model weights...")
self.tokenizer = AutoTokenizer.from_pretrained("gpt2")
self.model = AutoModelForCausalLM.from_pretrained("gpt2")
logger.info("Model loaded")
@modal.method()
def eval_prompt(self, prompt: str) -> dict:
logger.info("Evaluating prompt", extra={"prompt_preview": prompt[:80]})
tokens = self.tokenizer(prompt, return_tensors="pt")
out = self.model.generate(**tokens, max_new_tokens=64)
text = self.tokenizer.decode(out[0], skip_special_tokens=True)
logger.info("Eval complete", extra={"response_preview": text[:80]})
return {"prompt": prompt, "response": text}
You can fan this out:
@app.local_entrypoint()
def run_batch():
prompts = ["Hello world", "Write a haiku about GPUs", ...]
server = ModelServer()
results = list(server.eval_prompt.map(prompts))
print("Got", len(results), "responses")
Operationally:
- The
ModelServerclass loads weights once per container (@modal.enter), which is critical for performance. - Logs for each
eval_promptcall are visible in the Modal apps page, with timestamps and extra fields. - The environment (PyTorch,
transformers) is locked by the Image definition—no “works on my machine” surprises.
Pro Tip: Treat your Modal Image definition as the source of truth for your environment. Whenever you change a dependency version, commit that change in Git and re-deploy. This gives you a clean audit trail for why behavior changed across runs.
Summary
If your question is “is there a way to run my Python code in the cloud with reproducible environments and logs/metrics without writing a bunch of YAML?”, the practical answer is: define everything in Python with Modal.
You:
- Use Modal Images to describe deterministic environments.
- Wrap functions and classes with decorators to turn them into autoscaled workloads and endpoints.
- Get unified logs, execution metadata, and visibility into every call in the Modal dashboard—no separate logging stack, no Kubernetes manifests, no config sprawl.
You keep iteration speed high because the same code you test with modal run is what you deploy with modal deploy.