{"record":{"id":"0a981d6c9d754d85","repo":"apache/beam","slug":"steps-must-be-an-iterable-not-a-string","errorCode":null,"errorMessage":"Steps must be an iterable, not a string","messagePattern":"Steps must be an iterable, not a string","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"sdks/python/apache_beam/metrics/metric.py","lineNumber":383,"sourceCode":"    return self\n\n  def with_namespace(self, namespace: Union[type, str]) -> 'MetricsFilter':\n    return self.with_namespaces([namespace])\n\n  def with_namespaces(\n      self, namespaces: Iterable[Union[type, str]]) -> 'MetricsFilter':\n    if isinstance(namespaces, str):\n      raise ValueError('Namespaces must be an iterable, not a string')\n\n    self._namespaces.update([Metrics.get_namespace(ns) for ns in namespaces])\n    return self\n\n  def with_step(self, step: str) -> 'MetricsFilter':\n    return self.with_steps([step])\n\n  def with_steps(self, steps: Iterable[str]) -> 'MetricsFilter':\n    if isinstance(steps, str):\n      raise ValueError('Steps must be an iterable, not a string')\n\n    self._steps.update(steps)\n    return self\n\n\nclass Lineage:\n  \"\"\"Standard collection of metrics used to record source and sinks information\n  for lineage tracking.\"\"\"\n\n  LINEAGE_NAMESPACE = \"lineage\"\n  SOURCE = \"sources_v2\"\n  SINK = \"sinks_v2\"\n\n  _METRICS = {\n      SOURCE: Metrics.bounded_trie(LINEAGE_NAMESPACE, SOURCE),\n      SINK: Metrics.bounded_trie(LINEAGE_NAMESPACE, SINK)\n  }\n","sourceCodeStart":365,"sourceCodeEnd":401,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/python/apache_beam/metrics/metric.py#L365-L401","documentation":"MetricsFilter.with_steps guards against the common mistake of passing a single step name string where an iterable of names is expected; without the check, the string would be iterated character by character and silently match nothing.","triggerScenarios":"MetricsFilter().with_steps('Generate/Process') instead of with_steps(['Generate/Process']); triggered directly or via with_step misuse.","commonSituations":"MetricResults.query() with a filter built from a single step name pulled from config or a variable.","solutions":["Wrap the step in a list: with_steps(['Generate/Process']).","Use with_step('Generate/Process') for a single step.","Validate the input is a non-str iterable before calling."],"exampleFix":"- MetricsFilter().with_steps('Process')\n+ MetricsFilter().with_steps(['Process'])","handlingStrategy":"validation","validationCode":"steps = [step] if isinstance(step, str) else list(step)\nfilter_ = MetricsFilter().with_steps(steps)","typeGuard":"def as_step_list(steps):\n    return [steps] if isinstance(steps, str) else list(steps)","tryCatchPattern":"try:\n    f = MetricsFilter().with_steps(steps)\nexcept ValueError:\n    f = MetricsFilter().with_steps([steps])","preventionTips":["Use with_step() for a single step name.","Wrap scalar step names in a list before with_steps().","Validate config-provided steps are lists of strings."],"tags":["python","apache-beam","metrics-filter","argument-validation"],"backgroundTag":"invalid-argument-format","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}