Wardy
Wardy

Reputation: 73

Is there a way to hint that an attribute can't be None in certain circumstances?

I'm trying to avoid having a bunch of ifs or asserts in the code using this class.

class TemplateRow(tp.NamedTuple):
    """Parsed template row, if not error."""

    template: Template | None = None
    error: str | None = None

    @property
    def valid(self) -> bool:
        """Determine if this row is valid.

        Returns:
            bool: is this template row valid
        """
        return self.error is None


def read_template(filename: str | Path) -> tp.Iterator[TemplateRow]:
    """Read a TSV file into a list of templates.

    Args:
        filename (str): Path to the TSV file

    Yields:
        TemplateRow : parsed template row, or error message
    """
    with open(filename, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f, dialect=csv.excel_tab)
        for row in reader:
            try:
                yield TemplateRow(template=Template.parse_obj(row))
            except pydantic.ValidationError as e:
                yield TemplateRow(error=str(e))

So, I read in a TSV and try to convert it to a Pydantic model with a bunch of validators. If it works, I return the filled-in model. If it fails, I return the validation error string

To make the caller be able to just expect one type, I return a NamedTuple which can contain the parsed model instance, or an error string.

There's a simple property to say if it's valid, mainly for readability and for a possible future enhancement.

Anyway, if I have a loop such as:

ok: list[Template]=[]
notok: list[str] = []

for row in read_template(file):
    if row.valid:
       ok.append(row.template)
    else:
       notok.append(row.error)

I will get mypy errors that row.error might be None

As I've already checked the .valid property, it can't be.

Is there anyway to communicate this to type checkers?

thanks

Upvotes: 0

Views: 63

Answers (2)

Daniil Fajnberg
Daniil Fajnberg

Reputation: 18751

I don't understand the restriction of having the iterator yield only one type.

Why not just do it like this?

import csv
from collections.abc import Iterator
from pathlib import Path

from pydantic import BaseModel, ValidationError


class Template(BaseModel):
    foo: str
    bar: int


def read_template(filename: str | Path) -> Iterator[Template | ValidationError]:
    with open(filename, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f, dialect=csv.excel_tab)
        for row in reader:
            try:
                yield Template.parse_obj(row)
            except ValidationError as e:
                yield e


def main() -> None:
    ok: list[Template] = []
    notok: list[str] = []
    for obj in read_template("path/to/file.tsv"):
        if isinstance(obj, Template):
            ok.append(obj)
        else:
            notok.append(str(obj))
    ...

This seems to accomplish the same thing without the need for TemplateRow. If you have a good reason for that restriction, please elaborate.

Upvotes: 1

Paweł Rubin
Paweł Rubin

Reputation: 3439

Instead of a TypeGuard, you could actually define two types - ValidTemplateRow and InvalidTemplateRow. Then, simply use isinstance to check for either.


class ValidTemplateRow(tp.NamedTuple):
    template: Template


class InvalidTemplateRow(tp.NamedTuple):
    error: str


def read_template(filename: str | Path) -> tp.Iterator[InvalidTemplateRow | ValidTemplateRow]:
    """Read a TSV file into a list of templates.

    Args:
        filename (str): Path to the TSV file

    Yields:
        TemplateRow : parsed template row, or error message
    """
    with open(filename, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f, dialect=csv.excel_tab)
        for row in reader:
            try:
                yield ValidTemplateRow(template=Template.parse_obj(row))
            except pydantic.ValidationError as e:
                yield InvalidTemplateRow(error=str(e))


ok: list[Template] = []
notok: list[str] = []

for row in read_template("file"):
    if isinstance(row, ValidTemplateRow):
        ok.append(row.template)
    else:
        notok.append(row.error)

Upvotes: 2

Related Questions