If you’ve ever returned structured data from a function or tried to group related values cleanly, you’ve probably run into both namedtuple and dataclass. They look similar on the surface; both let you create objects with named fields, but they’re built for different jobs.
I get this question a lot in Python training sessions: “When do I use one over the other?” In this guide, I’m going to answer that clearly, with real examples, so you can make the right call in your own code.
A Quick Look at Both
Before comparing them, let’s see what each one looks like in practice.
namedtuple (from the collections module — available since Python 2.6):
from collections import namedtuple
Employee = namedtuple('Employee', ['name', 'department', 'salary'])
emp = Employee(name="Sarah Johnson", department="Engineering", salary=95000)
print(emp.name) # Sarah Johnson
print(emp.department) # Engineering
dataclass (from the dataclasses module — available since Python 3.7):
from dataclasses import dataclass
@dataclass
class Employee:
name: str
department: str
salary: float
emp = Employee(name="Sarah Johnson", department="Engineering", salary=95000)
print(emp.name) # Sarah Johnson
print(emp.department) # Engineering
Both give you named field access. Both auto-generate a readable __repr__. But that’s where the similarities start to narrow.
The Core Difference in One Sentence
A namedtuple is a lightweight, immutable tuple with named fields. A dataclass is a fully-featured, mutable class with named fields — and a lot more flexibility.
namedtuple: What It’s Good At
A namedtuple is still a tuple under the hood. That means:
- It’s immutable — you can’t change values after creation
- It supports tuple unpacking and indexing
- It’s slightly faster and lighter in memory than a class
- It works great as a dictionary key (since it’s hashable)
from collections import namedtuple
# Great for returning structured data from a function
Coordinates = namedtuple('Coordinates', ['latitude', 'longitude'])
def get_office_location(city):
locations = {
"New York": Coordinates(40.7128, -74.0060),
"Austin": Coordinates(30.2672, -97.7431),
"Seattle": Coordinates(47.6062, -122.3321),
}
return locations.get(city)
nyc = get_office_location("New York")
print(nyc.latitude) # 40.7128
print(nyc.longitude) # -74.006
# It's still a tuple — you can unpack it
lat, lon = nyc
print(lat, lon) # 40.7128 -74.006
# You can use it as a dictionary key
office_ratings = {nyc: 4.5}
Because it’s immutable, a namedtuple makes a strong promise: “these values don’t change.” That’s exactly what you want for coordinates, configuration constants, or any function return value that the caller shouldn’t be able to mess with.
dataclass: What It’s Good At
A dataclass is essentially a regular Python class with a lot of boilerplate auto-generated for you — __init__, __repr__, and __eq__ are written automatically based on your field definitions.
But unlike namedtuple, a dataclass is mutable by default, supports default values, type hints, custom methods, and inheritance — all the things you’d expect from a real class.
from dataclasses import dataclass, field
from typing import List
@dataclass
class ShoppingCart:
customer_name: str
state: str = "TX"
items: List[str] = field(default_factory=list)
discount: float = 0.0
def add_item(self, item: str):
self.items.append(item)
def item_count(self):
return len(self.items)
cart = ShoppingCart(customer_name="Marcus Thompson")
cart.add_item("Wireless Keyboard")
cart.add_item("Monitor Stand")
cart.state = "CA" # You can change values after creation
print(cart)
# ShoppingCart(customer_name='Marcus Thompson', state='CA',
# items=['Wireless Keyboard', 'Monitor Stand'], discount=0.0)
print(cart.item_count()) # 2
Try doing any of that with a namedtuple — you can’t. No methods, no defaults on mutable fields (like lists), no changing values after creation.
Side-by-Side Comparison
| Feature | namedtuple | dataclass |
|---|---|---|
| Mutable (can change values) | ❌ No | ✅ Yes (by default) |
| Immutable option | ✅ Always | ✅ @dataclass(frozen=True) |
| Default field values | ⚠️ Limited | ✅ Full support |
| Default mutable fields (lists, dicts) | ❌ No | ✅ field(default_factory=...) |
| Custom methods | ❌ No | ✅ Yes |
| Inheritance | ⚠️ Awkward | ✅ Full class inheritance |
| Tuple unpacking | ✅ Yes | ❌ No |
| Works as dict key (hashable) | ✅ Yes | ✅ Only if frozen=True |
| Memory usage | ✅ Lighter | Slightly heavier |
| Python version required | 2.6+ | 3.7+ |
| Type hints | ⚠️ Optional | ✅ Built-in |
__slots__ support | ✅ Built-in | ✅ Python 3.10+ |
Make a namedtuple Immutable vs. a Frozen dataclass
One thing worth knowing: you can make a dataclass behave like a namedtuple by setting frozen=True. This prevents any field from being changed after creation and makes the object hashable.
from dataclasses import dataclass
@dataclass(frozen=True)
class GeoPoint:
latitude: float
longitude: float
label: str = "Unknown"
point = GeoPoint(latitude=37.7749, longitude=-122.4194, label="San Francisco")
print(point.label) # San Francisco
# Try to change it:
point.latitude = 40.0 # ❌ FrozenInstanceError: cannot assign to field 'latitude'
A frozen dataclass and a namedtuple are now close in behavior, but there’s still a key difference: a frozen dataclass is not a tuple. You can’t unpack it or use index access like point[0].
Real-World Scenarios: Which One to Pick
Let me walk through some concrete situations you’ll actually run into.
Scenario 1: Return structured data from a function
You have a function that calculates sales metrics and returns total, average, and highest sale. This value is read-only — the caller just needs to read it.
Use namedtuple. It’s lightweight, immutable, and perfect for this:
from collections import namedtuple
SalesMetrics = namedtuple('SalesMetrics', ['total', 'average', 'highest'])
def calculate_metrics(sales: list) -> SalesMetrics:
total = sum(sales)
average = round(total / len(sales), 2)
highest = max(sales)
return SalesMetrics(total=total, average=average, highest=highest)
q1_sales = [12400, 15600, 9800, 14200, 11000]
metrics = calculate_metrics(q1_sales)
print(f"Total: ${metrics.total:,}") # Total: $63,000
print(f"Average: ${metrics.average:,}") # Average: $12,600.0
print(f"Highest: ${metrics.highest:,}") # Highest: $15,600
You can see the output in the screenshot below.

