When you need lightweight, immutable data structures that are more readable than plain tuples, NamedTuples provide named field access while retaining tuple efficiency. They are memory-efficient, hashable, and work seamlessly with tuple unpacking.

Why Use NamedTuples?

  • Immutable (safe for use as dict keys)
  • Memory efficient (lighter than classes)
  • Access by name or index
  • Tuple unpacking support
  • Type hints integration

Basic NamedTuple

p1
basic.py
Replay: real traced execution (multi-file project)
# Basic namedtuple usage

from collections import namedtuple

# Create a namedtuple type
Point = namedtuple('Point', ['x', 'y'])

# Create instances
p1 = Point(10, 20)
p2 = Point(x=30, y=40)

# Access by name
print(f"p1: x={p1.x}, y={p1.y}")

# Access by index (still a tuple)
print(f"p1[0]={p1[0]}, p1[1]={p1[1]}")

# Tuple operations
print(f"Length: {len(p1)}")
print(f"As tuple: {tuple(p1)}")
# Basic namedtuple usage

from collections import namedtuple

# Create a namedtuple type
Point = namedtuple('Point', ['x', 'y'])

# Create instances
p1 = Point(5, 15)
p2 = Point(x=30, y=40)

# Access by name
print(f"p1: x={p1.x}, y={p1.y}")

# Access by index (still a tuple)
print(f"p1[0]={p1[0]}, p1[1]={p1[1]}")

# Tuple operations
print(f"Length: {len(p1)}")
print(f"As tuple: {tuple(p1)}")
# Basic namedtuple usage

from collections import namedtuple

# Create a namedtuple type
Point = namedtuple('Point', ['x', 'y'])

# Create instances
p1 = Point(-3, 8)
p2 = Point(x=30, y=40)

# Access by name
print(f"p1: x={p1.x}, y={p1.y}")

# Access by index (still a tuple)
print(f"p1[0]={p1[0]}, p1[1]={p1[1]}")

# Tuple operations
print(f"Length: {len(p1)}")
print(f"As tuple: {tuple(p1)}")
  1. Point ← <class '__main__.Point'>, p1 ← Point(x=10, y=20), p2 ← Point(x=30, y=40)

    5# Create a namedtuple type6Point→ <class '__main__.Point'> = namedtuple('Point', ['x', 'y'])78# Create instances9p1→ Point(x=10, y=20) = Point(10, 20)10#@p1=Point(5, 15), Point(-3, 8)11p2→ Point(x=30, y=40) = Point(x=30, y=40)1213# Access by name14print(f"p1: x={p1.x10}, y={p1.y20}")1516# Access by index (still a tuple)17print(f"p1[0]={p1[0]10}, p1[1]={p1[1]20}")1819# Tuple operations20print(f"Length: {len(p1Point(x=10, y=20))}")21print(f"As tuple: {tuple(p1Point(x=10, y=20))}")
    outputp1: x=10, y=20
    p1[0]=10, p1[1]=20
    Length: 2
    As tuple: (10, 20)
  1. Point ← <class '__main__.Point'>, p1 ← Point(x=5, y=15), p2 ← Point(x=30, y=40)

    5# Create a namedtuple type6Point→ <class '__main__.Point'> = namedtuple('Point', ['x', 'y'])78# Create instances9p1→ Point(x=5, y=15) = Point(5, 15)10p2→ Point(x=30, y=40) = Point(x=30, y=40)1112# Access by name13print(f"p1: x={p1.x5}, y={p1.y15}")1415# Access by index (still a tuple)16print(f"p1[0]={p1[0]5}, p1[1]={p1[1]15}")1718# Tuple operations19print(f"Length: {len(p1Point(x=5, y=15))}")20print(f"As tuple: {tuple(p1Point(x=5, y=15))}")
    outputp1: x=5, y=15
    p1[0]=5, p1[1]=15
    Length: 2
    As tuple: (5, 15)
  1. Point ← <class '__main__.Point'>, p1 ← Point(x=-3, y=8), p2 ← Point(x=30, y=40)

    5# Create a namedtuple type6Point→ <class '__main__.Point'> = namedtuple('Point', ['x', 'y'])78# Create instances9p1→ Point(x=-3, y=8) = Point(-3, 8)10p2→ Point(x=30, y=40) = Point(x=30, y=40)1112# Access by name13print(f"p1: x={p1.x-3}, y={p1.y8}")1415# Access by index (still a tuple)16print(f"p1[0]={p1[0]-3}, p1[1]={p1[1]8}")1718# Tuple operations19print(f"Length: {len(p1Point(x=-3, y=8))}")20print(f"As tuple: {tuple(p1Point(x=-3, y=8))}")
    outputp1: x=-3, y=8
    p1[0]=-3, p1[1]=8
    Length: 2
    As tuple: (-3, 8)
namedtuple A lightweight, immutable data structure that combines tuple efficiency with named field access, created using collections.namedtuple or typing.NamedTuple.

Typed NamedTuple with Methods

The typing.NamedTuple class syntax allows type hints and custom methods.

typed_with_methods.py
Replay: real traced execution (multi-file project)
# Typed NamedTuple with methods

