Object orientation won't offer you good support for dynamic (runtime) created attributes.
but you can create an improve a class that will load the attributes it should support as data, and operate upon them.
A "typeddict" also will have much less value past the pre-running stage of type checking - which doesn't apply to user, runtime, generated content at all.
So, having a class that would getthe attributes needed as a dictionary - it can be a plain dictionary, mapping each attrite name to all the information you need for that attribute - from typing, to creation metadata, maybe formulas for computed output attributes, etc... and spills out usable Python objects - which don't need to have a class of their own, but can offer whatever interface you need to work with those attributes: constrain input types, create a corresponding table in a SQL database, etc...
Since attributes are data you may not want to interact with them using python code, as it would require typing in the attribute name in your source files: you will use the attributes by their string name - it is possible to do so with the getattr and setattr built-ins - but it is awkward compared to using the ["attrname"] syntax.
With this reasoning, what seems that will fit you better is a custom "MutableMapping" class - maybe with other ancillary classes, for dealing, for example, with the metadata - and a well written set of __getitem__, and __setitem__ types. It could even render itself into a Python dataclass Python source code, if you'd need to "solidify" one of these instances.
Say, something along:
from collections.abc import MutableMapping, Mapping, Sequence
class UserContent(MutableMapping):
def __init__(self, name, spec, initial=None):
self.spec = spec
self.name = name
self.data = {}
if initial and isinstance(initial, Sequence):
for key, value in zip(spec.keys(), initial):
self[key] = value
elif isinstance(initial, Mapping):
self.data.update(initial)
def __iter__(self):
yield from self.data.keys()
def __setitem__(self, key, value):
if key not in self.spec or not isinstance(value, self.spec[key]):
raise KeyError("...")
self.data[key] = value
def __getitem__(self, key):
return self.data[key]
def __delitem__(self, key):
del self.data[key]
def __call__(self, *args):
"""create s a new "instance" of this with the same specs, and new content"""
if len(args) == 1 and isinstance(args[0], Mapping):
initial = args[0]
else:
initial = args
return type(self)(self.name, self.spec, initial)
def iscomplete(self):
# Assumes all keys in spec are required - returns
# False if there are mssing attributes, True otherwise
return not (set(self.data.keys()) - set(self.spec.keys()))
def __dir__(self):
return list(self.spec.keys())
def __len__(self):
return len(self.data)
def __repr__(self):
return f"<{self.name}({self.data})>"
TrickJump = UserContent("TrickJump", {"id": int, "name": str, "difficulty": int})
myjump = TrickJump(1, "Double Jump", 10)
You can enhance this as desired. For example "isinstance" won't work, because it expects the seconf argument to be an actual class -but you could just add a .isinstance method there and check if the testes instance has the same spec.
In the same way, the spec can be as sophisticated as desired. Also, pickling, copying, etc should work out of the box with that
typeexplicitly. If you want instances ofTrickjumpwith attribute values provided by a user, which you choose depends on whether you want an object with attributes or adict; it's a matter of interface preference, not correctness. Note that subclasses ofTypedDictare primarily used as type hints, and a function likedef foo(x: Trickjump): ...would accept{'id': 3, 'name': 'bob', 'difficulty': 'low'}just as well asTrickjump(3, 'bob', 'low').