Scenario 2: Model a user profile in a web app
You have a User object that starts with basic info but gets updated (email changes, subscription status updates, etc.).
Use dataclass. You need mutability and probably some methods:
from dataclasses import dataclass, field
from typing import List
@dataclass
class UserProfile:
user_id: int
full_name: str
email: str
state: str = "NY"
is_premium: bool = False
purchase_history: List[str] = field(default_factory=list)
def upgrade_to_premium(self):
self.is_premium = True
def add_purchase(self, item: str):
self.purchase_history.append(item)
def display_name(self):
tier = "Premium" if self.is_premium else "Free"
return f"{self.full_name} ({tier})"
user = UserProfile(user_id=1042, full_name="Jordan Williams", email="jordan@email.com")
user.upgrade_to_premium()
user.add_purchase("Annual Plan")
print(user.display_name()) # Jordan Williams (Premium)
print(user.purchase_history) # ['Annual Plan']
You can see the output in the screenshot below.

Scenario 3: Store app configuration constants
You have database config or API settings that should never change at runtime.
Use a frozen dataclass or namedtuple. Both work, I slightly prefer frozen dataclass here because of type hints and the option to add validation:
from dataclasses import dataclass
@dataclass(frozen=True)
class DatabaseConfig:
host: str
port: int
db_name: str
ssl: bool = True
db = DatabaseConfig(host="db.myapp.com", port=5432, db_name="prod_db")
print(db.host) # db.myapp.com
# db.port = 3306 # ❌ FrozenInstanceError — config is locked
You can see the output in the screenshot below.

