Mark a set of keys of a TypedDict as incompatible

I have a couple of TypedDicts for use in type hinting that are supposed to have some sets of incompatible keys.

For example, if I need to specify that a particular dict can have bar or baz but not both of them, I can write two TypedDicts:

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
}

However, this doesn’t scale very well if I were to have multiple sets; for example, a simple multiplication shows that three sets of 2, 3 and 4 keys each will result in 24 {{{TypedDict}}}s. A solution would be:

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

…or, with a decorator somewhat similar to @overload:

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

I asked this question on StackOverflow and was informed that such a feature doesn’t exist. How about adding it to Python then?

This feels like it’s going to be a complicated, hard to understand feature that’s rarely useful. What are some example scenarios where this would apply? I saw your SO post mentioned something about the API for a website.

How would it interact with inheritance between two TypedDicts, or with the NotRequired field specifier? How should type checkers check usage of these TypedDicts?

2 Likes

The original problem is to ensure that a dict using that interface would not have more than one key for each set.

@incompatible should make all given keys NotRequired, which means:

class Foo(TypedDict):  # 'foo' is required
  foo: str

@incompatible('foo', 'bar')
class Bar(Foo):  # Both 'foo' and 'bar' is not required.
  bar: NotRequired[int]  # This excessive usage should be warned.

lorem: Foo = {}  # Warning ('foo' is not found)

ipsum: Bar = {  # Warning (both 'foo' and 'bar' found)
  'foo': 'foo',
  'bar': 12
}

dolor: Bar = {  # No warning.
  'foo': 'foo'
}

As for usages, unfortunately I can’t think of any cases other than what I already stated in the SO post linked above.