fix(util): keep None as null in to_pod instead of the string "None" - #3368
fix(util): keep None as null in to_pod instead of the string "None"#3368kevin9327 wants to merge 1 commit into
Conversation
to_pod() passes str/int/float/bool through unchanged but fell through to str(value) for None, turning it into the string "None". output_steps() and FlowSpec._graph_info run every decorator's attribute dict through to_pod(), and most decorators default their optional attributes to None. @resources(cpu=2, memory=8192) therefore serialized gpu/disk/shared_memory as "None" rather than null. "None" is a truthy string, so a consumer of _graph_info reading attributes["gpu"] concludes a GPU was requested when the user never asked for one. None is already plain-old-data, so returning it unchanged is consistent with the other JSON-native scalars the function already passes through.
Greptile SummaryThis PR corrects
Confidence Score: 5/5The PR appears safe to merge, with the narrowly scoped null-preservation fix covered at both utility and graph-output levels. The new branch preserves a valid POD value without affecting other scalar or fallback conversions, and no repository consumer was found to depend on the previous
|
| Filename | Overview |
|---|---|
| metaflow/util.py | Adds an explicit None passthrough before existing scalar and container conversion logic. |
| test/unit/test_to_pod.py | Covers direct, nested, and falsiness behavior for preserved None values. |
| test/unit/test_graph_structure.py | Verifies unset resource decorator attributes remain null in output_steps() metadata. |
Reviews (1): Last reviewed commit: "fix(util): keep None as null in to_pod i..." | Re-trigger Greptile
Shriprasad-P
left a comment
There was a problem hiding this comment.
Independently reproduced on current master (72591a0): to_pod(None) is "None", and @resources(cpu=2, memory=8192) serializes gpu/disk/shared_memory as the truthy string "None" through output_steps().
The is None passthrough in to_pod is the right place and the right test (is None, not falsiness). Unset resource attributes become JSON null on this head; False/0/""/callables are unchanged. Production to_pod callers are graph.py and flowspec.py graph metadata; no in-repo consumer matches to_pod output against "None".
Focused tests: 66 passed (test/unit/test_to_pod.py, test/unit/test_graph_structure.py). Black/pre-commit on the three changed files passed locally.
GitHub Actions Test (pre-commit + Python matrix) has not run on this SHA; only Greptile Review is present. That is not a code defect in this diff.
No substantive review findings. Approving the change.
Shriprasad-P
left a comment
There was a problem hiding this comment.
Reviewed the to_pod change and the related regression coverage. I reproduced the current behavior where None is serialized as the string "None" and verified that this patch correctly preserves it as None, producing JSON null while leaving other scalar behavior unchanged.
I also checked the graph/decorator path and relevant compatibility cases, and the focused tests and pre-commit checks pass locally. I did not find any blocking issues with this change.
Shriprasad-P
left a comment
There was a problem hiding this comment.
Review
Clear bugfix: to_pod(None) must stay JSON-null instead of becoming the string "None", especially for unset @resources attributes in _graph_info.
Strengths
- Early return is small and correct; nested cases work because recursion hits the same path.
- Unit tests cover top-level, nested dict/list, and the truthiness pitfall that motivated the change.
Approve.
Shriprasad-P
left a comment
There was a problem hiding this comment.
Review
Clear bugfix: to_pod(None) must stay JSON-null instead of becoming the string "None", especially for unset @resources attributes in _graph_info.
Strengths
- Early return is small and correct; nested cases work because recursion hits the same path.
- Unit tests cover top-level, nested dict/list, and the truthiness pitfall that motivated the change.
Approve.
PR Type
Summary
to_pod()turnedNoneinto the string"None". Because_graph_infoserializes every decorator's attribute dict through
to_pod(), and mostdecorators default their optional attributes to
None, a plain@resources(cpu=2, memory=8192)was recorded as"gpu": "None"instead of"gpu": null."None"is truthy, so anything reading those attributes backconcludes a GPU was requested.
Issue
No existing issue -- I hit this while reading a run's
_graph_infoand can fileone first if you'd prefer that order.
Reproduction
Runtime: local
Commands to run:
pip install -e . python repro_to_pod.pyrepro_to_pod.py-- goes through the sameoutput_steps()call thatFlowSpec._graph_infouses (metaflow/flowspec.py:535,metaflow/graph.py:618):Where evidence shows up: the
_graph_infoartifact -- so alsocurrent.graph, thegraph_infopassed to cards (card_cli.py:721), andRun(...)["_graph_info"].datavia the client.Before (on master)
After
Root Cause
metaflow/util.py:563.to_pod()passes the JSON-native scalars throughunchanged and ends with a catch-all
return str(value):Nonematches none of the branches above, so it reachesstr(value)andbecomes
"None". The invariant the function is supposed to hold -- "valuesthat are already plain-old-data come back unchanged" -- is broken for the one
POD value that isn't a
str, a number or a container.The reason it shows up so widely is that
node_to_dict()ingraph.py:618does
"attributes": to_pod(deco.attributes), anddeco.attributesis thedecorator's
defaultsdict updated with whatever the user passed. Unsetoptional attributes stay
None. For@resourcesthat isgpu/disk/shared_memory; the same holds for other decorators that defaultoptional attributes to
None.Why This Fix Is Correct
Two added lines, ahead of the existing scalar branch:
Noneis valid POD and serializes to JSONnull, so returning it unchanged isexactly what the surrounding branches already do for
str,int,floatandbool. Nothing else in the function changes, and no other value can reach thenew branch.
Failure Modes Considered
comparing against
"None"; the only== "None"inmetaflow/iscmd/develop/stub_generator.py:499, which inspects type annotations andnever sees
to_podoutput. Card modules render_graph_infoattributes fordisplay rather than matching on them.
written into new
_graph_infoartifacts. Runs recorded before this changekeep their
"None"strings and still load, since nothing parses the value --a reader that used to get a truthy
"None"now gets a falsyNone, which isthe intended reading.
boolregressing to the catch-all.boolsubclassesint, soTrue/Falsestill hit the existingisinstance(value, (str, int, float))branch and are unaffected; the new branch is
is None, not falsiness, so0,""andFalseare untouched. Covered by the existingtest_to_pod_primitives.to_podrecurses into keys as well(
{to_pod(k): to_pod(v) ...}). ANonekey now staysNoneinstead ofbecoming
"None"; that is still a valid Python dict key, andjson.dumpscoerces it to
"null"the same way it coerces other non-string keys.Tests
Added to
test/unit/test_to_pod.py:test_to_pod_none_is_preservedtest_to_pod_none_nested_in_dict_and_listtest_to_pod_none_is_falsy_after_conversionand to
test/unit/test_graph_structure.py(next to the existingoutput_steps()/to_podregression test):test_unset_decorator_attributes_stay_null_in_output_stepsPinned against regressions -- the whole of
test/unitbefore and after thechange, same invocation:
The delta is exactly the four new tests. In the interest of being straight
about it: those 21 failures and 103 errors are pre-existing on my machine and
are not related to this change. I develop on Windows, which Metaflow does not
support (
setup.pyclassifiers list only macOS and Linux) -- several coremodules
import fcntlat module scope, so every test that actually spawns a runfails locally for me regardless of this patch. The counts are identical before
and after, and the pure-logic tests that matter here run fine. I am relying on
CI for the Linux/macOS matrix.
black25.12.0 with the target list from.pre-commit-config.yamlreports allthree files unchanged.
Non-Goals
I did not touch the catch-all
return str(value)for genuinely non-POD values(that is what makes callables and arbitrary objects safe to embed), and I did
not change any decorator's defaults.
AI Tool Usage
Claude Code, used to grep the callers of
to_podand to draft this description.The reproduction, the before/after test runs and the counts above are from
commands I actually ran; I understand why
Nonefell through tostr(value)and why moving the check ahead of the scalar branch is safe.