49

I'm trying to convert the data from a simple object graph into a dictionary. I don't need type information or methods and I don't need to be able to convert it back to an object again.

I found this question about creating a dictionary from an object's fields, but it doesn't do it recursively.

Being relatively new to python, I'm concerned that my solution may be ugly, or unpythonic, or broken in some obscure way, or just plain old NIH.

My first attempt appeared to work until I tried it with lists and dictionaries, and it seemed easier just to check if the object passed had an internal dictionary, and if not, to just treat it as a value (rather than doing all that isinstance checking). My previous attempts also didn't recurse into lists of objects:

def todict(obj):
    if hasattr(obj, "__iter__"):
        return [todict(v) for v in obj]
    elif hasattr(obj, "__dict__"):
        return dict([(key, todict(value)) 
            for key, value in obj.__dict__.iteritems() 
            if not callable(value) and not key.startswith('_')])
    else:
        return obj

This seems to work better and doesn't require exceptions, but again I'm still not sure if there are cases here I'm not aware of where it falls down.

Any suggestions would be much appreciated.

Community
  • 1
  • 1
Shabbyrobe
  • 12,298
  • 15
  • 60
  • 87
  • 2
    in python it is not so bad to use exceptions and sometimes it can simplify the coding, a pythonic way- EAFP (Easier to Ask Forgiveness than Permission) – Anurag Uniyal Jun 24 '09 at 04:52
  • special case could be when object has __slots__, edited answer – Anurag Uniyal Jun 24 '09 at 04:56
  • 1
    point taken, but the exception thing is a bit of a holy war and i tend towards prefering them never to be thrown unless something is truly exceptional, rather than expected program flow. each to their own on that one :) – Shabbyrobe Jun 24 '09 at 04:59
  • 1
    yes mostly it is style but sometime exception can really simply code e.g. when inner most functions communicate thru exceptions, and sometime it can speed up things e.g. if 99.99 % objects have dict in that case instead of checking 99.99% times for dict, check for exception .01% time – Anurag Uniyal Jun 24 '09 at 05:01

14 Answers14

61

An amalgamation of my own attempt and clues derived from Anurag Uniyal and Lennart Regebro's answers works best for me:

def todict(obj, classkey=None):
    if isinstance(obj, dict):
        data = {}
        for (k, v) in obj.items():
            data[k] = todict(v, classkey)
        return data
    elif hasattr(obj, "_ast"):
        return todict(obj._ast())
    elif hasattr(obj, "__iter__") and not isinstance(obj, str):
        return [todict(v, classkey) for v in obj]
    elif hasattr(obj, "__dict__"):
        data = dict([(key, todict(value, classkey)) 
            for key, value in obj.__dict__.items() 
            if not callable(value) and not key.startswith('_')])
        if classkey is not None and hasattr(obj, "__class__"):
            data[classkey] = obj.__class__.__name__
        return data
    else:
        return obj
Ben
  • 2,314
  • 1
  • 19
  • 36
Shabbyrobe
  • 12,298
  • 15
  • 60
  • 87
  • nicely done. only implementation that works just as i wanted it to, so far. – Murat Ayfer Nov 19 '10 at 05:17
  • 4
    Thank you, that _mostly_ worked. Few caveats: in Python 3.5 `iteritems()` should be `items()`, and `[todict(v, classkey) for v in obj]` (line 10) tries to iterate characters in strings, fixed that with: `elif hasattr(obj, "__iter__") and not isinstance(obj, str)`. – Sander Verhagen Aug 20 '16 at 21:32
  • This converts my string values to None. A small but important miss. I solved this problem by catching objects of string type and returning str(obj) in those cases. – Vaibhav Tripathi Jan 20 '20 at 08:44
  • trying to convert a complex object with nested objects, I am getting this error. `RecursionError: maximum recursion depth exceeded while calling a Python object` and traceback points to this line `data = dict([(key, todict(value, classkey))`. Any idea of what this could be? – Cesar Flores Jul 08 '21 at 16:41
35

One line of code to convert an object to JSON recursively.

