I'm trying to annotate the following code.
The function designed to work when both zone and zones defined, or when file is defined (but not both):
def get_file(zone: str, zones: dict[str, str]) -> pathlib.Path:
pass
def connect(
zone: str | None = None,
zones: dict[str, str] | None = None,
file: pathlib.Path | None = None,
) -> bool:
file = file or get_file(zone, zones)
But it makes mypy angry -
1. Argument of type "str | None" cannot be assigned to parameter "zone" of type "str" in function "_get_vpn_file"
Type "str | None" cannot be assigned to type "str"
Type "None" cannot be assigned to type "str"
2. Argument of type "dict[str, str] | None" cannot be assigned to parameter "zones" of type "dict[str, str]" in function "_get_vpn_file"
Type "dict[str, str] | None" cannot be assigned to type "dict[str, str]"
Type "None" cannot be assigned to type "dict[str, str]"
Then I tried to make some aggressive type narrowing:
def _check_params_are_ok(
zone: str | None, zones: dict[str, str] | None, file: pathlib.Path | None,
) -> tuple[str, dict[str, str], None] | tuple[None, None, pathlib.Path]:
if zone is not None and file is not None:
raise ValueError("Pass `file` or `zone`, but not both.")
if zone is not None and zones is None:
raise ValueError("connect: Must define `zones` when `zone` is defined.")
if zone is None and file is None:
raise ValueError("connect: Must define `zone` or `file`.")
assert file is not None or (zone is not None and zones is not None)
# Type narrowing
if zone is not None and zones is not None and file is None:
return zone, zones, file
if zone is None and zones is None and file is not None:
return zone, zones, file
raise NotImplementedError("This error from _check_params_ok shouldn't happen.")
def connect(
zone: str | None = None,
zones: dict[str, str] | None = None,
file: pathlib.Path | None = None,
) -> bool:
zone, zones, file = _check_params_are_ok(zone, zones, file)
file = file or get_file(zone, zones)
And mypy still shows the same errors.
Mypy still shows the same errors even when adding very clear assertions:
zone, zones, file = _check_params_are_ok(zone, zones, file)
if file is None:
assert zone is not None and zones is not None
file = file or get_file(zone, zones)
The best solution I found so far is to cast the types inline, but it effects the code readability and make the line hard to read:
file = file or get_file(cast(str, zone), cast(dict[str, str], zones))
Is there any good way to narrow the types?
In your code when you do the — as you put it — very clear assertions, you can assign to
fileonly in the body of theifstatement:Then you will be very explicit and
mypyshouldn't complain anymore about this.