Skip to content

DAGNode.foreach_param / output_steps()["foreach_artifact"] correctly populated on python3.12+#3237

Merged
saikonen merged 3 commits into
Netflix:masterfrom
vivekkhimani:vivek/dag-metadata-bug
Jun 5, 2026
Merged

DAGNode.foreach_param / output_steps()["foreach_artifact"] correctly populated on python3.12+#3237
saikonen merged 3 commits into
Netflix:masterfrom
vivekkhimani:vivek/dag-metadata-bug

Conversation

@vivekkhimani

@vivekkhimani vivekkhimani commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

PR Type

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

Summary

The static graph parser silently drops the foreach= / num_parallel= keyword values on Python 3.14+, so DAGNode.foreach_param, num_parallel, and the foreach_artifact exposed by output_steps() all come back None. Node typing is unaffected, only the artifact name/value is lost. This swaps the removed ast.Str.s read for an ast.Constant fallback (the same one already used for switch conditions a few lines above).

Issue

Fixes #3236

Reproduction

Runtime: local

Commands to run: Run the flow below under Python 3.14 (where ast.Str and the .s alias are gone). On 3.12/3.13 the deprecated .s alias still resolves, so the bug stays hidden there. Save it to a file and run it with python <file>.py, or paste it into a Python 3.14 shell.

from metaflow import FlowSpec, step
from metaflow.graph import FlowGraph


class ForeachRepro(FlowSpec):
    @step
    def start(self):
        self.items = [1, 2, 3]
        self.next(self.process, foreach="items")

    @step
    def process(self):
        self.next(self.join)

    @step
    def join(self, inputs):
        self.next(self.end)

    @step
    def end(self):
        pass


g = FlowGraph(ForeachRepro)
print(g["start"].type)                                        # 'foreach'
print(g["start"].foreach_param)                               # expected 'items'
print(g.output_steps()[0]["start"].get("foreach_artifact"))   # expected 'items'

Where evidence shows up: parent console (the static graph metadata, also consumed by show, the DAG card, and Argo/Airflow compilation)

Before (Python 3.14.3)
type            = 'foreach'
foreach_param   = None
foreach_artifact = None
After (Python 3.14.3)
type            = 'foreach'
foreach_param   = 'items'
foreach_artifact = 'items'

Root Cause

In DAGNode._parse, the keyword extractor read getattr(k.value, "s", None). .s was the attribute on the legacy ast.Str node. Since Python 3.8 string literals are ast.Constant with the value in .value, and the .s compatibility alias was removed in 3.14. So on 3.14 the getattr falls through to its None default and the value is dropped.

Type detection still works because it checks for the presence of the keyword name ("foreach" in keywords), not its value, which is why the type stays correct while the value goes missing. The switch-condition branch just above already had the ast.Constant fallback, so this was effectively a half-applied migration.

Why This Fix Is Correct

Restores the invariant that a string/int literal keyword in self.next(...) round-trips to foreach_param / num_parallel. The _kw_value helper keeps the ast.Str branch for older interpreters and reads ast.Constant.value everywhere else, matching the existing switch-condition handling. Nothing else in the parse path changes.

Failure Modes Considered

  1. Older Python (< 3.14): ast.Str/.s still exist, and the hasattr(ast, "Str") branch preserves the old path. Verified no regression on 3.12.
  2. Non-literal keyword values (e.g. foreach=some_var): the node is an ast.Name, not ast.Constant, so _kw_value returns None, same as before. No behavior change there.

Tests

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

Existing test/unit/test_graph_structure.py and test_sourceless_dag_node.py pass on 3.14 (55 passed). No new unit test added yet since the existing suite runs on the default interpreter where the bug is masked; happy to add a 3.14-gated regression test if preferred.

Non-Goals

No change to graph topology, edge resolution, matching_join, or the switch-condition path. Not touching how non-literal keyword values are handled.

AI Tool Usage

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

Claude Code was used to reproduce the bug on Python 3.14.3, apply the fix, and verify against the existing unit tests. All generated code was reviewed, understood, and tested.

@greptile-apps

greptile-apps Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a Python 3.14 regression where foreach= and num_parallel= keyword values were silently dropped during static graph parsing because ast.Str and its .s alias were removed. The fix adds a small _ast_literal_value helper at module level that branches on ast.Str for older interpreters and reads ast.Constant.value everywhere else, then uses it consistently for both the keyword-dict build and the switch-condition path.

  • Adds _ast_literal_value module-level helper with clear docstring covering both the old ast.Str path (pre-3.14) and the new ast.Constant path (3.8+/3.14).
  • Replaces the broken getattr(k.value, "s", None) in the keywords dict comprehension with _ast_literal_value, restoring foreach_param, num_parallel, and foreach_artifact on Python 3.14+.
  • Refactors the switch-condition extraction to use the same helper, removing the previously duplicated compat logic.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to AST node reading, backward-compatible with older interpreters, and restores correct behavior on Python 3.14+ without altering graph topology or any other parse path.

The fix correctly handles both the legacy ast.Str path (guarded by hasattr(ast, 'Str')) and the modern ast.Constant path. The only code paths touched are the keyword-value extractor and the switch-condition branch, both of which are now consistent. The change introduces no new logic — it consolidates existing, already-working patterns.

No files require special attention.

Important Files Changed

Filename Overview
metaflow/graph.py Introduces module-level _ast_literal_value helper to replace getattr(k.value, "s", None) with a correct ast.Constant-aware lookup; also refactors the switch-condition extraction to use the same helper, eliminating duplicated AST-compat logic.

Reviews (3): Last reviewed commit: "refactor: hoist keyword-value extractor ..." | Re-trigger Greptile

Comment thread metaflow/graph.py Outdated
vivekkhimani and others added 2 commits June 3, 2026 13:00
Move the ast.Str/ast.Constant literal extractor out of DAGNode._parse
into a module-level _ast_literal_value() so it is not re-created per
call, and reuse it for the switch-condition extraction that previously
duplicated the same logic. Also keeps the keywords dict multi-line so
black is satisfied (the longer helper name no longer collapses to one
line), fixing the pre-commit failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@vivekkhimani vivekkhimani changed the title dag metadata bug fix DAGNode.foreach_param / output_steps()["foreach_artifact"] correctly populated on python3.12+ Jun 3, 2026
@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 44.44444% with 5 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (master@91923d2). Learn more about missing BASE report.

Files with missing lines Patch % Lines
metaflow/graph.py 44.44% 3 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##             master    #3237   +/-   ##
=========================================
  Coverage          ?   28.32%           
=========================================
  Files             ?      381           
  Lines             ?    52448           
  Branches          ?     9252           
=========================================
  Hits              ?    14855           
  Misses            ?    36628           
  Partials          ?      965           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@romain-intel romain-intel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i'll let sakari take a quick look too but lgtm.

@saikonen
saikonen self-requested a review June 5, 2026 11:47
@saikonen
saikonen merged commit 6cc3431 into Netflix:master Jun 5, 2026
42 checks passed
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.

DAGNode.foreach_param / output_steps()["foreach_artifact"] is always None on Python ≥ 3.12 (static graph parse uses removed ast.Str.s)

3 participants