0

I have an interface named Foo which is supposed to have, aside from other common keys, either one of the two given keys, bar and baz. To let PyCharm know, I wrote two interfaces:

from typing import TypedDict

class Foo1(TypedDict):
  bar: str
    
class Foo2(TypedDict):
  baz: int

Foo = Foo1 | Foo2

foo_instance_1: Foo = {  # Works fine
  'bar': 'foobar'
}
foo_instance_2: Foo = {  # Also fine
  'baz': 42
}

foo_instance_3: Foo = {  # Warning: Expected type 'Foo1 | Foo2', got 'dict[str, str | int]' instead
  'bar': 'foobar',
  'baz': 42
}

That works. However, the problem is the real interface I'm dealing with has more than just one incompatible set of keys. That being said, if there are three sets with 2, 3 and 4 keys correspondingly, I'll have to write 2 * 3 * 4 or 24 interfaces. It would be great if something like this exists:

class Foo(TypedDict):
  bar: IncompatibleWith('baz', 'qux')[str]
  baz: IncompatibleWith('bar', 'qux')[int]
  qux: IncompatibleWith('bar', 'baz')[bool]

...or, better yet:

@incompatible('bar', 'baz', 'qux')
# ...
class Foo(TypedDict):
  bar: str
  baz: int
  qux: bool

Real world context: I'm writing a source code generator to generate API interfaces for a site, which I do not have control of, in Python (that site's API system has an API for retrieving API documentation). These interfaces are for type hinting only. While it is true that I can just generate all combinations, that would make the file much much longer.

Is there a short and easy way to mark a set of keys of an interface as incompatible with each other?

InSync
  • 4,851
  • 4
  • 8
  • 30
  • With what you have you are already correctly getting a warning when `foo_instance_3: Foo` is assigned with a dict of keys not exclusively in either `Foo1` or `Foo2`. I just don't see a need to invent something as you described. – blhsing Apr 11 '23 at 01:35
  • @blhsing That warning is correct, true. But that's not my point: I want to get that same warning without having to write two interfaces. – InSync Apr 11 '23 at 01:39
  • 1
    I see. As far as I know there's currently no such support in Python's typing system so there is no way a static type checker can do this. The best you can do is to write your own version of `TypedDict` that checks for incompatible keys at runtime. – blhsing Apr 11 '23 at 01:42
  • That would be the job of the API itself. I don't want to wait until runtime; I want PyCharm (or whatever type checker) to warn me whenever I add incorrect keys. That's what type hinting is all about. – InSync Apr 11 '23 at 01:47
  • 1
    Exactly my point. Until Python has an official support for what you describe there is no way for a static type checker such as PyCharm/mypy/PyLance to perform static analysis of a custom behavior. – blhsing Apr 11 '23 at 01:50
  • And why introduce this complexity? If the groups are non-overlapping, you can just create a `TypedDict` (or, even better, `dataclass`) with one or more fields, each representing chosen "oneof" alternative (e.g. it can be `@dataclass class Foo: field_a: tuple[Literal['bar'], str] | tuple[Literal['baz'], int] | tuple[Literal['qux'], bool]`; more fields can be added to represent another groups). Then add something like `as_out(self)` method that will return `{k: v for k, v in dataclasses.asdict(self).values()}`, reverse converter to create from dict (classmethod), if necessary, and `__getitem__` – STerliakov Apr 11 '23 at 14:14
  • The suggestion above assumes that `bar`, `baz` and `quz` can be semantically grouped into a single field, otherwise it might look too synthetic. Nothing close to your original suggestion exists, but you're free to write a `mypy` plugin supporting this (in `get_class_decorator_hook` just create all 24 definitions and put them into the symbol table, nothing esoteric). You're completely out of luck with PyCharm. – STerliakov Apr 11 '23 at 14:21
  • (and dictcomp should be just `dict(dataclasses.asdict(self).values())`, sorry) – STerliakov Apr 11 '23 at 14:32
  • @SUTerliakov The problem with `dataclass` is that I have to call the class explicitly, which means I will get a warning I do not want: `def f(d: Foo): ...; f({ 'field_a': ... }) # Warning`. These interfaces are supposed to be a self-contained package that represents the API documentation and supports type checking when passing JSON-serializable `dict`s to functions that do API calls. Adding a new self-made field means that I have to convert them back in the functions, which, again, I do not have access to. – InSync Apr 11 '23 at 23:11
  • Some links to TypeScript questions on the same subject: [typescript interface require one of two properties to exist](https://stackoverflow.com/q/40510611), [Typescript Interface - Possible to make "one or the other" properties required?](https://stackoverflow.com/q/37688318) – InSync Apr 12 '23 at 10:54

0 Answers0