{"record":{"id":"58a638185ca4d315","repo":"apache/beam","slug":"cannot-use-an-unkeyed-model-handler-with-pre-or","errorCode":null,"errorMessage":"Cannot use an unkeyed model handler with pre or postprocessing functions defined in a keyed model handler. All pre/postprocessing functions must be defined on the outer modelhandler.","messagePattern":"Cannot use an unkeyed model handler with pre or postprocessing functions defined in a keyed model handler\\. All pre/postprocessing functions must be defined on the outer modelhandler\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"sdks/python/apache_beam/ml/inference/base.py","lineNumber":785,"sourceCode":"            'pre/postprocessing functions must be defined on the outer model'\n            'handler.')\n      self._env_vars = getattr(unkeyed, '_env_vars', {})\n      self._unkeyed = unkeyed\n      return\n\n    self._max_models_per_worker_hint = max_models_per_worker_hint\n    # To maintain an efficient representation, we will map all keys in a given\n    # KeyModelMapping to a single id (the first key in the KeyModelMapping\n    # list). We will then map that key to a ModelHandler. This will allow us to\n    # quickly look up the appropriate ModelHandler for any given key.\n    self._id_to_mh_map: dict[str, ModelHandler[ExampleT, PredictionT,\n                                               ModelT]] = {}\n    self._key_to_id_map: dict[str, str] = {}\n    for mh_tuple in unkeyed:\n      mh = mh_tuple.mh\n      keys = mh_tuple.keys\n      if len(mh.get_preprocess_fns()) or len(mh.get_postprocess_fns()):\n        raise ValueError(\n            'Cannot use an unkeyed model handler with pre or '\n            'postprocessing functions defined in a keyed model handler. All '\n            'pre/postprocessing functions must be defined on the outer model'\n            'handler.')\n      hints = mh.get_resource_hints()\n      if len(hints) > 0:\n        logging.warning(\n            'mh %s defines the following resource hints, which will be'\n            'ignored: %s. Resource hints are not respected when more than one '\n            'model handler is used in a KeyedModelHandler. If you would like '\n            'to specify resource hints, you can do so by overriding the '\n            'KeyedModelHandler.get_resource_hints() method.',\n            mh,\n            hints)\n      batch_kwargs = mh.batch_elements_kwargs()\n      if len(batch_kwargs) > 0:\n        logging.warning(\n            'mh %s defines the following batching kwargs which will be '","sourceCodeStart":767,"sourceCodeEnd":803,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/python/apache_beam/ml/inference/base.py#L767-L803","documentation":"The multi-model (list) constructor of KeyedModelHandler accepts handler/key tuples; each inner handler must be free of preprocess/postprocess functions because they must be defined on the outer keyed handler. A ValueError is raised for any tuple whose handler has decorated functions.","triggerScenarios":"KeyedModelHandler([KeyedModelHandlerTuple(mh1, ['k1']), KeyedModelHandlerTuple(mh2, ['k2'])]) where mh1 or mh2 has pre/postprocess fns attached via with_preprocess_fns/with_postprocess_fns.","commonSituations":"Building multi-model serving setups where each cohort's handler was individually decorated; refactoring decorated handlers into keyed multi-handler configs without moving the functions outward.","solutions":["Strip the pre/postprocessing functions from each inner handler (use undecorated instances).","Attach the functions once on the resulting KeyedModelHandler via with_preprocess_fns/with_postprocess_fns.","If per-model preprocessing is truly needed, implement it inside a custom ModelHandler subclass instead."],"exampleFix":"# before\nmh_a = HandlerA().with_preprocess_fns(fn)\nkeyed = KeyedModelHandler([KeyedModelHandlerTuple(mh_a, ['a'])])\n\n# after\nkeyed = KeyedModelHandler([KeyedModelHandlerTuple(HandlerA(), ['a'])]).with_preprocess_fns(fn)","handlingStrategy":"validation","validationCode":"bad = [mh for mh, _ in [(t.mh, t.keys) for t in tuples] if mh.get_preprocess_fns() or mh.get_postprocess_fns()]\nassert not bad, f'Inner handlers with pre/post fns: {bad}'","typeGuard":"def all_undecorated(tuples):\n    return all(not t.mh.get_preprocess_fns() and not t.mh.get_postprocess_fns() for t in tuples)","tryCatchPattern":"try:\n    keyed = KeyedModelHandler(tuples)\nexcept ValueError as e:\n    if 'pre or postprocessing' in str(e):\n        tuples = [KeyedModelHandlerTuple(strip_fns(t.mh), t.keys) for t in tuples]\n        keyed = KeyedModelHandler(tuples).with_preprocess_fns(fn)\n    else:\n        raise","preventionTips":["Define pre/postprocessing once on the KeyedModelHandler, never per inner handler.","When refactoring decorated handlers into keyed setups, remove decorations first.","Assert handler tuples are undecorated in pipeline-builder utilities."],"tags":["python","apache-beam","ml-inference","model-handler","api-misuse"],"backgroundTag":"conflicting-config-options","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-14T21:17:11.552Z"}