Back to Blog
Python

Python Dict Conversion: Objects, JSON, and Type Casting

python dict conversion: Learn how to convert Python dicts to objects, JSON strings, and typed values, including nested structures, custom encoders, and common failure...

PythondictionariesJSONdataclassesserialization
Illustration of a Python dictionary being converted into objects, JSON strings, and typed values with arrows showing the transformation flow.

Python dict conversion is a routine operation in most Python codebases, yet the choice of conversion method changes how the result behaves. A dictionary is the default interchange format for data entering and leaving a Python program, so converting between dicts and objects, JSON strings, or typed values appears in nearly every service, script, and data pipeline.

Converting Objects to Dictionaries

The simplest way to turn an object into a dict is vars():

class User: def __init__(self, name, email): self.name = name self.email = email user = User("Ada", "ada@example.com") data = vars(user) print(data) # {'name': 'Ada', 'email': 'ada@example.com'}

vars() reads the instance's __dict__ attribute, which stores the object's writable attributes. It works for plain classes, but it fails for objects that use __slots__ because those instances have no __dict__. For dataclasses, dataclasses.asdict() is more reliable because it recursively converts nested dataclasses, lists, and tuples:

from dataclasses import dataclass, asdict @dataclass class Address: city: str zip_code: str @dataclass class User: name: str address: Address user = User("Ada", Address("London", "E1 6AN")) data = asdict(user) print(data) # {'name': 'Ada', 'address': {'city': 'London', 'zip_code': 'E1 6AN'}}

asdict() performs a deep conversion, so nested dataclass instances become nested dicts. vars() only copies the top-level attribute mapping and leaves nested objects as they are. Choose vars() when you need a shallow snapshot of a simple object, and asdict() when the object graph contains dataclasses that must be fully converted.

Converting Dictionaries Back to Objects

The reverse direction requires explicit construction. For a dataclass, you can pass the dict as keyword arguments:

user = User(**data)

This works when the dict keys exactly match the dataclass field names. If the dict contains extra keys, the constructor raises a TypeError. If it is missing keys, the constructor fails unless the field has a default. For plain classes, you need a manual mapping:

class User: def __init__(self, name, email): self.name = name self.email = email def user_from_dict(data): return User(data["name"], data["email"])

A manual constructor keeps the mapping explicit, which is often preferable when the dict comes from an external API whose field names differ from the internal attribute names. Using **data blindly couples the object's constructor signature to the external data shape, so a renamed field in the API breaks the conversion without a clear error.

JSON Serialization and Deserialization

The most common dict conversion in practice is JSON round-tripping. json.dumps() converts a dict to a JSON string, and json.loads() converts a JSON string back to a dict:

import json payload = {"user": "Ada", "roles": ["admin", "editor"]} json_str = json.dumps(payload) restored = json.loads(json_str)

The standard library json module only handles basic types: dicts, lists, strings, numbers, booleans, and None. Custom objects, datetime instances, and Decimal values raise TypeError during serialization. A common fix is a custom encoder:

from datetime import datetime import json class DateTimeEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, datetime): return obj.isoformat() return super().default(obj) json_str = json.dumps({"created": datetime.now()}, cls=DateTimeEncoder)

On the way back, json.loads() returns strings for ISO-formatted dates, so you must parse them explicitly if the application needs datetime objects again. The round trip is not symmetric unless you add a custom object_hook to json.loads():

def decode_datetime(data): if "created" in data: data["created"] = datetime.fromisoformat(data["created"]) return data restored = json.loads(json_str, object_hook=decode_datetime)

Type Conversion Within Dictionary Values

Sometimes the dict itself contains values of the wrong type. A JSON payload may deliver numeric strings, or a configuration file may store booleans as strings. Converting the dict then means transforming each value, not just restructuring the container:

raw = {"port": "8080", "debug": "true", "timeout": "30.5"} converted = { "port": int(raw["port"]), "debug": raw["debug"].lower() == "true", "timeout": float(raw["timeout"]), }

A comprehension like this works well when the key set is known in advance. For dynamic keys, a helper function that inspects the value type is more appropriate:

def convert_value(value): if isinstance(value, str): if value.isdigit(): return int(value) try: return float(value) except ValueError: return value return value converted = {key: convert_value(value) for key, value in raw.items()}

The heuristic approach is fragile because it guesses types from string content. Prefer an explicit mapping when the schema is fixed, and reserve heuristics for cases where the input format is genuinely untyped.

Nested Structures and Recursive Conversion

Shallow conversion leaves nested dicts untouched. When an API returns a deeply nested structure, converting only the top level is rarely enough. A recursive function can apply the same transformation at every level:

def deep_convert(obj, converter): if isinstance(obj, dict): return {key: deep_convert(value, converter) for key, value in obj.items()} if isinstance(obj, list): return [deep_convert(item, converter) for item in obj] return converter(obj)

This pattern is useful when you need to convert every leaf value, such as turning all ISO date strings into datetime objects. The recursion terminates because each call descends into a smaller structure, and non-container values pass through the converter directly.

Performance and Runtime Cost

Dict conversion is not free. dataclasses.asdict() performs a full recursive copy of the object graph, which allocates new dicts and lists at every level. For large object graphs, this can dominate the cost of a request handler. If you only need to serialize once for a response, the conversion is unavoidable. If you convert the same object repeatedly, caching the converted dict can avoid redundant work.

json.dumps() also has measurable cost for large payloads. The standard library implementation is pure Python, so for very large payloads, a C-accelerated serializer may reduce latency. The tradeoff is dependency weight and compatibility. For most services, the standard library is sufficient, and the conversion cost is small compared to network I/O.

Common Failure Modes

The most frequent failure in dict conversion is a key mismatch. When a dict is built from one source and consumed by another, a renamed field produces a KeyError at the point of access. Using .get() with a default masks the error but can silently pass None into downstream logic. The safer pattern is to validate required keys early:

required = {"name", "email"} missing = required - set(data.keys()) if missing: raise ValueError(f"Missing fields: {missing}")

Another failure mode is circular references. A dict that contains a reference to itself cannot be serialized with json.dumps(), which raises ValueError: Circular reference detected. The same problem affects recursive conversion functions that do not track visited objects. If circular structures are possible, the conversion function must keep a set of seen object IDs and stop descending when it encounters one already processed.

For objects with __slots__, vars() raises TypeError because the instance has no __dict__. In that case, use dataclasses.asdict() or a manual mapping that reads each slot explicitly.

python dict conversion: Practical Usage and Code Examples | RYUSLOG DEV