Plain tuples are useful, but they have one rough edge: every field is identified by position. product[0] is the name and product[2] is the stock, and the reader has to remember that mapping. Named tuples fix this by giving each position a name, so you can write product.name and product.stock while keeping every property that made tuples nice in the first place. This lesson covers both ways Python ships them, collections.namedtuple and typing.NamedTuple, and when to pick each.
A small tuple is fine to read. (29.99, 5) is obviously a price and a quantity, and you can unpack it as price, qty = line and be done. The problem shows up when the tuple grows past two or three fields, or when the same kind of record gets passed through many functions.
Nothing prints, because there are 12 in stock and the threshold is 5. But that's not the issue. The issue is that product[3] is a mystery to anyone reading this for the first time. Is it the stock? The rating? Some flag? You'd have to find where the tuple was created and count fields.
It gets worse when somebody reorders the fields. Move price to the end and every product[2] in the codebase is now wrong, silently. The code still runs, the numbers just stop meaning what they used to. There is no error and no warning.
A named tuple turns this:
Into this:
Same data, same immutability, same speed, but the field has a name. product.price reads like English, and reordering the fields no longer breaks anything because nothing relies on position.
collections.namedtuplenamedtuple is a factory function. You call it once to create a new tuple subclass, then you use that subclass to make instances.
Two things to notice. First, the call to namedtuple returns a class, which is why we capitalize the name (Product) by convention. Second, when you print an instance, Python shows the field names, not just the values. That alone makes debugging easier than with plain tuples.
The first argument to namedtuple is the type name, and it should match the variable you assign the result to. The second argument is the field names, and you have a few ways to write them.
The space-separated string form is the one you'll see most in real code. It's shorter, and there's a small payoff: when you read "street city zip_code country", the field names line up like a header row in a table.
A name has to be a valid Python identifier (letters, digits, underscores, no leading digit) and can't be a reserved keyword. You also can't reuse a name within the same tuple definition.
namedtuple has a rename=True flag for the rare cases where you're generating field names dynamically and might accidentally produce duplicates or invalid identifiers; it replaces bad names with _0, _1, etc. You won't use this in normal code, but it's good to know it exists.
Instances behave like any class constructor. You can pass arguments by position, by keyword, or mix the two.
Keyword arguments are the safer style once you have more than two or three fields. They survive field reordering, and they make the call site self-documenting.
If you pass the wrong number of arguments, you get the same error a regular function would raise:
Python tells you exactly which field you forgot. With a plain tuple, missing a field is a silent bug that surfaces somewhere far from the cause.
This is the headline feature. A named tuple acts like an object when you want attribute access, and like a tuple when you want indexing. Same instance, two views.
The relationship between these two views is what makes named tuples worth using:
The orange path (indexing) is what makes the instance a tuple. Existing code that loops over the values, unpacks them, or passes the tuple to a function expecting a sequence keeps working. The green path (attribute access) is the new ergonomics named tuples add. The teal path is unpacking, which behaves exactly like it does for any tuple of the same length.
Unpacking is worth showing concretely because it's how named tuples often interact with the rest of your code:
You also keep equality and hashing behavior. Two instances with the same field values compare equal, and named tuples are hashable (because tuples are), which means you can use them as dictionary keys or set members.
Two Coordinate instances with the same values are equal and hash the same, so the set keeps only one. This is the kind of behavior you want for value types like coordinates, IDs, or RGB colors.
A named tuple is a tuple. You can't change its fields after creation, by name or by index.
Index assignment fails the same way as it does for any tuple:
This is by design. If you want a mutable record, a named tuple is the wrong tool; reach for a dataclass or a plain class. If you want a "changed" version of a named tuple, you don't change it, you make a new one. That's what _replace is for.
defaultsReal records often have fields with sensible defaults. A product might default to stock=0 when it's first registered. namedtuple supports this with the defaults parameter, added in Python 3.7.
defaults is an iterable applied to the rightmost fields. If your tuple has four fields and you pass two defaults, they fill in the last two fields. The remaining fields are still required.
The rule about rightmost fields is the same rule Python uses for function default arguments. A field with a default can't come before a field without one, because positional arguments fill from the left. If you need a non-defaulted field after a defaulted one, reorder your fields.
If you pass too many defaults, Python raises an error:
You can check what defaults a named tuple has via the _field_defaults dictionary, which is one of the inspection helpers the type carries:
Only the fields with explicit defaults appear. name and category are required, so they're not in the dictionary.
_asdict, _replace, _fields, _makeNamed tuples come with a small set of helper methods. They all start with an underscore, which is unusual for public API. The reason is that the underscore prefix keeps them from colliding with field names you choose. If a method were called asdict, you couldn't have a field called asdict without clobbering it. The leading underscore is a namespace, not a "don't touch this" marker. These methods are part of the public API and you're meant to use them.
_asdict(): Convert to a Dictionary_asdict() returns a regular dict with the field names as keys. This is handy for serializing to JSON, logging, or passing into something that expects a dict.
Since Python 3.8, _asdict() returns a regular dict (it returned an OrderedDict in older versions, but regular dicts preserve insertion order since 3.7 anyway, so the change is mostly cosmetic).
_replace(**kwargs): Make a Changed CopyBecause named tuples are immutable, you can't update a field in place. _replace builds a new instance with some fields swapped out, leaving the original unchanged.
The original mouse is untouched. _replace is the named-tuple equivalent of "update this field", just functional in style. You get a new value instead of mutating the old one. In a system where the same product gets passed around to many functions, that immutability is a feature: nobody can change your data behind your back.
_replace only accepts keyword arguments, and the keywords must match real field names. A typo raises:
That early error is one of the small wins over dict-based records, where a typo on a key just creates a new key and silently breaks things later.
_fields: The Field Names_fields is a tuple of the field names. This is what makes the class self-describing.
You can use this to drive generic code that works across any named tuple type, like a function that prints a record as a table or that builds a CSV header row.
_make(iterable): Build From an Iterable_make is a class method that constructs an instance from any iterable of the right length. The most common use is reading rows out of a CSV or a database.
_make does the same thing as unpacking the iterable into the constructor (Product(*row)), with one small advantage: the intent is clearer. Anyone reading Product._make(row) knows immediately you're taking an existing sequence and tagging its positions with names.
typing.NamedTuplePython 3.6 added a second way to define named tuples that uses class syntax with type annotations. It lives in the typing module and ends up creating the same kind of object: a tuple subclass with named fields.
Same behavior as collections.namedtuple. The instance prints the same way, supports the same attribute and index access, has the same immutability, and carries the same underscore methods (_asdict, _replace, _fields, _make).
What's different is the definition itself. You get three things the factory form doesn't easily give you.
The fields carry types. This isn't enforced at runtime (Python doesn't refuse a string where you said int), but it's read by type checkers like mypy and by IDEs that offer autocomplete. For team codebases that use type checking, this alone is reason enough to prefer the class form.
mypy would flag the "twenty bucks" argument, but the runtime is happy to store whatever you pass. Type annotations on named tuples are documentation and tooling hooks, not validators.
Defaults are written directly on the field, the same way you'd write them on a class attribute or a function parameter.
Same rule as before: defaulted fields have to come after non-defaulted ones. The class-based form is the more readable version of this, since the default sits right next to the field it applies to.
You can define methods on the class. These become instance methods, just like on a regular class. The fields stay immutable, but methods give you a clean place to put logic that depends on the data.
is_in_stock is a simple predicate. discounted returns a new product with the price adjusted; it uses _replace internally, which is the standard pattern for "modify" operations on immutable types.
You can't define instance state outside the declared fields, since the underlying tuple is fixed. Methods can only read fields and return new values.
Subclassing a typing.NamedTuple is allowed if you're inheriting to add methods, but it has a sharp edge: you can't add fields to a subclass.
Adding fields in a subclass produces an error. If you need a different set of fields, define a new named tuple rather than subclassing. The fixed shape is part of how named tuples stay small and fast, and the type system enforces that constraint.
collections.namedtuple vs typing.NamedTupleBoth produce the same kind of object at runtime. The differences are about the definition site and the developer experience around it.
| Feature | collections.namedtuple | typing.NamedTuple |
|---|---|---|
| Style | Factory function | Class definition |
| Type annotations | No | Yes |
| Default values | defaults=[...] parameter | Inline on each field |
| Methods | Not directly | Defined in the class body |
| Docstrings | Set via Product.__doc__ = "..." | Standard class docstring |
| Inheritance | Awkward | Allowed (no new fields) |
| Module | collections (stdlib, always present) | typing (stdlib since 3.5) |
| Available since | Python 2.6 | Python 3.6 |
Pick typing.NamedTuple when:
Pick collections.namedtuple when:
Both are interchangeable for most purposes, and you'll see both forms in real codebases. New code in a typed codebase usually goes with typing.NamedTuple.
Named tuples sit between plain tuples and full classes. They're not always the right tool; knowing when to reach for them is half the battle.
The four options each have their place:
Plain tuple is right when the record is small, throwaway, and the positions are obvious. Returning (min_value, max_value) from a function doesn't need names; the function name tells you what you're getting.
Named tuple is right when you have a small, fixed set of fields, you want immutability, and the record gets passed around enough that named access pays for the slightly heavier definition. Things like Product, OrderLine, Address, Coordinate, and Customer are classic fits.
Dataclass is right when you need mutability, more methods, or a class that grows over time. @dataclass from the standard library gives you a similar concise definition with full class powers.
Dictionary is right when the keys are dynamic (you don't know all field names at code-writing time), or when you're working with JSON-shaped data that's already a dict and there's no payoff to wrapping it.
A short comparison of the same Product record across the four:
All four hold the same data. The differences are in how you reach the fields, whether you can mutate them, and what tooling you get for free. Named tuples are the right answer often enough to be worth knowing well, even if they're not the right answer every time.
Because named tuples are tuples, they're hashable as long as every field value is hashable. That means they can serve as dictionary keys or set members. This is one of their most useful properties for code that needs to look up records by a composite key.
If any field holds an unhashable value (a list, for instance), the whole named tuple becomes unhashable and you'll get a TypeError when you try to put it in a dict or set.
A few patterns show up often enough to be worth seeing:
Returning a named tuple from a function is much friendlier than returning a four-element plain tuple. The caller can write result.total instead of result[3], and the print output shows what each number means.
OrderLine is the kind of thing that would otherwise tempt you to write a full class. With typing.NamedTuple you get the same readability, plus immutability for free, in fewer lines.
_make is the bridge between positional data sources (CSV rows, query results) and the named-field world. Once the data is in named tuples, the rest of the code reads cleanly.
10 quizzes