Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

disjoint unions in mypy? #6863

Closed
kurtgn opened this issue May 18, 2019 · 4 comments
Closed

disjoint unions in mypy? #6863

kurtgn opened this issue May 18, 2019 · 4 comments
Labels

Comments

@kurtgn
Copy link

kurtgn commented May 18, 2019

I am trying to customize a function that takes an object of a union type and accesses different attributes depending on the type. The type of the object is specified by a type attribute.

from typing import Union

from dataclasses import dataclass


TEXT = 'TEXT'
IMAGE = 'IMAGE'


@dataclass
class TextMessage:
    text: str
    type = TEXT


@dataclass
class ImageMessage:
    url: str
    type = IMAGE


def output(message: Union[TextMessage, ImageMessage]) -> None:
    if message.type == TEXT:
        print(message.text)

Mypy does not like this, saying:

 error: Item "ImageMessage" of "Union[TextMessage, ImageMessage]" has no attribute "text"

And I understand it. I am trying to guess one attribute based on the value of another one. This is poor engineering.

However, the same thing works in other languages, for example, in JS Flow:

// @flow

type TextMessage = {
	type: 'TEXT',
  	text: string
};
type ImageMessage = {
	type: 'IMAGE',
  	url: string
};

function output(message: TextMessage | ImageMessage) {
  if (message.type === 'TEXT') {
    console.log(message.text);
  }
} 

Live Flow link: here

So the question is, is there a way to make mypy understand my conditions? Maybe using Literal types? I tried to figure this out but the docs are a bit confusing.

P.S. I can always do if isinstance(message, TextMessage) and this will satisfy mypy, but putting lots of isinstance() in my code seems even more hacky.

@ethanhs
Copy link
Collaborator

ethanhs commented May 19, 2019

P.S. I can always do if isinstance(message, TextMessage) and this will satisfy mypy, but putting lots of isinstance() in my code seems even more hacky.

Aren't you essentially trading off putinig lots of isinstance(message, TextMessage) with lots of if message.type == 'TEXT'? The first is 1) more Pythonic 2) better supported 3) more readable and 4) doesn't require you to add a type tag to all of your classes.

As for using Literal to solve your issues, under "Extra" features to add in #5935 there is "Type narrowing with equality and inequality checks", which is what you want.

@ilevkivskyi
Copy link
Member

But why this

def output(message: Union[TextMessage, ImageMessage]) -> None:
    if message.type == TEXT:
        print(message.text)

is better than this

def output(message: Union[TextMessage, ImageMessage]) -> None:
    if isinstance(message, TextMessage):
        print(message.text)

?

Anyway, I propose to close this since this is essentially the same as "Type narrowing with equality and inequality checks" item from #5935 as @ethanhs mentioned.

@Garrett-R
Copy link

Garrett-R commented Dec 14, 2022

Anyway, I propose to close this

Agreed!

Interestingly, TypeScript does offer this feature (discriminating unions), but @mneira10 pointed out that they're more important in TS given that TS doesn't have a clean way of doing isinstance like Python does. You could write user-defined type guards (example), but that's messy.

@JukkaL
Copy link
Collaborator

JukkaL commented Dec 15, 2022

Besides, the original example works already if using Final and literal types:

from typing import Final, Literal
from dataclasses import dataclass


TEXT: Final = 'TEXT'
IMAGE: Final = 'IMAGE'


@dataclass
class TextMessage:
    type: Literal['TEXT']
    text: str


@dataclass
class ImageMessage:
    type: Literal['IMAGE']
    url: str


def output(message: TextMessage | ImageMessage) -> None:
    if message.type == TEXT:
        print(message.text)  # No error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants