Hacker News new | past | comments | ask | show | jobs | submit login

One caveat of the tip in the "Deduplicating shared variant state" section about including an underspecified discriminator field in the base class, is that it doesn't play well if you're using Literals instead of Enums as the discriminator type. Python does not allow you to narrow a literal type of a field in a subclass, so the following doesn't type check:

  from typing import Literal
  
  class _FrobulatedBase:
      kind: Literal['foo', 'bar']
      value: str
  
  class Foo(_FrobulatedBase):
      kind: Literal['foo'] = 'foo'
      foo_specific: int
  
  class Bar(_FrobulatedBase):
      kind: Literal['bar'] = 'bar'
      bar_specific: bool


  "kind" overrides symbol of same name in class "_FrobulatedBase"
    Variable is mutable so its type is invariant
      Override type "Literal['foo']" is not the same as base type "Literal['foo', 'bar']"
https://pyright-play.net/?code=GYJw9gtgBALgngBwJYDsDmUkQWEMo...



> it doesn't play well if you're using Literals instead of Enums as the discriminator type

The original example code with Enums doesn't type-check either, and for the same reason:

If the type checker allowed that, someone could take an object of type Foo, assign it to a variable of type _FrobulatedBase, then use that variable to modify the kind field to 'bar' and now you have an illegal Foo with kind 'bar'.


mypy typechecks that just fine[1].

However, I think that's possibly a bug :-) -- I agree that narrowing a literal via subclassing is unsound. That's why the example in the blog used `str` for the superclass, not the closure of all `Literal` variants.

(I use this pattern pretty extensively in Python codebases that are typechecked with mypy, and I haven't run into many issues with mypy failing to understand the variant shapes -- the exception to this so far has been with `RootModel`, where mypy has needed Pydantic's mypy plugin[2] to understand the relationship between the "root" type and its underlying union. But it's possible that this is essentially unsound as well.)

[1]: https://mypy-play.net/?mypy=latest&python=3.12&gist=f35da62e...

[2]: https://docs.pydantic.dev/latest/integrations/mypy/


Using str in the superclass equally unsound and also doesn't type check. There's no good way to do it, as the discriminator type is by definition disjoint between all kinds.


The problem here seems to be inheritance. Why share a base class? Why not write Frobulated = Foo|Bar and go about your day writing functions like

    def frotz(x: Frobulated) -> str:
        return f”{x.value} is the value of x”




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: