Couldn’t you pack attachment and attachment_name into a tuple of two elements (str, str), then do something like this?
type AttachmentData = tuple[str, str]
# Or another TypedDict for AttachmentData
class PrivateBinAPIJSON(TypedDict):
paste: str
attachment_data: tuple[AttachmentData, ...]
It would also stop usage of the class with say 3 attachments but 5 attachment-names. Maybe even a dictionary for name to attachment would work (unless names can be repeated).
I think for this case, and probably many others, a bit of redesigning can be helpful.
I don’t think it’s fair to assume that people have the ability to redesign external JSON APIs (which seems like what’s being used here) just for the sake of simplifying typing them in Python.
I assume @jobe’s idea was not to redesign the PrivateBin API but to choose your own more ergonomic data structure(s) and convert to/from PrivateBin’s structure at the boundaries where you send/receive that data.
I would probably model an API like that with a NewType(dict[str, Any]) and then make my own dataclass, e.g.,
While using the type union would allow the type checker to warn you that you missed one of the cases, a small restructure means that you only need to care about it in exactly two places.
It is already exclusively used at the boundaries. Doesn’t mean I don’t want type safety there, though.
Something like:
def create(
paste: str,
attachments: Attachment | Iterable[Attachment] | None,
) -> Paste:
data = PrivateBinAPIJSON(paste=paste)
match attachments:
case Attachment():
data["attachment"] = attachments.content
data["attachment_name"] = attachments.name
case Iterable():
data["attachment"] = [a.content for a in attachments]
data["attachment_name"] = [a.name for a in attachments]
response = httpx.post(
url,
json=data,
)
return Paste.from_response(response)
Regardless, I don’t really like where this conversation is going. The current type system makes a real-world API like this impractical to type, while this PEP makes it trivial. The fact that you can tuck the untyped code away somewhere else or isolate it at the boundary is orthogonal to that because it doesn’t address the underlying limitation.
I can achieve this today with 3 TypedDicts but that’s simply too verbose too bother with.
Nerd-sniped into doing it with 2
class Paste(TypedDict):
paste: str
class PasteAttachment[A: (str, list[str])](Paste):
attachment: A
attachment_name: A
type PrivateBinAPIJSON[A: (str, list[str])] = Paste | PasteAttachment[A]
Feel we’re getting off topic with the PasteBin stuff, but I would find this general PEP super useful, even with the consideration of alternatives raised by @Viicos
def format_name_of_type(t: TypeHint) -> str:
module = getattr(t, '__module__', None)
name = getattr(t, '__name__', None)
args = getattr(t, '__args__', None)
if module is not None and name is not None and args is None:
return f'{modle}.{name}'
# ... Handle other edge cases
The above could wouldn’t fail or fall into the edge cases if __name__ == '' - it would produce __main__. which is just confusing. Either don’t set it at all, or set it to a non-empty string. An empty string is an obviously terrible idea.