Footguns

Things that bit other authors first, organized by where the trap is.

Configuration

Don't add numpy to requirements.txt

pipelogic already depends on numpy at the version the platform expects. Listing it again pulls a different version into the runtime image and produces silent crashes.

# WRONG — never list numpy
numpy==2.0.0

# Right — pipelogic ships numpy

Pin every dependency exactly

==1.2.3, never >=, ~=, or unpinned. Loose pins make builds non-reproducible and quietly pull in new CVEs on rebuild.

build_system matters

The default build_system: 2 is the lean Python runtime. ML components need a wider variant:

build_systemIncludes
2Python + pipelogic + opencv-python-headless
2-opencv4.11+ system OpenCV
2-cuda12.8-onnxrtgpu1.22+ ONNX Runtime GPU
2-cuda12.8-torch2.8-onnxrtgpu1.22+ PyTorch + ORT
2-cuda12.8-torch2.8-ultralytics+ Ultralytics
2-cuda12.8-torch2.8-onnxrtgpu1.22-roboflow+ Roboflow inference

Picking the wrong build_system means importing torch fails at runtime because torch isn't in the image. See the Models page for the full matrix.

Types

Numpy views from Bytes / List are zero-copy and short-lived

import numpy as np

arr = np.asarray(pipe_bytes)                               # zero-copy view, valid for current tick
arr = pipe_bytes.unsafe_numpy(dtype=np.uint8, shape=(h, w, 3))  # zero-copy with shape/dtype
arr = np.array(pipe_bytes, copy=True)                      # always-copy
arr = pipe_bytes.safe_numpy()                              # always-copy

Writes through the zero-copy view flow back into the underlying buffer (intentional for in-place preprocessing). The view is invalidated the next time the runtime advances the stream — don't keep it past the current tick. Use np.array(..., copy=True) or .safe_numpy() for values that need to outlive the call.

Default int is Int64, default float is Double

from pipelogic.types import List

L = List([1, 2, 3])              # element type: Int64
L = List([1.0, 2.0])             # element type: Double
L = List([1, 2, 3], '[Int32]')   # explicit Int32

Don't construct dicts for typed values

The high-level wrappers exist for a reason — they handle the named-type registration and field layout for you:

# WRONG — manual dict construction breaks when type fields drift
return {"width": w, "height": h, "data": arr.tobytes(), "format": ...}

# Right — let the wrapper do it
return Image(arr, color_space=ColorSpace.BGR)

The same applies to BoundingBox, Tensor, AudioFrame, Mask, Landmark, etc. Prefer the wrappers in pipelogic.cv and pipelogic.types — they handle field layout and named-type registration for you. Drop down to raw dicts only when you have a reason the wrappers don't cover.

Workers

Virtual input is opt-in by parameter name

To receive virtual-input messages, name the parameter virtual_input on your worker function. The runtime detects the name and switches the worker into virtual-input mode:

def from_vin(virtual_input):
    return virtual_input[0]

run(from_vin)

Returning a tuple from a single-output worker wraps it as one output

If component.yml declares one output_type and your function returns (a, b), the runtime serializes the tuple as your one output — it does not auto-split. To return multiple outputs, declare them in component.yml:

worker:
  output_types:
    - BoundingBox
    - Image

then return (boxes, image) in declared order.

Stateful workers must return a dict

def stateful(x, state):
    return {"output": x * 2, "state": state + 1}    # required keys

Returning just the output value works in non-stateful mode but raises in stateful mode (state must be provided when stateful is True).

Virtual-output workers must return a list

def with_virtual_out(x):
    return {"output": x, "virtual_output": [item1, item2]}

virtual_output must be a list (possibly empty) — the runtime calls set_on_push per item.

CV wrappers

Stereo audio is not auto-mixed to mono

AudioFrame.numpy() returns the original (samples, channels) shape. Many audio models expect mono — call .mono() explicitly:

audio_arr = audio.mono()              # always 1-D, 16-bit-mixed

Image color space is enforced — don't lie

Image(arr, color_space=ColorSpace.RGB) advertises RGB on the wire. Downstream consumers that expect a specific color space (e.g., Image.to_gray() chain) compute the right conversion based on the declared color space. If you pass BGR data labeled as RGB, every downstream conversion is wrong.

image.numpy() returns the current color space

If the input was BGR, image.numpy() is BGR. If you want a specific space, call to_bgr()/to_rgb()/to_gray() — they always return the right space (and copy when conversion is needed).

Image.resize((h, w)) takes height first, then width

OpenCV is the opposite — cv2.resize(img, (w, h)). Pipelogic's wrapper takes (height, width) to match numpy shape conventions.

File and model paths

find_model_file requires exactly one match

If the directory has zero or two .pt files the helper raises. That's intentional — it forces you to be explicit about which weights file is "the model". For multi-file checkpoints, point at a parent directory and write your own loader.

ensure_local_dir respects offline mode

Set HF_HUB_OFFLINE=1 (or TRANSFORMERS_OFFLINE=1) in your container env to prevent any network calls. ensure_local_dir will refuse to download and the build-time-prefetched cache wins.

Init and startup

Don't import pipelogic lazily

pipelogic connects to the running backend at import time. If you delay the import, your component won't be able to attach to its streams. Put from pipelogic.worker import run at the top of main.py.

What's next

  • Quickstart — clean start with no footguns.
  • API: types — full reference for everything you might construct.

Was this page helpful?