pythontuplespython-dataclasses

How can I provide type hints while destructuring my class


I would like to create a class that looks something like ConfigAndPath:

import pathlib
from typing import TypeVar, Generic
from dataclasses import dataclass, astuple

class ConfigBase:
    pass
T = TypeVar("T", bound=ConfigBase)

@dataclass
class ConfigAndPath(Generic[T]):
    path: pathlib.Path
    config: T

I often have a list of these ConfigAndPath, and so would want to destructure it in list comprehensions like this:

l: list[ConfigAndPath[MyConfig]] = ...
filenames = [_path.name for _path, _my_config in l]

So I added an __iter__ method to my class:

# In ConfigAndPath
def __iter__(self):
    return iter(astuple(self))

However, I'm not sure how to make it so that my typechecker (Pyright) realizes that _path is a pathlib.Path and _my_config is a MyConfig. This works with NamedTuple, however I can't appear to use generics with NamedTuple due to it not allowing multiple inheritance. Is this possible to specify what I want?

I tried writing an as_tuple method:

# In ConfigAndPath
def as_tuple(self) -> Tuple[pathlib.Path, T]:
    return self.path, self.config

Which then allows me to write

filenames2 = [_path.name for _path, _my_config in [i.as_tuple() for i in l]]

Which gives me a type hint but is quite verbose and walks the list twice.


Solution

  • You can destructure a dataclass using a match block (requires python ≥ 3.10). It is more verbose, but type checkers understand it. For example, using mypy:

    @dataclass
    class Foo(Generic[T]):
        x: int
        y: T
    
    foo = Foo[float](1, 2.0)
    
    match foo:
        # NB. this case means must be an object, and have an attribute x and attribute y,
        # which will be stored in the current scope as x and y respectively.
        case object(x=x, y=y):
            reveal_type(x)  # note: Revealed type is "builtins.int"
            reveal_type(y)  # note: Revealed type is "builtins.float"
            assert x + y == 3
        case _:
            raise RuntimeException('unreachable')
    

    However, a match block is a statement and not an expression. As such it cannot be used within a list comprehension.