How to add a list of field names to a class based on NamedTuple?

Question:

I have a basic class like this:

from typing import NamedTuple

class A(NamedTuple):
    f1: str = ""


aa = A("haha")
print(aa)

Now suppose I have a list of more fields that I want to use in Class A, essentially:

more_field_names = ['f2', 'f3', 'f4']

and I want to make Class A look like this, but without manually typing all the field names:

class A(NamedTuple):
    f1: str = ""
    f2: str = ""
    f3: str = ""
    f4: str = ""

Is there a way to use something like list comprehension to add each field names to the Class A definition?

Asked By: user3768495

||

Answers:

I’m not entirely sure if this is what you’re looking for, but you can dynamically generate the class definition to copy-paste into a python file, based on a list of fields that you want to add (assuming you have a lot of same-type fields to add):

def gen_named_tuple_schema(fields: list[str], class_name='A'):
    fields = 'n'.join(f"    {f}: str = ''" for f in fields)
    return f'class {class_name}(NamedTuple):n{fields}'

Then you can use it like so:

print(gen_named_tuple_schema(['f1', 'f2', 'f3', 'f4']))

Output:

class A(NamedTuple):
    f1: str = ''
    f2: str = ''
    f3: str = ''
    f4: str = ''

I would add that, even though it is technically possible to run exec() on the generated code, I would honestly not recommend it. The reason is that type checkers won’t know the type of A by default, so they won’t be able to offer field auto-completion and type checking.

For example, in my case PyCharm does not complain at all if I do this:

return_dict = {}
exec(gen_named_tuple_schema(['f1', 'f2', 'f3', 'f4']),
     {'NamedTuple': typing.NamedTuple},
     return_dict)
A = return_dict['A']  # my type checker: what is A?

print(A(1, '2', 3, '4'))   # A(f1=1, f2='2', f3=3, f4='4')
Answered By: rv.kvetch
from typing import NamedTuple
fields = {f'f{i}': str for i in range(5)}
foo = NamedTuple('foo', **fields)
print(foo.__annotations__)
# {'f0': str, 'f1': str, 'f2': str, 'f3': str, 'f4': str}
Answered By: KonstantinosKokos

Consider using the namedtuple() factory function from the collections module. As shown in the documentation, it is well suited to supplying the field names as data:

from collections import namedtuple

A = namedtuple('A', ('f1'))
B = namedtuple('B', A._fields + ('f2', 'f3', 'f4'))

Optionally default values can given as well:

Person = namedtuple('Person', ('name', 'age', 'points'), defaults=(0, 0))

Annotations can be added dynamically using the same technique as in the code for NamedTuple:

types = {'name': str, 'age': int, 'points': int}
Person.__annotations__ = Person.__new__.__annotations__ = types

Dynamically added types will show-up in tool tips, help(), and calls to inspect.get_signature(). Static type checkers such as MyPy may have trouble with dynamically generated information.

Answered By: Raymond Hettinger