Python dataclass vs namedtuple: Which Should You Use?

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

Featurenamedtupledataclass
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✅ LighterSlightly heavier
Python version required2.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.

Python dataclass vs namedtuple

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.

Difference between Python dataclass and namedtuple

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.

Python dataclass and namedtuple

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<