from typing import NamedTuple

class Point(NamedTuple):
    x: float
    y: float

    def distance_from_origin(self) -> float:
        return (self.x ** 2 + self.y ** 2) ** 0.5

    def distance_to(self, other: 'Point') -> float:
        dx = self.x - other.x
        dy = self.y - other.y
        return (dx ** 2 + dy ** 2) ** 0.5

# Create points
p1 = Point(3.0, 4.0)
p2 = Point(6.0, 8.0)

print(f"p1 distance from origin: {p1.distance_from_origin():.2f}")
print(f"Distance from p1 to p2: {p1.distance_to(p2):.2f}")
  1. p1 ← Point(x=3.0, y=4.0), p2 ← Point(x=6.0, y=8.0)

    5class Point(NamedTuple):6    x(empty): float7    y(empty): float8    9    def distance_from_origin(self) -> float:10        return (self.x ** 2 + self.y ** 2) ** 0.511    12    def distance_to(self, other: 'Point') -> float:13        dx = self.x - other.x14        dy = self.y - other.y15        return (dx ** 2 + dy ** 2) ** 0.51617# Create points18p1→ Point(x=3.0, y=4.0) = Point(3.0, 4.0)19p2→ Point(x=6.0, y=8.0) = Point(6.0, 8.0)2021print(f"p1 distance from origin: {p1Point(x=3.0, y=4.0).distance_from_origin():.2f}")22print(f"Distance from p1 to p2: {p1.distance_to(p2):.2f}")
  2. def distance_from_origin(self) -> float:

    9def distance_from_origin(selfPoint(x=3.0, y=4.0)) -> float:10    return (self.x3.0 ** 2 + self.y4.0 ** 2) ** 0.5
  3. print(f"p1 distance from origin: {p1.distance_from_origin():.2f}")

    21print(f"p1 distance from origin: {p1Point(x=3.0, y=4.0).distance_from_origin():.2f}")22print(f"Distance from p1 to p2: {p1Point(x=3.0, y=4.0).distance_to(p2Point(x=6.0, y=8.0)):.2f}")
    outputp1 distance from origin: 5.00
  4. dx ← -3.0, dy ← -4.0

    12def distance_to(selfPoint(x=3.0, y=4.0), otherPoint(x=6.0, y=8.0): 'Point') -> float:13    dx→ -3.0 = self.x3.0 - other.x6.014    dy→ -4.0 = self.y4.0 - other.y8.015    return (dx-3.0 ** 2 + dy-4.0 ** 2) ** 0.5
  5. print(f"Distance from p1 to p2: {p1.distance_to(p2):.2f}")

    21print(f"p1 distance from origin: {p1.distance_from_origin():.2f}")22print(f"Distance from p1 to p2: {p1Point(x=3.0, y=4.0).distance_to(p2Point(x=6.0, y=8.0)):.2f}")
    outputDistance from p1 to p2: 5.00

Default Values

NamedTuples support default values for fields.

default_values.py
Replay: real traced execution (multi-file project)
# NamedTuple with default values

from typing import NamedTuple

class Person(NamedTuple):
    name: str
    age: int
    country: str = "USA"
    active: bool = True

# Use defaults
person1 = Person("Alice", 30)
print(person1)

# Override defaults
person2 = Person("Bob", 25, "Canada", False)
print(person2)

# Partial override
person3 = Person("Charlie", 35, country="UK")
print(person3)
  1. country ← (empty), active ← (empty), person1 ← Person(name='Alice', age=30, country='USA', active=True)

    5class Person(NamedTuple):6    name(empty): str7    age(empty): int8    country→ (empty): str = "USA"9    active→ (empty): bool = TrueTrue1011# Use defaults12person1→ Person(name='Alice', age=30, country='USA', active=True) = Person("Alice", 30)13print(person1Person(name='Alice', age=30, country='USA', active=True))1415# Override defaults16person2→ Person(name='Bob', age=25, country='Canada', active=False) = Person("Bob", 25, "Canada", False)17print(person2Person(name='Bob', age=25, country='Canada', active=False))1819# Partial override20person3→ Person(name='Charlie', age=35, country='UK', active=True) = Person("Charlie", 35, country="UK")21print(person3Person(name='Charlie', age=35, country='UK', active=True))
    outputPerson(name='Alice', age=30, country='USA', active=True)
    Person(name='Bob', age=25, country='Canada', active=False)
    Person(name='Charlie', age=35, country='UK', active=True)

Immutability and _replace()

NamedTuples are immutable, but _replace() creates modified copies.

immutable_replace.py
Replay: real traced execution (multi-file project)
# Immutability and _replace()

from typing import NamedTuple

class Config(NamedTuple):
    host: str
    port: int
    debug: bool

# Create config
config = Config("localhost", 8080, False)
print(f"Original: {config}")

# Cannot modify (immutable)
try:
    config.port = 9000
except AttributeError as e:
    print(f"Error: {e}")

# Use _replace() to create modified copy
new_config = config._replace(debug=True)
print(f"Modified: {new_config}")
print(f"Original unchanged: {config}")

