{"record":{"id":"0fd2b5239ca59ecd","repo":"huggingface/smolagents","slug":"source-code-must-define-a-class","errorCode":null,"errorMessage":"Source code must define a class","messagePattern":"Source code must define a class","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/smolagents/tool_validation.py","lineNumber":231,"sourceCode":"                    self.invalid_attributes.append(\n                        f\"Class attribute 'name' must be a valid Python identifier and not a reserved keyword, found '{node.value.value}'\"\n                    )\n\n        def _check_init_function_parameters(self, node):\n            # Check defaults in parameters\n            for arg, default in reversed(list(zip_longest(reversed(node.args.args), reversed(node.args.defaults)))):\n                if default is None:\n                    if arg.arg != \"self\":\n                        self.non_defaults.add(arg.arg)\n                elif not isinstance(default, (ast.Constant, ast.Dict, ast.List, ast.Set)):\n                    self.non_literal_defaults.add(arg.arg)\n\n    class_level_checker = ClassLevelChecker()\n    source = get_source(cls)\n    tree = ast.parse(source)\n    class_node = tree.body[0]\n    if not isinstance(class_node, ast.ClassDef):\n        raise ValueError(\"Source code must define a class\")\n    class_level_checker.visit(class_node)\n\n    errors = []\n    # Check invalid class attributes\n    if class_level_checker.invalid_attributes:\n        errors += class_level_checker.invalid_attributes\n    if class_level_checker.complex_attributes:\n        errors.append(\n            f\"Complex attributes should be defined in __init__, not as class attributes: \"\n            f\"{', '.join(class_level_checker.complex_attributes)}\"\n        )\n    if class_level_checker.non_defaults:\n        errors.append(\n            f\"Parameters in __init__ must have default values, found required parameters: \"\n            f\"{', '.join(class_level_checker.non_defaults)}\"\n        )\n    if class_level_checker.non_literal_defaults:\n        errors.append(","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/huggingface/smolagents/blob/30bb1161095dbae2271e6bc3cc4c219cc3897a57/src/smolagents/tool_validation.py#L213-L249","documentation":"validate_tool_attributes parses the source code of a Tool subclass via inspect/ast and expects the first top-level statement to be an ast.ClassDef. If the retrieved source starts with something else (decorator-heavy definitions, dynamically generated classes, comments/encoding lines preceding in odd ways, or a class created via type() or exec), the check fails with ValueError('Source code must define a class').","triggerScenarios":"Calling validate_tool_attributes (directly or via Tool.to_dict / get_tools_definition_code) on a class whose get_source output does not begin with a class statement: classes built dynamically (type(...)), defined in REPL/exec/lambda contexts, or whose source retrieval returns a module/statement other than the class.","commonSituations":"Programmatically generating tool classes at runtime; defining tools in Jupyter cells where source introspection is unreliable; tools defined inside functions or via metaclass factories; source-unavailable frozen/compiled environments (.pyc-only installs).","solutions":["Define the Tool subclass statically at module top level so inspect.getsource returns a clean class statement","If generating tools dynamically, emit real source text and exec it in a module so a ClassDef exists","Ensure the file defining the tool ships as .py source (not a binary/REPL-only artifact)","Drop validation (skip validate_tool_attributes / avoid to_dict) for throwaway dynamic classes"],"exampleFix":"# before\nMyTool = type(\"MyTool\", (Tool,), {\"name\": \"my_tool\", ...})  # no source ClassDef\n\n# after\nclass MyTool(Tool):\n    name = \"my_tool\"\n    description = \"...\"\n    inputs = {...}\n    output_type = \"text\"\n    def forward(self, ...):\n        ...","handlingStrategy":"type-guard","validationCode":"import inspect\n\ndef has_classdef_source(cls) -> bool:\n    try:\n        tree = __import__(\"ast\").parse(inspect.getsource(cls))\n    except (OSError, TypeError, SyntaxError):\n        return False\n    return isinstance(tree.body[0], __import__(\"ast\").ClassDef)","typeGuard":"def is_statically_defined(cls) -> bool:\n    import inspect\n    try:\n        return inspect.getsourcefile(cls) is not None and has_classdef_source(cls)\n    except TypeError:\n        return False","tryCatchPattern":"try:\n    validate_tool_attributes(MyTool)\nexcept ValueError as e:\n    if \"must define a class\" in str(e):\n        # rewrite tool as a static class definition\n        raise","preventionTips":["Define tools as plain module-level classes, never via type()/exec","Test tool validation in CI with the real source files present","Avoid defining tools solely in notebooks; move them to .py modules"],"tags":["smolagents","tool-validation","ast","introspection"],"backgroundTag":"source-introspection-failed","analyzedSha":"30bb1161095dbae2271e6bc3cc4c219cc3897a57","analyzedAt":"2026-08-28T18:52:54.169Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}