Modern Python Types
NamedTuple Introduction
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
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)}")
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)
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)
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}")
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}")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.5print(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.00dx ← -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.5print(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)
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}")
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)except AttributeError as e:
16 config.port = 900017except AttributeError as e:18 print(f"Error: {ecan't set attribute}")outputError: can't set attributenew_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}")
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