I was quite surprised to see that this feature is actually possible!! (at least using python 3.11, not tried before)
This is enabled thanks to https://peps.python.org/pep-0622/#exhaustiveness-checks that provides a match construction that must pass type checking.
Execute the following code with $ python foo.py and run the type checking with $ mypy --strict foo.py.
Demo:
### Define our types
# See https://github.com/python/mypy/issues/17139 to get a nicer
# syntax once the bug is solved
class Mult:
# Expr is not yet defined, so we use forward references
# https://peps.python.org/pep-0484/#forward-references
left: 'Expr'
right: 'Expr'
def __init__(self, left:'Expr', right:'Expr'):
self.left = left
self.right = right
class Add:
# Expr is not yet defined, so we use forward references
# https://peps.python.org/pep-0484/#forward-references
left: 'Expr'
right: 'Expr'
def __init__(self, left:'Expr', right:'Expr'):
self.left = left
self.right = right
class Const:
val: int
def __init__(self, val:int):
self.val = val
Expr = Const | Add | Mult
### Define our functions
def my_eval(e : Expr) -> int:
match e:
case Const():
return e.val
case Add():
return my_eval(e.left) * my_eval(e.right)
case Mult():
return my_eval(e.left) + my_eval(e.right)
### Use them
print(my_eval(Const(42)))
print(my_eval(Add(Const(42),Const(45))))
$ python foo.py
42
1890
$ mypy --strict foo.py
Success: no issues found in 1 source file
If you decide now to remove one item, for instance by commenting the Add() case, you get an error (I have to admit, not extremely clear):
$ mypy --strict foo2.py
foo2.py:37: error: Missing return statement [return]
Found 1 error in 1 file (checked 1 source file)
TODO It would be great to find a simpler interface to define types, notably I found
class Mult(tuple[int, int]):
pass
but it does not seem to be working so far inside mypy https://github.com/python/mypy/issues/17139
Caveats
Note that this method has a few drawbacks. Notably, as pointed in comments (I filled a bug report here), if the return type is None (say you want to return nothing) like def my_eval(e : Expr) -> None, then the test will pass even if you remove a case:
# Execute with "python foo.py" and
# run the type checking with "mypy --strict foo.py"
### Define our types
# See https://github.com/python/mypy/issues/17139 to get a nicer
# syntax once the bug is solved
class Mult:
# Expr is not yet defined, so we use forward references
# https://peps.python.org/pep-0484/#forward-references
left: 'Expr'
right: 'Expr'
def __init__(self, left:'Expr', right:'Expr'):
self.left = left
self.right = right
class Add:
# Expr is not yet defined, so we use forward references
# https://peps.python.org/pep-0484/#forward-references
left: 'Expr'
right: 'Expr'
def __init__(self, left:'Expr', right:'Expr'):
self.left = left
self.right = right
class Const:
val: int
def __init__(self, val:int):
self.val = val
Expr = Const | Add | Mult
### Define our functions
x = 0
def my_eval(e : Expr) -> None:
match e:
# This should fail: still it works
# case Const():
# print(e.val)
case Add():
print("(")
my_eval(e.left)
print(") + (")
my_eval(e.right)
print(")")
case Mult():
print("(")
my_eval(e.left)
print(") + (")
my_eval(e.right)
print(")")
### Use them
my_eval(Add(Const(42),Const(45)))
Note also that this does not prevent the function from raising an Exception, or never ending https://github.com/python/mypy/issues/17140
Answer from tobiasBora on Stack OverflowI am playing around with the python typing stuff (https://docs.python.org/3/library/typing.html) and I am running python 3.8.3
I have code like:
from typing import Literal, TypedDict
UserPayload = dict[Literal['user'], str]
class IOUPayload(TypedDict):
lender: str
borrower: str
amount: float
def post(path, payload:Union[UserPayload, IOUPayload]=None):
if path == "/iou" and isinstance(payload, IOUPayload):
print(f"{payload['borrower']} owes {payload['lender']} now!")
if path == "/add" and isinstance(payload, UserPayload): # this is wrong!
print(f"add {payload['user']} to DB")
Examples that I am finding use "primitive" types like list, int, dict in the Union. Which isinstance works for. How do I check for the type "alias" (custom type?) that I created?
EDIT:
I want this for mypy eventually, I am starting with the pyright plugin for now though.
As a workaround I converted UserPayload to a class::
class UserPayload(TypedDict):
user: strI also noticed that I have never updated my pyenv install so I thought they had not packaged python 3.9.1 yet. I'm busy installing it now and it is taking suspiciously long. I am hoping it might be to do with the version.
EDIT 2
It just clicked that typing has type hints which is why u/double_en10dre was suggestion pydantic. I was trying to use hints at runtime. My current solution is to just check a property of the dict so:
from typing import Literal, TypedDict
UserPayload = dict[Literal['user'], str]
class IOUPayload(TypedDict):
lender: str
borrower: str
amount: float
def post(path, payload:Union[UserPayload, IOUPayload]=None):
if path == "/iou" and 'lender' in payload:
print(f"{payload['borrower']} owes {payload['lender']} now!")
if path == "/add" and 'user' in payload:
print(f"add {payload['user']} to DB")
The problem now is that I guess I have to write code to make sure payload is valid since I can't use external modules.