{"record":{"id":"d5a62a37999b21c1","repo":"apache/beam","slug":"file-path-is-not-a-valid-py-file","errorCode":null,"errorMessage":"File \"{path}\" is not a valid .py file.","messagePattern":"File \"(.+?)\" is not a valid \\.py file\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"sdks/python/apache_beam/yaml/yaml_mapping.py","lineNumber":308,"sourceCode":"        }}\n      }}\n    }}\n    return obj;\n  }}\n\n  function __wrapper__(row) {{\n    return {user_entrypoint}(__convert_dates__(row));\n  }}\n  \"\"\"\n\n  return _JsFunctionWrapper(source_code, '__wrapper__')\n\n\ndef _expand_python_mapping_func(\n    original_fields, expression=None, callable=None, path=None, name=None):\n  if path and name:\n    if not path.endswith('.py'):\n      raise ValueError(f'File \"{path}\" is not a valid .py file.')\n    py_file = FileSystems.open(path).read().decode()\n\n    return python_callable.PythonCallableWithSource.load_from_script(\n        py_file, name)\n\n  elif expression:\n    # TODO(robertwb): Consider constructing a single callable that takes\n    # the row and returns the new row, rather than invoking (and unpacking)\n    # for each field individually.\n    source = '\\n'.join(['def fn(__row__):'] + [\n        f'  {name} = __row__.{name}'\n        for name in original_fields if name in expression\n    ] + ['  return (' + expression + ')'])\n\n  else:\n    source = callable\n\n  return python_callable.PythonCallableWithSource(source)","sourceCodeStart":290,"sourceCodeEnd":326,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/python/apache_beam/yaml/yaml_mapping.py#L290-L326","documentation":"The Beam YAML MapToFields transform raises this when a Python UDF is specified via both `path` and `name` but the file path does not end with '.py'. The transform loads the script with PythonCallableWithSource.load_from_script and requires a .py extension as a sanity check.","triggerScenarios":"Calling _expand_python_mapping_func with {'path': '...', 'name': '...'} where path has a non-.py extension, e.g. pointing at a compiled .pyc, a .txt, a module directory, or an IPython notebook .ipynb.","commonSituations":"Users reference a packaged module, a notebook, a .pyc, or a file with a missing/wrong extension; also happens when copy-pasting config from a JavaScript example and forgetting to change the extension.","solutions":["Ensure the `path` points to a plain-text file ending in '.py' containing the callable named by `name`.","Extract the function into a .py file if it currently lives in a notebook or module.","Alternatively use the inline `expression` or `callable` config options instead of `path`."],"exampleFix":"# before\nconfig:\n  path: transforms/udf.pyc\n  name: my_fn\n# after\nconfig:\n  path: transforms/udf.py\n  name: my_fn","handlingStrategy":"validation","validationCode":"assert path.endswith('.py'), f'File \"{path}\" is not a valid .py file.'","typeGuard":"def is_python_udf_spec(cfg: dict) -> bool:\n    return isinstance(cfg.get('path'), str) and cfg['path'].endswith('.py') and bool(cfg.get('name'))","tryCatchPattern":"try:\n    out = run_pipeline(spec)\nexcept ValueError as e:\n    if 'is not a valid .py file' in str(e):\n        spec['config']['path'] = spec['config']['path'] + '.py'\n        out = run_pipeline(spec)\n    else:\n        raise","preventionTips":["Reference only plain .py text files, never .pyc or notebooks","Always pair `path` with `name` for Python UDFs","Verify the file is committed/uploaded and readable by the worker"],"tags":["yaml","beam-yaml","python","file-extension","validation"],"backgroundTag":"invalid-argument-value","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-20T03:17:13.778Z"}