# Multiple changes
prod_config = config._replace(host="0.0.0.0", port=80)
print(f"Production: {prod_config}")
  1. config ← Config(host='localhost', port=8080, debug=False)

    5class Config(NamedTuple):6    host(empty): str7    port(empty): int8    debug(empty): bool910# Create config11config→ Config(host='localhost', port=8080, debug=False) = Config("localhost", 8080, False)12print(f"Original: {configConfig(host='localhost', port=8080, debug=False)}")
    outputOriginal: Config(host='localhost', port=8080, debug=False)
  2. except AttributeError as e:

    16    config.port = 900017except AttributeError as e:18    print(f"Error: {ecan't set attribute}")
    outputError: can't set attribute
  3. new_config ← Config(host='localhost', port=8080, debug=True), prod_config ← Config(host='0.0.0.0', port=80, debug=False)

    20# Use _replace() to create modified copy21new_config→ Config(host='localhost', port=8080, debug=True) = configConfig(host='localhost', port=8080, debug=False)._replace(debug=True)22print(f"Modified: {new_configConfig(host='localhost', port=8080, debug=True)}")23print(f"Original unchanged: {configConfig(host='localhost', port=8080, debug=False)}")2425# Multiple changes26prod_config→ Config(host='0.0.0.0', port=80, debug=False) = configConfig(host='localhost', port=8080, debug=False)._replace(host="0.0.0.0", port=80)27print(f"Production: {prod_configConfig(host='0.0.0.0', port=80, debug=False)}")
    outputModified: Config(host='localhost', port=8080, debug=True)
    Original unchanged: Config(host='localhost', port=8080, debug=False)
    Production: Config(host='0.0.0.0', port=80, debug=False)
_replace() A method that creates a new NamedTuple instance with specified fields replaced, since NamedTuples are immutable and cannot be modified in place.

Tuple Unpacking and _fields

NamedTuples support unpacking and provide metadata access through _fields and _asdict().

unpacking_fields.py
Replay: real traced execution (multi-file project)
# Tuple unpacking and _fields

from typing import NamedTuple

class Employee(NamedTuple):
    name: str
    id: int
    department: str
    salary: float

employee = Employee("Alice Johnson", 1001, "Engineering", 95000)

# Tuple unpacking
name, emp_id, dept, salary = employee
print(f"{name} (ID: {emp_id}) works in {dept}")

# Access _fields attribute
print(f"\nFields: {employee._fields}")

# Convert to dict using _asdict()
emp_dict = employee._asdict()
print(f"\nAs dict: {emp_dict}")

# Create from dict
data = {"name": "Bob Smith", "id": 1002, "department": "Sales", "salary": 85000}
new_employee = Employee(**data)
print(f"\nFrom dict: {new_employee}")
  1. employee ← Employee(name='Alice Johnson', id=1001, department='Engineering', salary=95000)

    5class Employee(NamedTuple):6    name(empty): str7    id<built-in function id>: int8    department(empty): str9    salary(empty): float1011employee→ Employee(name='Alice Johnson', id=1001, department='Engineering', salary=95000) = Employee("Alice Johnson", 1001, "Engineering", 95000)1213# Tuple unpacking14name→ Alice Johnson, emp_id→ 1001, dept→ Engineering, salary→ 95000 = employeeEmployee(name='Alice Johnson', id=1001, department='Engineering', salary=95000)15print(f"{nameAlice Johnson} (ID: {emp_id1001}) works in {deptEngineering}")1617# Access _fields attribute18print(f"\nFields: {employee._fields('name', 'id', 'department', 'salary')}")1920# Convert to dict using _asdict()21emp_dict→ {'name': 'Alice Johnson', 'id': 1001, 'department': 'Engineering', 'salary': 95000} = employeeEmployee(name='Alice Johnson', id=1001, department='Engineering', salary=95000)._asdict()22print(f"\nAs dict: {emp_dict{'name': 'Alice Johnson', 'id': 1001, 'department': 'Engineering', 'salary': 95000}}")2324# Create from dict25data→ {'name': 'Bob Smith', 'id': 1002, 'department': 'Sales', 'salary': 85000} = {"name": "Bob Smith", "id": 1002, "department": "Sales", "salary": 85000}26new_employee→ Employee(name='Bob Smith', id=1002, department='Sales', salary=85000) = Employee(**data{'name': 'Bob Smith', 'id': 1002, 'department': 'Sales', 'salary': 85000})27print(f"\nFrom dict: {new_employeeEmployee(name='Bob Smith', id=1002, department='Sales', salary=85000)}")
    outputAlice Johnson (ID: 1001) works in Engineering
    
    Fields: ('name', 'id', 'department', 'salary')
    
    As dict: {'name': 'Alice Johnson', 'id': 1001, 'department': 'Engineering', 'salary': 95000}
    
    From dict: Employee(name='Bob Smith', id=1002, department='Sales', salary=85000)

@seealso dataclass_intro "Dataclasses for mutable data" @seealso typing_intro "Type hints"

Exercise: practical.py

Create a Color NamedTuple with RGB values, hex conversion, and brightness calculation methods