Skip to content

fix(parameters): repair the type-mismatch message for list-typed deploy-time fields - #3370

Open
kevin9327 wants to merge 1 commit into
Netflix:masterfrom
kevin9327:fix/deploy-time-field-type-list
Open

fix(parameters): repair the type-mismatch message for list-typed deploy-time fields#3370
kevin9327 wants to merge 1 commit into
Netflix:masterfrom
kevin9327:fix/deploy-time-field-type-list

Conversation

@kevin9327

Copy link
Copy Markdown

PR Type

  • Bug fix
  • New feature
  • Core Runtime change (higher bar -- see CONTRIBUTING.md)
  • Docs / tooling
  • Refactoring

Summary

If a @trigger(event=<function>) returns the wrong type, the user gets
TypeError: unhashable type: 'list' from inside Metaflow instead of the
ParameterFieldTypeMismatch telling them what type was expected. The
type-mismatch message could not be built, so it never got raised.

Issue

No existing issue -- happy to file one first if you prefer that order.

Reproduction

Runtime: local (the failure is in message construction, before any
orchestrator call)

Commands to run:

pip install -e .
python repro.py

repro.py -- constructs the DeployTimeField exactly as
events_decorator.py:127 does for a callable event=:

import metaflow.parameters as P
from metaflow.parameters import DeployTimeField, ParameterContext

P.context_proto = ParameterContext(
    flow_name="TriggerFlow", user_name="tester", parameter_name=None,
    logger=print, ds_type="local", configs=None,
)

# @trigger(event=<fn>) -> parameter_type=[str, dict]
f = DeployTimeField("event", [str, dict], None, lambda ctx, dt: 42, False)
try:
    f(deploy_time=True)
except Exception as e:
    print("RAISED: %s -> %s" % (type(e).__name__, e))