Scenario 4: Work with large datasets in memory
You’re processing 500,000 records and storing them as objects. Memory matters.
Use namedtuple — it’s lighter than a dataclass. If you need even more efficiency, look into __slots__ with dataclass in Python 3.10+.
pythonfrom collections import namedtuple
import sys
# namedtuple
Transaction = namedtuple('Transaction', ['tx_id', 'amount', 'merchant'])
t1 = Transaction(tx_id="TX001", amount=49.99, merchant="Target")
# Compare memory size
from dataclasses import dataclass
@dataclass
class TransactionDC:
tx_id: str
amount: float
merchant: str
t2 = TransactionDC(tx_id="TX001", amount=49.99, merchant="Target")
print(sys.getsizeof(t1)) # ~72 bytes (tuple-based)
print(sys.getsizeof(t2)) # ~48 bytes (but __dict__ adds overhead elsewhere)
For truly large-scale data, also consider __slots__ or libraries like attrs.
When namedtuple Falls Short
I like namedtuple a lot, but there are real limitations:
- No default values for mutable fields. You can’t do tags: list = [] safely in a namedtuple.
- No custom methods. If you want cart.add_item(), you need a class — namedtuple can’t do it.
- Inheritance is awkward. Technically possible but messy. dataclass handles it much more naturally.
- Adding fields later is painful. Every field is positional — adding one in the middle breaks existing code.
When dataclass Falls Short
dataclass isn’t perfect either:
- Not a tuple. You lose tuple unpacking and can’t use it as a dictionary key unless you freeze it.
- Requires Python 3.7+. Not a big deal today, but worth noting if you maintain legacy code.
- Slightly more verbose. For simple read-only return values, namedtuple is just cleaner to write.
The typing.NamedTuple Syntax (Modern namedtuple)
If you like the dataclass class-style syntax but still want a namedtuple, Python gives you a better way to define them using typing.NamedTuple. This is the modern approach I recommend over the old collections.namedtuple factory syntax:
from typing import NamedTuple
class FlightInfo(NamedTuple):
flight_number: str
origin: str
destination: str
duration_hours: float
on_time: bool = True # Default values are supported here
flight = FlightInfo(
flight_number="AA2401",
origin="Dallas",
destination="Chicago",
duration_hours=2.5
)
print(flight.flight_number) # AA2401
print(flight.on_time) # True
# Still a tuple
origin, destination = flight.origin, flight.destination
lat, lon, dur, status = flight[1], flight[2], flight[3], flight[4]
This gives you type hints, default values, and clean class syntax — while still being a true tuple underneath.
Quick Decision Guide
Use this when you’re not sure which to reach for:
- namedtuple (or typing.NamedTuple) — read-only data, function return values, dictionary keys, large in-memory collections
- dataclass — mutable objects, objects with methods, app models, anything that evolves after creation
- frozen dataclass — immutable config or settings objects where you want type hints and validation but not tuple behavior
- dataclass with __slots__ (Python 3.10+) — memory-critical mutable objects
Frequently Asked Questions
Is namedtuple faster than dataclass?
Generally yes, because a namedtuple is backed by a C-level tuple. The difference is small for most use cases, but it’s noticeable when creating millions of instances.
Can I convert a namedtuple to a dictionary?
Yes — call ._asdict() on any namedtuple instance. It returns an OrderedDict (or a regular dict in Python 3.8+).
pythonemp = Employee(name=”Alex Reed”, department=”Marketing”, salary=72000)
print(emp._asdict())
# {‘name’: ‘Alex Reed’, ‘department’: ‘Marketing’, ‘salary’: 72000}
Can I use dataclass with inheritance?
Yes, and it works much more cleanly than namedtuple. You can have a base dataclass and extend it with child classes, inheriting all fields and adding new ones.
Which one should I use in a public API?
If you’re building a library that others will import, dataclass is generally safer — it’s more flexible, easier to version, and doesn’t carry the “positional index” fragility of a tuple.
What about attrs?
attrs is a third-party library that predates dataclass and still offers more features (validators, converters, etc.). If you’re already using it or need advanced validation, it’s excellent. For most projects, built-in dataclass is sufficient.
Wrapping Up
Here’s the short version:
- Reach for namedtuple (or typing.NamedTuple) when you want a lightweight, immutable, tuple-compatible container — especially for function return values.
- Reach for dataclass when you need a mutable object with methods, default values, or anything that behaves like a real class.
- Use frozen=True on a dataclass when you want immutability but don’t need tuple behavior.
Neither is universally better. They’re tools for different jobs, and knowing which one fits the situation is what separates clean Python from cluttered Python.
You may also read:
- Convert a Tuple to JSON in Python
- Convert Tuple to Dict in Python
- Convert Tuple to Int in Python
- Create an Empty Tuple in Python

Bijay Kumar is an experienced Python and AI professional who enjoys helping developers learn modern technologies through practical tutorials and examples. His expertise includes Python development, Machine Learning, Artificial Intelligence, automation, and data analysis using libraries like Pandas, NumPy, TensorFlow, Matplotlib, SciPy, and Scikit-Learn. At PythonGuides.com, he shares in-depth guides designed for both beginners and experienced developers. More about us.