Skip to main content

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'>
The Underscore Prefix

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}
note

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

ScenarioMethod
Single NamedTupleobj._asdict()
List of NamedTuples[x._asdict() for x in items]
Nested structuresRecursive conversion function
JSON serializationCustom encoder or pre-convert with _asdict()
Selective fieldsDict comprehension with filtering
Field renamingDict comprehension with a field mapping
Dict to NamedTupleMyTuple(**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.

Best Practice

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.