How to Convert a NamedTuple to a Dictionary in Python
NamedTuples provide memory-efficient, readable data structures with named field access, but JSON serialization, APIs, and many libraries require standard dictionaries. Converting between the two is straightforward using the built-in _asdict() method.
In this guide, you will learn how to convert NamedTuples to dictionaries, handle nested structures, serialize to JSON, and perform selective field conversion.
Using _asdict() (Official Method)
Every NamedTuple instance has a built-in _asdict() method that returns a standard dictionary:
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(10, 20)
data = p._asdict()
print(data)
print(type(data))
Output:
{'x': 10, 'y': 20}
<class 'dict'>
Despite the underscore, _asdict() is part of the public API and is intended for regular use. The underscore prefix is a convention to prevent naming conflicts with user-defined fields. If you had a field called asdict, the method would still be accessible as _asdict().
Class-Based NamedTuple (Modern Python)
With typing.NamedTuple (Python 3.6+), which supports type annotations, the same _asdict() method works identically:
from typing import NamedTuple
class User(NamedTuple):
id: int
name: str
email: str
user = User(1, "Alice", "alice@example.com")
print(user._asdict())
Output:
{'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}
With Default Values
from typing import NamedTuple
class Config(NamedTuple):
host: str = "localhost"
port: int = 8080
debug: bool = False
config = Config(host="192.168.1.1")
print(config._asdict())
Output:
{'host': '192.168.1.1', 'port': 8080, 'debug': False}
Default values are preserved in the dictionary output just as they are in the NamedTuple instance.
Converting a List of NamedTuples
A common scenario is serializing collections of NamedTuples, such as database query results:
import json
from collections import namedtuple
User = namedtuple("User", ["id", "name", "email"])
users = [
User(1, "Alice", "alice@example.com"),
User(2, "Bob", "bob@example.com"),
User(3, "Charlie", "charlie@example.com")
]
user_dicts = [u._asdict() for u in users]
json_str = json.dumps(user_dicts, indent=2)
print(json_str)
Output:
[
{
"id": 1,
"name": "Alice",
"email": "alice@example.com"
},
{
"id": 2,
"name": "Bob",
"email": "bob@example.com"
},
{
"id": 3,
"name": "Charlie",
"email": "charlie@example.com"
}
]
Selective Field Conversion
Filtering Out Sensitive Fields
from typing import NamedTuple
class Employee(NamedTuple):
id: int
name: str
salary: float
ssn: str # Sensitive field
emp = Employee(1, "Alice", 75000.0, "123-45-6789")
# Exclude sensitive fields
public_data = {k: v for k, v in emp._asdict().items() if k != "ssn"}
print(public_data)
Output:
{'id': 1, 'name': 'Alice', 'salary': 75000.0}
Renaming Fields During Conversion
When internal field names do not match the expected API output:
from typing import NamedTuple
class DBRecord(NamedTuple):
user_id: int
user_name: str
record = DBRecord(1, "Alice")
# Map internal names to API-friendly names
field_map = {"user_id": "id", "user_name": "name"}
api_data = {field_map.get(k, k): v for k, v in record._asdict().items()}
print(api_data)
Output:
{'id': 1, 'name': 'Alice'}
The field_map.get(k, k) call uses the mapped name if one exists, otherwise falls back to the original name.
Handling Nested NamedTuples
When NamedTuples contain other NamedTuples as fields, _asdict() only converts the top level. A recursive function handles the full depth:
from typing import NamedTuple
class Address(NamedTuple):
city: str
zip_code: str
class Person(NamedTuple):
name: str
address: Address
def namedtuple_to_dict(obj):
"""Recursively convert NamedTuples to dictionaries."""
if hasattr(obj, '_asdict'):
return {k: namedtuple_to_dict(v) for k, v in obj._asdict().items()}
elif isinstance(obj, list):
return [namedtuple_to_dict(item) for item in obj]
else:
return obj
person = Person("Alice", Address("New York", "10001"))
# Without recursion: address remains a NamedTuple
print(person._asdict())
# With recursion: fully converted
print(namedtuple_to_dict(person))
Output:
{'name': 'Alice', 'address': Address(city='New York', zip_code='10001')}
{'name': 'Alice', 'address': {'city': 'New York', 'zip_code': '10001'}}
JSON Serialization with a Custom Encoder
For seamless JSON serialization without manually calling _asdict() each time, create a custom encoder:
import json
from typing import NamedTuple
class Product(NamedTuple):
id: int
name: str
price: float
def convert_namedtuples(obj):
if hasattr(obj, '_asdict'):
return obj._asdict()
elif isinstance(obj, list):
return [convert_namedtuples(item) for item in obj]
elif isinstance(obj, dict):
return {key: convert_namedtuples(value) for key, value in obj.items()}
return obj
class NamedTupleEncoder(json.JSONEncoder):
def encode(self, obj):
obj = convert_namedtuples(obj)
return super().encode(obj)
def default(self, obj):
if hasattr(obj, '_asdict'):
return obj._asdict()
return super().default(obj)
product = Product(1, "Widget", 29.99)
# Serialize a single NamedTuple
print(json.dumps(product, cls=NamedTupleEncoder))
# Works with lists too
products = [Product(1, "Widget", 10.0), Product(2, "Gadget", 20.0)]
print(json.dumps(products, cls=NamedTupleEncoder, indent=2))
Output:
{"id": 1, "name": "Widget", "price": 29.99}
[
{
"id": 1,
"name": "Widget",
"price": 10.0
},
{
"id": 2,
"name": "Gadget",
"price": 20.0
}
]
Alternative: Using zip() with _fields
An alternative approach pairs the _fields attribute with the tuple values using zip():
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(10, 20)
# Using zip with _fields
data = dict(zip(p._fields, p))
print(data)
# _asdict() is cleaner and more explicit
data = p._asdict()
print(data)
Output:
{'x': 10, 'y': 20}
{'x': 10, 'y': 20}
Both produce the same result, but _asdict() is more readable and self-documenting.
Reverse Operation: Dictionary to NamedTuple
To convert a dictionary back to a NamedTuple, use the ** unpacking operator:
from typing import NamedTuple
class User(NamedTuple):
id: int
name: str
# Straightforward conversion
data = {"id": 1, "name": "Alice"}
user = User(**data)
print(user)
# Handling dictionaries with extra keys
data_extra = {"id": 1, "name": "Alice", "extra": "ignored"}
user = User(**{k: v for k, v in data_extra.items() if k in User._fields})
print(user)
Output:
User(id=1, name='Alice')
User(id=1, name='Alice')
The filtering step is important because passing unexpected keyword arguments to a NamedTuple constructor raises a TypeError.
Practical Example: API Response Builder
A complete example combining nested NamedTuples, recursive conversion, and JSON serialization:
from typing import NamedTuple, List
import json
class OrderItem(NamedTuple):
product_id: int
quantity: int
price: float
class Order(NamedTuple):
order_id: int
customer: str
items: List[OrderItem]
def to_api_response(obj):
"""Convert nested NamedTuples to an API-friendly dictionary."""
if hasattr(obj, '_asdict'):
return {k: to_api_response(v) for k, v in obj._asdict().items()}
elif isinstance(obj, list):
return [to_api_response(item) for item in obj]
else:
return obj
order = Order(
order_id=1001,
customer="Alice",
items=[
OrderItem(1, 2, 29.99),
OrderItem(2, 1, 49.99)
]
)
response = to_api_response(order)
print(json.dumps(response, indent=2))
Output:
{
"order_id": 1001,
"customer": "Alice",
"items": [
{
"product_id": 1,
"quantity": 2,
"price": 29.99
},
{
"product_id": 2,
"quantity": 1,
"price": 49.99
}
]
}
Performance Comparison
import timeit
from collections import namedtuple
Point = namedtuple("Point", ["x", "y", "z"])
p = Point(1, 2, 3)
def with_asdict():
return p._asdict()
def with_manual():
return {"x": p.x, "y": p.y, "z": p.z}
def with_zip():
return dict(zip(p._fields, p))
print(f"_asdict(): {timeit.timeit(with_asdict, number=100000):.4f}s")
print(f"Manual: {timeit.timeit(with_manual, number=100000):.4f}s")
print(f"zip(): {timeit.timeit(with_zip, number=100000):.4f}s")
Typical output:
_asdict(): 0.0234s
Manual: 0.0156s
zip(): 0.0345s
Manual construction is marginally faster for simple cases, but _asdict() is preferred because it automatically adapts when fields are added or removed from the NamedTuple definition.
Quick Reference
| Scenario | Method |
|---|---|
| Single NamedTuple | obj._asdict() |
| List of NamedTuples | [x._asdict() for x in items] |
| Nested structures | Recursive conversion function |
| JSON serialization | Custom encoder or pre-convert with _asdict() |
| Selective fields | Dict comprehension with filtering |
| Field renaming | Dict comprehension with a field mapping |
| Dict to NamedTuple | MyTuple(**data) |
Conclusion
Converting NamedTuples to dictionaries in Python is straightforward thanks to the built-in _asdict() method. For flat NamedTuples, a single _asdict() call is all you need. For collections, a list comprehension like [x._asdict() for x in items] handles the conversion cleanly. For nested NamedTuples, a small recursive helper function ensures every level is fully converted.
Always use _asdict() instead of manually constructing dictionaries from NamedTuple fields. It is less error-prone and automatically includes all fields even if the NamedTuple definition changes later. For nested NamedTuples, create a reusable recursive conversion function that you can apply consistently across your codebase.