import json

def get_json(obj):
  return json.loads(
    json.dumps(obj, default=lambda o: getattr(o, '__dict__', str(o)))
  )

obj = SomeClass()
print("Json = ", get_json(obj))
vvvvv
  • 25,404
  • 19
  • 49
  • 81
Archit Dwivedi
  • 416
  • 4
  • 9
  • 1
    May I ask if there is a such concise way of loading an object from JSON? – Tengerye Apr 17 '20 at 04:56
  • You can do this obj.__dict__ = { 'key': 'value'} – Archit Dwivedi Apr 17 '20 at 21:10
  • to load object from json, look at this: https://stackoverflow.com/questions/6578986/how-to-convert-json-data-into-a-python-object – Peter F Aug 11 '20 at 12:44
  • 1
    This solution did not work for me. Got an ValueError: Circular reference detected. Did not further investigate since accepted solution worked. – Arigion Mar 03 '21 at 09:35
  • Probably because one of the object might have a reference to a parent object. – Archit Dwivedi Mar 04 '21 at 11:06
  • 1
    This doesn't work if you have complex objects. `ValueError: Circular reference detected` probably will pop up or you would get an error pointing that the object of type xxx is not JSON serializable. – Cesar Flores Jul 08 '21 at 16:31
  • Wow. This helper function worked very smooth for me when inserting a recursive object into mongodb. Thanks a lot! – Leon Nov 01 '21 at 19:36
8

I don't know what is the purpose of checking for basestring or object is? also dict will not contain any callables unless you have attributes pointing to such callables, but in that case isn't that part of object?

so instead of checking for various types and values, let todict convert the object and if it raises the exception, user the orginal value.

todict will only raise exception if obj doesn't have dict e.g.

class A(object):
    def __init__(self):
        self.a1 = 1

class B(object):
    def __init__(self):
        self.b1 = 1
        self.b2 = 2
        self.o1 = A()

    def func1(self):
        pass

def todict(obj):
    data = {}
    for key, value in obj.__dict__.iteritems():
        try:
            data[key] = todict(value)
        except AttributeError:
            data[key] = value
    return data

b = B()
print todict(b)

it prints {'b1': 1, 'b2': 2, 'o1': {'a1': 1}} there may be some other cases to consider, but it may be a good start

special cases if a object uses slots then you will not be able to get dict e.g.

class A(object):
    __slots__ = ["a1"]
    def __init__(self):
        self.a1 = 1

fix for the slots cases can be to use dir() instead of directly using the dict

Anurag Uniyal
  • 85,954
  • 40
  • 175
  • 219
  • Thanks for the help and inspiration. I just realised that it doesn't handle lists of objects, so I've updated my version to test for __iter__. Not sure if that's a good idea though. – Shabbyrobe Jun 24 '09 at 05:19
  • looks like it will get trickier because what happens for a object which provides a iter to iterate a list attribute which you have already put in dict, may be general solution is not possible. – Anurag Uniyal Jun 24 '09 at 05:53
  • Thanks @AnuragUniyal! You made my day! # noinspection PyProtectedMember def object_to_dict(obj): data = {} if getattr(obj, '__dict__', None): for key, value in obj.__dict__.items(): try: data[key] = object_to_dict(value) except AttributeError: data[key] = value return data else: return obj – Alexey Korolkov Mar 05 '22 at 23:57
4

A slow but easy way to do this is to use jsonpickle to convert the object to a JSON string and then json.loads to convert it back to a python dictionary:

dict = json.loads(jsonpickle.encode( obj, unpicklable=False ))

Tom
  • 18,685
  • 15
  • 71
  • 81
4

I realize that this answer is a few years too late, but I thought it might be worth sharing since it's a Python 3.3+ compatible modification to the original solution by @Shabbyrobe that has generally worked well for me:

import collections
try:
  # Python 2.7+
  basestring
except NameError:
  # Python 3.3+
  basestring = str 

