Olve Heitmann
10/28/2025, 8:34 PMMarvin
10/28/2025, 8:34 PMNate
10/28/2025, 8:36 PMFoo(...).run_as_flow()
where
class Foo:
...
def run_as_flow(self, **kwargs):
from prefect import flow
return flow(self.run)(**kwargs)Nate
10/28/2025, 8:37 PMprefect into a class that only contextually has prefectMarvin
10/28/2025, 8:40 PM@task-decorated function still behaves like a normal Python function when called outside a flow, so it won’t interfere with non-Prefect usage as long as you don’t rely on Prefect-only context (e.g., get_run_logger(), retries) during those calls.
Good patterns to consider
- Keep business logic pure; wrap with thin Prefect adapters
- Keep your ETL class free of Prefect imports and orchestration concerns.
- Create small wrapper functions (or methods) that call into your class and decorate those with @task. This localizes Prefect to your orchestration layer and keeps your core code portable and testable.
# core.py (no Prefect imports)
class ETL:
def __init__(self, cfg):
self.cfg = cfg
def extract(self, src):
...
def transform(self, data):
...
def load(self, data, dest):
...
# orchestrate.py
from prefect import task, flow
from core import ETL
@task
def extract_t(cfg, src):
etl = ETL(cfg)
return etl.extract(src)
@task
def transform_t(data):
etl = ETL(cfg=None) # or pass cfg if needed
return etl.transform(data)
@task
def load_t(cfg, data, dest):
etl = ETL(cfg)
etl.load(data, dest)
@flow
def etl_flow(cfg, src, dest):
raw = extract_t.submit(cfg, src)
clean = transform_t.submit(raw)
load_t.submit(cfg, clean, dest)
- Decorating methods directly (works, with caveats)
- You can put @task on instance methods; Prefect will bind self correctly.
- Be mindful that passing a live instance to a remote worker means that instance will be serialized (pickled). If self holds non-serializable or heavy state (DB conns, clients), prefer passing config and constructing the object inside the task instead.
from prefect import task
class ETL:
def __init__(self, cfg):
self.cfg = cfg
@task
def extract(self, src):
return ... # uses self.cfg
# Outside a flow this is fine – runs like a normal function
etl = ETL(cfg=...)
rows = etl.extract(src) # synchronous call, no orchestration
- Conditional/no-op decoration if you want zero coupling
- If you want one code path that can run with or without Prefect:
import os
from prefect import task
ORCHESTRATE = os.getenv("PREFECT_ORCHESTRATE", "1") == "1"
def maybe(deco):
return deco if ORCHESTRATE else (lambda f: f)
@maybe(task(retries=2))
def transform(data):
...
- Use the .fn attribute and with_options for reuse and testing
- my_task.fn(...) calls the undecorated function (great for unit tests).
- my_task.with_options(name="X", retries=3) gives you a reconfigured task without redecorating.
- Logging pattern that works in and out of Prefect
- Use Prefect run logger when available; otherwise fall back to stdlib logging.
from prefect import get_run_logger
import logging
def loggable(msg, **kwargs):
try:
logger = get_run_logger()
<http://logger.info|logger.info>(msg, **kwargs)
except Exception:
logging.getLogger(__name__).info(msg)
- Result and parameter serialization
- By default, Prefect pickles task inputs/outputs. Passing large, stateful objects (e.g., class instances) increases payload size and can fail if they’re not pickleable. Prefer passing small, serializable config and constructing resources inside the task.
- If you need JSON-only I/O, set a serializer on the task and return JSON-serializable objects.
from prefect.serializers import JSONSerializer
from prefect import task
@task(result_serializer=JSONSerializer())
def summarize(data: dict) -> dict:
...
- Caching with bound methods
- If you decorate instance methods, self becomes part of the task inputs and can affect cache keys. If you want cache keys to ignore self, either switch to the wrapper pattern or provide a custom cache_key_fn that excludes it.
- Async supportMarvin
10/28/2025, 8:40 PM@task function outside a flow executes it immediately like a normal function and returns the raw result.
- Features that require orchestration context (e.g., retries, get_run_logger(), get_run_context(), caching, concurrency limits) are only active inside flows/tasks. If you try to use those context APIs outside a flow, you’ll get an error—so keep those in the wrappers, not your core class.
Practical guidance to choose a pattern
- Use wrapper tasks if:
- Your class holds heavy or non-serializable state, or opens connections.
- You want to keep Prefect imports out of your core module.
- Decorate methods if:
- The instance is light/serializable and you like the ergonomics.
- You’re primarily running locally and not shipping tasks to remote workers.
- Use conditional decoration if:
- You must ship a single artifact that can run both orchestrated and standalone with a toggle.
Docs to dig deeper
- Tasks concept: Tasks in Prefect 3
- Runtime context and logging: Runtime context and Add logging
- Testing tips: Test workflows
If you can share a quick sketch of your ETL class (how stateful it is, whether you’ll run workers remotely, and what types you pass around), I can recommend the most suitable pattern and tailor the code samples.Nate
10/28/2025, 8:41 PMOlve Heitmann
10/28/2025, 8:54 PM