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?


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.