def todict(obj):
  """ 
  Recursively convert a Python object graph to sequences (lists)
  and mappings (dicts) of primitives (bool, int, float, string, ...)
  """
  if isinstance(obj, basestring):
    return obj 
  elif isinstance(obj, dict):
    return dict((key, todict(val)) for key, val in obj.items())
  elif isinstance(obj, collections.Iterable):
    return [todict(val) for val in obj]
  elif hasattr(obj, '__dict__'):
    return todict(vars(obj))
  elif hasattr(obj, '__slots__'):
    return todict(dict((name, getattr(obj, name)) for name in getattr(obj, '__slots__')))
  return obj

If you're not interested in callable attributes, for example, they can be stripped in the dictionary comprehension:

elif isinstance(obj, dict):
  return dict((key, todict(val)) for key, val in obj.items() if not callable(val))
hbristow
  • 1,896
  • 1
  • 12
  • 9
2

In Python there are many ways of making objects behave slightly differently, like metaclasses and whatnot, and it can override getattr and thereby have "magical" attributes you can't see through dict, etc. In short, it's unlikely that you are going to get a 100% complete picture in the generic case with whatever method you use.

Therefore, the answer is: If it works for you in the use case you have now, then the code is correct. ;-)

To make somewhat more generic code you could do something like this:

import types
def todict(obj):
    # Functions, methods and None have no further info of interest.
    if obj is None or isinstance(subobj, (types.FunctionType, types.MethodType))
        return obj

    try: # If it's an iterable, return all the contents
        return [todict(x) for x in iter(obj)]
    except TypeError:
        pass

    try: # If it's a dictionary, recurse over it:
        result = {}
        for key in obj:
            result[key] = todict(obj)
        return result
    except TypeError:
        pass

    # It's neither a list nor a dict, so it's a normal object.
    # Get everything from dir and __dict__. That should be most things we can get hold of.
    attrs = set(dir(obj))
    try:
        attrs.update(obj.__dict__.keys())
    except AttributeError:
        pass

    result = {}
    for attr in attrs:
        result[attr] = todict(getattr(obj, attr, None))
    return result            

Something like that. That code is untested, though. This still doesn't cover the case when you override getattr, and I'm sure there are many more cases that it doens't cover and may not be coverable. :)

Lennart Regebro
  • 167,292
  • 41
  • 224
  • 251
2

No custom implementation is required. jsons library can be used.

import jsons

object_dict = jsons.dump(object_instance)
aminkr
  • 66
  • 3
1

Thanks @AnuragUniyal! You made my day! This is my variant of code that's working for me:

# noinspection PyProtectedMember
def object_to_dict(obj):
    data = {}
    if getattr(obj, '__dict__', None):
        for key, value in obj.__dict__.items():
            try:
                data[key] = object_to_dict(value)
            except AttributeError:
                data[key] = value
        return data
    else:
        return obj
Alexey Korolkov
  • 181
  • 2
  • 5
0

A little update to Shabbyrobe's answer to make it work for namedtuples:

def obj2dict(obj, classkey=None):
    if isinstance(obj, dict):
        data = {}
        for (k, v) in obj.items():
            data[k] = obj2dict(v, classkey)
        return data
    elif hasattr(obj, "_asdict"):
        return obj2dict(obj._asdict())
    elif hasattr(obj, "_ast"):
        return obj2dict(obj._ast())
    elif hasattr(obj, "__iter__"):
        return [obj2dict(v, classkey) for v in obj]
    elif hasattr(obj, "__dict__"):
        data = dict([(key, obj2dict(value, classkey))
                     for key, value in obj.__dict__.iteritems()
                     if not callable(value) and not key.startswith('_')])
        if classkey is not None and hasattr(obj, "__class__"):
            data[classkey] = obj.__class__.__name__
        return data
    else:
        return obj
oroszgy
  • 123
  • 7
0
def list_object_to_dict(lst):
    return_list = []
    for l in lst:
        return_list.append(object_to_dict(l))
    return return_list

