Search code examples
pythonyamlpyyaml

Python Console cannot find associated class when loading from yaml file


I would like to save an instance of a class in a YAML File (to make it more human readable).

Consider this example code:

import tempfile
from pathlib import Path

import yaml


class MyClass:
    def __init__(self, attribute):
        self.attribute = attribute

if __name__=='__main__':

    my_instance = MyClass(attribute="Hello World")

    with tempfile.TemporaryDirectory() as tmpdir:
        # save file
        with open(Path(tmpdir) / 'my_save.yaml','w') as file:
            yaml.dump(my_instance,file)

        # load file
        with open(Path(tmpdir) / 'my_save.yaml','r') as file:
            my_instance_loaded = yaml.load(file,Loader= yaml.UnsafeLoader)

        print(my_instance_loaded.attribute)

This code does work without hiccups if I run the script via the commandline.

But if run the same code in the Python console, I get the following error:

yaml.constructor.ConstructorError: while constructing a Python object cannot find 'MyClass' in the module '__main__'

While this is not an acute problem, my approach seems to be error prone. Especially if I load the yaml file in other parts of my program.

Is there a way to tell the yaml loader just to go with the name of the class (e.g.MyClass) instead looking up the whole "object-Path" (e.g.__main__.MyClass)?


Solution

  • If you don't mind modifying your objects so that they're registered for PyYAML, you can make MyClass inherit from yaml.YAMLObject, like they instruct in the documentation

    i.e:

    import yaml
    
    class MyClass(yaml.YAMLObject):
        yaml_tag = '!MyClass'
    
        def __init__(self, attribute):
            self.attribute = attribute
    
    my_instance = MyClass(attribute="Hello World")
    
    dumped = yaml.dump(my_instance)
    
    print(dumped)
    > !MyClass
    > attribute: Hello World
    
    loaded = yaml.load(dumped)
    
    print(loaded.__dict__)
    > {'attribute': 'Hello World'}
    
    print(my_instance.__dict__)
    > {'attribute': 'Hello World'}
    

    Alternatively, you could manually register constructors and representers for your objects like this:

    import yaml
    
    class MyClass:
        def __init__(self, attribute):
            self.attribute = attribute
    
    # This maps objects attributes to a dictionary and a `yaml_tag` to it
    def myclass_representer(dumper, data):
        return dumper.represent_mapping("!MyClass", {"attribute": data.attribute})
    
    # This maps a `MappingNode` to the construction of a class. Note that we construct a mapping because before we represented a mapping
    def myclass_constructor(loader, node):
        value = loader.construct_mapping(node)
        return MyClass(**value)
    
    yaml.add_representer(MyClass, myclass_representer)
    yaml.add_constructor('!MyClass', myclass_constructor)
    
    my_instance = MyClass(attribute="Hello World")
    
    dumped = yaml.dump(my_instance)
    
    print(dumped)
    > !MyClass
    > attribute: Hello World
    
    loaded = yaml.load(dumped)
    
    print(loaded.__dict__)
    > {'attribute': 'Hello World'}
    
    print(my_instance.__dict__)
    > {'attribute': 'Hello World'}