Where evidence shows up: parent console, during
argo-workflows create / step-functions create (or any deploy_time_eval
of the decorator's attributes).

Before (on master) -- all three list-typed call sites
@trigger(event=<fn>) returning an int                -> TypeError: unhashable type: 'list'
@trigger(event={'parameters': <fn>}) returning an int -> TypeError: unhashable type: 'list'
@trigger_on_finish(flow=<fn>) returning an int       -> TypeError: unhashable type: 'list'
After
@trigger(event=<fn>) returning an int                -> ParameterFieldTypeMismatch: The value returned by the deploy-time function for the parameter *event* field *None* has a wrong type. Expected one of the following str, dict.
@trigger(event={'parameters': <fn>}) returning an int -> ParameterFieldTypeMismatch: ... Expected one of the following list, dict, tuple.
@trigger_on_finish(flow=<fn>) returning an int       -> ParameterFieldTypeMismatch: ... Expected one of the following str, dict.

Root Cause

metaflow/parameters.py:184, in DeployTimeField._check_type:

TYPES = {bool: "bool", int: "int", float: "float", list: "list", dict: "dict"}
...
if isinstance(self.parameter_type, list):
    if not any(isinstance(val, x) for x in self.parameter_type):
        msg += "Expected one of the following %s." % TYPES[self.parameter_type]
        raise ParameterFieldTypeMismatch(msg)

The guard establishes that self.parameter_type is a list, and then uses
that list as a dict key. Lists are unhashable, so the % operand raises
TypeError while the mismatch is being reported -- ParameterFieldTypeMismatch
is never constructed. _check_type is called from __call__'s else clause,
outside its try, so the TypeError propagates raw to the user.

Two things are wrong with the lookup, not one: even if parameter_type were
hashable, TYPES has no str entry, so [str, dict] could not be rendered
from TYPES anyway.

Reachable from every list-typed DeployTimeField in the tree, all in
metaflow/plugins/events_decorator.py:

line field parameter_type user-facing form
127 event [str, dict] @trigger(event=<fn>)
196 parameters [list, dict, tuple] @trigger(event={"parameters": <fn>})
413 fq_name [str, dict] @trigger_on_finish(flow=<fn>)

Why This Fix Is Correct

Render one name per entry instead of looking the whole list up:

msg += "Expected one of the following %s." % ", ".join(
    TYPES.get(t, getattr(t, "__name__", str(t))) for t in self.parameter_type
)

TYPES.get keeps the existing spellings for the five types the table covers,
and __name__ supplies "str" (and anything else a future call site lists)
rather than raising KeyError. Only the message text changes -- the
isinstance check that decides whether to raise is untouched, so no value that
used to be accepted is now rejected or vice versa.

The sibling branch three lines below already does this safely:

elif self.parameter_type in TYPES:      # guarded
    ...
    msg += "Expected a %s." % TYPES[self.parameter_type]

which is why only the list branch was broken.

Failure Modes Considered

  1. Changing which values are accepted. The fix is inside the if not any(isinstance(...)) body, i.e. on the path that was already committed to
    raising. test_list_of_types_accepts_any_listed_type pins that str/dict
    values still pass for [str, dict] and list/tuple/dict for
    [list, dict, tuple].
  2. A parameter_type entry that is not a plain class. getattr(t, "__name__", str(t)) falls back to str(t) rather than raising, so a typing construct or
    any object without __name__ degrades to a readable repr instead of turning
    the error report into a second exception -- which is the bug being fixed.
  3. Callers catching the old TypeError. Nothing in the tree catches
    TypeError around deploy_time_eval or _check_type (I grepped); the
    nearest except TypeError is in __call__, and it wraps only the user
    function invocation, not _check_type. Callers that already handle
    ParameterFieldTypeMismatch now receive it as intended.

Tests

  • Unit tests added/updated
  • Reproduction script provided
  • CI passes
  • If tests are impractical: explain why below and provide manual evidence above

New file test/unit/test_deploy_time_field.py (pytest, module-level functions,
monkeypatch for the module-level parameter context, parametrized over the two
real list shapes), covering:

  • test_list_of_types_reports_the_mismatch -- the bug, for [str, dict] and
    [list, dict, tuple]
  • test_list_of_types_accepts_any_listed_type (pin)
  • test_single_type_mismatch_still_reports / test_single_type_match_still_passes
    (pin for the guarded sibling branch)
# before, on unmodified master, metaflow/ untouched
$ python -m pytest test_deploy_time_field.py -q
E   TypeError: unhashable type: 'list'
FAILED test_deploy_time_field.py::test_list_of_types_reports_the_mismatch[trigger_event]
FAILED test_deploy_time_field.py::test_list_of_types_reports_the_mismatch[trigger_event_parameters]
2 failed, 6 passed

# after
$ python -m pytest test_deploy_time_field.py -q
8 passed

Whole of test/unit, same invocation, before and after:

before: 21 failed, 499 passed, 20 skipped, 103 errors
after:  21 failed, 507 passed, 20 skipped, 103 errors

The delta is exactly the eight new tests.

What I could not run: I develop on Windows, which Metaflow does not support
(setup.py classifiers list macOS and Linux only) -- several core modules
import fcntl at module scope, so every test that spawns a real run fails here
regardless of this patch. That is the 21 failures / 103 errors above; the counts
are identical before and after. I also did not deploy a flow to Argo or Step
Functions to watch this message appear there, since I have no cluster -- the
reproduction drives DeployTimeField directly, which is the code
deploy_time_eval calls. I am relying on CI for the Linux/macOS matrix.

black 25.12.0 with the target list from .pre-commit-config.yaml reports both
files unchanged.

Non-Goals

I did not add str to the TYPES table (that would change the wording of the
other branch's messages too), did not touch the isinstance vs type(val) !=
asymmetry between the two branches, and did not change any of the
events_decorator.py call sites.

AI Tool Usage

  • No AI tools were used in this contribution
  • AI tools were used (describe below)

Claude Code, used to enumerate the DeployTimeField call sites and to draft
this description. The reproduction and the before/after runs are commands I
actually ran; I understand why a list cannot be a dict key here and why the
guarded sibling branch was unaffected.

…oy-time fields

_check_type looked up TYPES[self.parameter_type] on the branch where
parameter_type is a *list* of acceptable types. A list is unhashable, so
building the message raised

    TypeError: unhashable type: 'list'

before ParameterFieldTypeMismatch was ever constructed. The user never saw
the message the code was trying to give them.

Reachable from three call sites in events_decorator.py, all of which pass a
list: @trigger(event=<fn>) and @trigger_on_finish(flow=<fn>) use [str, dict],
and event={"parameters": <fn>} uses [list, dict, tuple].

TYPES also has no entry for str, so even a hashable lookup would have raised
KeyError; join the per-type names with a __name__ fallback instead. The
sibling branch three lines below guards its lookup with
`elif self.parameter_type in TYPES`, which is why only this one was affected.
@greptile-apps

greptile-apps Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR repairs error-message construction for list-typed deploy-time fields without changing type-acceptance behavior.

  • Formats each allowed type individually, including types absent from the existing name table.
  • Restores the intended ParameterFieldTypeMismatch for invalid callable trigger values.
  • Adds coverage for both production list shapes, accepted values, and the existing single-type behavior.

Confidence Score: 5/5

The PR appears safe to merge; no actionable correctness, security, or repository-rule issues were identified.

The formatter handles every type used by the affected event-decorator call sites, preserves successful value handling, and now raises the intended domain exception on mismatches.

Important Files Changed

Filename Overview
metaflow/parameters.py Safely renders each allowed type in list-typed deploy-time mismatch messages while preserving existing validation behavior.
test/unit/test_deploy_time_field.py Adds focused regression and compatibility coverage for list-typed and single-type DeployTimeField validation.

Reviews (1): Last reviewed commit: "fix(parameters): repair the type-mismatc..." | Re-trigger Greptile

@Shriprasad-P Shriprasad-P left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the DeployTimeField type-mismatch fix and the associated regression tests. I reproduced the current TypeError: unhashable type: 'list' behavior on master and verified that this change restores the intended ParameterFieldTypeMismatch for both list-typed trigger cases without changing accepted-value behavior.

The focused tests and pre-commit checks pass locally, and I did not find any substantive code issues. I’m leaving this as a comment rather than an approval for now since the normal CI checks have not run yet and the repository’s issue/process requirements for this area still need to be satisfied.

@Shriprasad-P Shriprasad-P left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Nice fix for list-typed DeployTimeField mismatch messages. Using TYPES[self.parameter_type] when parameter_type is a list would KeyError (or produce a useless message) and hide the real type mismatch from @trigger users.

Joining human-readable type names with a __name__ fallback is the right UX, and the new tests cover list-of-types and single-type paths.

Approve.

@Shriprasad-P Shriprasad-P left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Nice fix for list-typed DeployTimeField mismatch messages. Using TYPES[self.parameter_type] when parameter_type is a list would KeyError (or produce a useless message) and hide the real type mismatch from @trigger users.

Joining human-readable type names with a __name__ fallback is the right UX, and the new tests cover list-of-types and single-type paths.

Approve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants