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<