def object_to_dict(object):
    dict = vars(object)
    for k,v in dict.items():
        if type(v).__name__ not in ['list', 'dict', 'str', 'int', 'float']:
                dict[k] = object_to_dict(v)
        if type(v) is list:
            dict[k] = list_object_to_dict(v)
    return dict
0

Looked at all solutions, and @hbristow's answer was closest to what I was looking for. Added enum.Enum handling since this was causing a RecursionError: maximum recursion depth exceeded error and reordered objects with __slots__ to have precedence of objects defining __dict__.

def todict(obj):
  """
  Recursively convert a Python object graph to sequences (lists)
  and mappings (dicts) of primitives (bool, int, float, string, ...)
  """
  if isinstance(obj, str):
    return obj
  elif isinstance(obj, enum.Enum):
    return str(obj)
  elif isinstance(obj, dict):
    return dict((key, todict(val)) for key, val in obj.items())
  elif isinstance(obj, collections.Iterable):
    return [todict(val) for val in obj]
  elif hasattr(obj, '__slots__'):
    return todict(dict((name, getattr(obj, name)) for name in getattr(obj, '__slots__')))
  elif hasattr(obj, '__dict__'):
    return todict(vars(obj))
  return obj
Will
  • 2,858
  • 6
  • 33
  • 50
0

I'd comment on the accepted answer but my rep is not high enough... The accepted answer is great but add another elif just after the if to support NamedTuples serialization to dict properly too:

    elif hasattr(obj, "_asdict"):
        return todict(obj._asdict())
Alex
  • 31
  • 1
  • 3
0

Well. Added functionality of limiting the depth to @Shabbyrobe answer. Thought it might be worth for the objects which loop back.

def todict(obj, limit=sys.getrecursionlimit(), classkey=None):
        if isinstance(obj, dict):
            if limit>=1:
                data = {}
                for (k, v) in obj.items():
                    data[k] = todict(v, limit-1,classkey)
                return data
            else:
                return 'class:'+obj.__class__.__name__
        elif hasattr(obj, "_ast"):
            return todict(obj._ast(), limit-1) if limit>=1 else {'class:'+obj.__class__.__name__}
        elif hasattr(obj, "__iter__") and not isinstance(obj, str):
            return [todict(v, limit-1, classkey) for v in obj] if limit>=1 else {'class:'+obj.__class__.__name__}
        elif hasattr(obj, "__dict__"):
            if limit>=1:
                data = dict([(key, todict(value, limit-1, classkey)) 
                    for key, value in obj.__dict__.items() 
                    if not callable(value) and not key.startswith('_')])
                if classkey is not None and hasattr(obj, "__class__"):
                    data[classkey] = obj.__class__.__name__
                return data
            else:
                return 'class:'+obj.__class__.__name__
        else:
            return obj
0

previous answers not work when class field is class instance. use this:

from dataclasses import dataclass, field

@dataclass
class BaseNumber:
    number:str = ''
    probability:float = 0.

@dataclass
class ContainerInfo:
    type:str = ''
    height:int = ''
    width:str = ''
    length:str = ''

@dataclass
class AdditionalNumber:
    number:str = ''
    prob:float = 0.
    info:ContainerInfo = ContainerInfo()

@dataclass  
class ContainerData:
    container_number = BaseNumber()
    container_type = AdditionalNumber()
    errors:list = field(default_factory=list)

    def todict(self, obj='sadasdas'):
        if obj == 'sadasdas':
            obj = self
            
        if isinstance(obj, dict):
            data = {}
            for (k, v) in obj.items():
                data[k] = self.todict(v)
            return data
        elif hasattr(obj, "_ast"):
            return self.todict(obj._ast())
        elif hasattr(obj, "__iter__") and not isinstance(obj, str):
            return [self.todict(v) for v in obj]
        elif hasattr(obj, "__dict__"):
            aaa = dir(obj)
            data = dict([(key, self.todict(value)) 
                for key, value in {field: getattr(obj, field) for field in dir(obj)}.items()
                if not callable(value) and not key.startswith('_')
            ])
            return data
        else:
            return obj
sashabal
  • 1
  • 1