An admin uploads a CSV export, and you learn its columns only when you
read the header. You still want each row to be a record with named
attributes, a readable repr and ==. dataclasses.make_dataclass
builds a dataclass from a list of fields while the program runs:
from dataclasses import fields, make_dataclass
header = ["id", "name", "email"] # read from the file
Row = make_dataclass("Row", [(name, str) for name in header], frozen=True)
row = Row(id="1", name="Ada", email="ada@example.com")
print(row) # Row(id='1', name='Ada', email='ada@example.com')
print([f.name for f in fields(Row)]) # ['id', 'name', 'email']
The result is an ordinary dataclass. Code that walks fields() works on
it the same way as on a class you wrote by hand.
Describe each field as a name, a pair or a triple
Each entry in the list is one field:
- A bare name, such as
"id". Its annotation meansAny. - A
(name, type)pair, such as("id", int). - A
(name, type, field(...))triple, for a default or another field option.
The keyword arguments are the options that @dataclass takes, such as
frozen=True and kw_only=True:
from dataclasses import field, make_dataclass
Row = make_dataclass(
"Row",
[
("id", int),
("name", str),
("tags", list[str], field(default_factory=list)),
],
frozen=True,
kw_only=True,
)
first = Row(id=1, name="Ada")
second = Row(id=2, name="Lin")
first.tags.append("reviewed")
print(first.tags, second.tags) # ['reviewed'] []
As with any dataclass, Python does not check the values against the
types, so Row(id="one", name=2) builds a row.
mypy cannot see the fields
mypy reads your source code, and the fields of Row exist only once the
program runs. So mypy types Row as a plain type and every row as
Any. A typo such as row.emial passes mypy --strict, then raises
AttributeError when the program runs.
Keep generated classes at the boundary of the program, where the data comes in. Check each value yourself before the rest of the code relies on it. If later code needs fixed fields, convert each row into a class declared in your source, so that mypy can check the code that uses it.
Common mistakes
Building the class again for each file or request. Every call makes a
new class, even with the same name and fields. The generated __eq__
compares only objects of the same class, so two rows with the same values
from different calls are not equal. Build the class once per schema and
reuse it.
Trusting the header. A field name must be a valid identifier, must not
be a keyword, and must not repeat. make_dataclass raises TypeError
for "first name", for "class" and for a repeated name. Check the
header first and report an error that names the bad column. If the schema also names types,
look them up in a dict of the types you allow, such as
{"int": int, "str": str}. Never pass schema text to eval.
Pickling rows of a class made inside a function. pickle saves a
class by its module and name, and looks the class up again when it loads
the data. A class built inside a function has no module-level name, so
pickle.dumps raises PicklingError. Assign the class to a module-level
variable with the same name as the class, or send plain dicts instead.
Check your understanding
You call make_dataclass("Row", columns) once per request with the same columns. Two rows from different requests hold the same values but compare unequal. Why?
Show answer
Hide answer
Each call creates a new class. The generated __eq__ returns
NotImplemented when the other object has a different class. Python then
falls back to comparing identity, and two separate objects are never
identical. Build the class once, when you first load the schema, and
reuse it.
Going further
namespace={"name": function}adds methods to the class, andbases=(Base,)gives it base classes.- Python 3.14 added a
decorator=argument, a function to call in place ofdataclasses.dataclass. It receives the class and the same keyword arguments. - If you know the schema before the program runs, generate Python source from it instead. Then mypy sees the fields.
The make_dataclass reference lists every option.