1

Preamble

I'm using polars's write_excel method which has a parameter column_formats which wants a ColumnFormatDict that is defined here and below

ColumnFormatDict: TypeAlias = Mapping[
    # dict of colname(s) or selector(s) to format string or dict
    Union[ColumnNameOrSelector, tuple[ColumnNameOrSelector, ...]],
    Union[str, Mapping[str, str]],
]

ColumnNameOrSelector: TypeAlias = Union[str, SelectorType]

SelectorType: TypeAlias = "Selector"

If I use it like this:

df.write_excel(..., column_formats={"a":"#,##0;[Red]-#,##0"})

then there is no complaint from the type checker.

If I do:

column_formats = {"a":"#,##0;[Red]-#,##0"}
df.write_excel(..., column_formats=column_formats)

then it complains:

Argument of type "dict[str, str]" cannot be assigned to parameter "column_formats" of type "ColumnFormatDict | None" in function "write_excel"
  Type "dict[str, str]" is not assignable to type "ColumnFormatDict | None"
    "dict[str, str]" is not assignable to "Mapping[ColumnNameOrSelector | tuple[ColumnNameOrSelector, ...], str | Mapping[str, str]]"
      Type parameter "_KT@Mapping" is invariant, but "str" is not the same as "ColumnNameOrSelector | tuple[ColumnNameOrSelector, ...]"
    "dict[str, str]" is not assignable to "None"

where the relevant line is: Type parameter "_KT@Mapping" is invariant, but "str" is not the same as "ColumnNameOrSelector | tuple[ColumnNameOrSelector, ...]"

I discovered that if I annotate my variable as

column_formats:dict[str|cs.Selector|tuple[str|cs.Selector], str]={"a":"abc"}

then it won't complain but the parameter is meant to be flexible and its intended flexibility is seemingly making it less flexible.

Questions:

  1. Why, in the first case, is a str a valid ColumnNameOrSelector | tuple[ColumnNameOrSelector, ...] but in the second case it isn't?

It's interesting that it is only complaining about the key and not the value portion of the Mapping, in other words I didn't have to annotate it as dict[str|cs.Selector|tuple[str|cs.Selector], str|dict[str,str]]

  1. Additionally, suppose I want to put in a PR to amend ColumnNameOrSelector is there a way to define it such that my second usage doesn't generate a type warning without an explicit annotation?

Assuming it can be done, I'm guessing there needs to be a mapping for every key possibility so maybe this (or maybe even that last tuple possibility needs to be split up to a 4th Mapping case.

ColumnFormatDict: TypeAlias = Union[
    Mapping[str, Union[str, Mapping[str, str]]],
    Mapping[SelectorType, Union[str, Mapping[str, str]]],
    Mapping[tuple[str|SelectorType, ...], Union[str, Mapping[str, str]]],
    ]
  1. Is that guess on the right track or way off?
3
  • In the inline case there is outer context, so {"a": "..."} is inferred as Mapping[ColumnNameOrSelector, Something]. If you define a separate variable, its type is inferred on the spot, and it is obviously inferred as dict[str, str]. Mapping is invariant in key type, so dict[str, V] is not assignable to Mapping[SomeSuperTypeOfStr, V] for any V. Commented Oct 24 at 16:17
  • I want to put in a PR to amend ColumnNameOrSelector - it doesn't depend on the definition, unless you want to make it a ColumnNameOrSelector: TypeAlias = str. Commented Oct 24 at 16:20
  • I'd recommend just writing column_formats: ColumnFormatDIct = {'a': '...'} (after all, that's why that type alias is public) if you want to store it in a variable, and using inline dict if you don't need to reuse that dict. Commented Oct 24 at 16:26

2 Answers 2

1

If I had to guess, I'd say reusability?

In your first example, the argument is not reusable. The type checker knows that the dictionary will never be used for anything except this function, so it's safe to assign it any type that is consistent with its value. That includes ColumnFormatDict.

In the second example, the type checker can't make that assumption. You might need the dictionary again, so it's not okay for the type checker to infer some wacky type for the keys like Union[ColumnNameOrSelector, tuple[ColumnNameOrSelector, ...]]. Instead, it infers that the dictionary is of type dict[str, str] using its regular procedures at assignment. Since Mapping keys are invariant, trying to pass column_formats to the function fails.

Sign up to request clarification or add additional context in comments.

Comments

0
  1. In the first case it isn't that str is a valid ColumnNameOrSelector | tuple[ColumnNameOrSelector, ...] and in the second case it isn't. It's that when we make the assignment column_formats = {"a":"#,##0;[Red]-#,##0"} the typer is inferring the most narrow definition it can which is a dict[str,str]. When we do df.write_excel(..., column_formats={"a":"#,##0;[Red]-#,##0"}) the typer knows that our parameter should be a ColumnFormatDict and at the same time, we're presenting it with {"a":"#,##0;[Red]-#,##0"} it will check if that fits. It is basically a design choice that it doesn't revalidate if it is a subtype.

  2. The only way the typer will accept dict[str,str] (or anything else) is if it is given a Union of Mappings rather than a Mapping[Union[keys], Union[values]]. For that to work, it needs every possible combination of keys and strings without any unions. It wouldn't be enough just to get [str,str] and [SelectorType, str], it'd also need [str|SelectorType, str], just to start. Every combination would be this:

 Union[
    Mapping[str, str],
    Mapping[str, Mapping[str, str]],
    Mapping[str, str | Mapping[str, str]],
    Mapping[SelectorType, str],
    Mapping[SelectorType, Mapping[str, str]],
    Mapping[SelectorType, str | Mapping[str, str]],
    Mapping[tuple[str, ...], str],
    Mapping[tuple[str, ...], Mapping[str, str]],
    Mapping[tuple[str, ...], str | Mapping[str, str]],
    Mapping[tuple[SelectorType, ...], str],
    Mapping[tuple[SelectorType, ...], Mapping[str, str]],
    Mapping[tuple[SelectorType, ...], str | Mapping[str, str]],
    Mapping[str | SelectorType, str],
    Mapping[str | SelectorType, Mapping[str, str]],
    Mapping[str | SelectorType, str | Mapping[str, str]],
    Mapping[str | tuple[str, ...], str],
    Mapping[str | tuple[str, ...], Mapping[str, str]],
    Mapping[str | tuple[str, ...], str | Mapping[str, str]],
    Mapping[str | tuple[SelectorType, ...], str],
    Mapping[str | tuple[SelectorType, ...], Mapping[str, str]],
    Mapping[str | tuple[SelectorType, ...], str | Mapping[str, str]],
    Mapping[SelectorType | tuple[str, ...], str],
    Mapping[SelectorType | tuple[str, ...], Mapping[str, str]],
    Mapping[SelectorType | tuple[str, ...], str | Mapping[str, str]],
    Mapping[SelectorType | tuple[SelectorType, ...], str],
    Mapping[SelectorType | tuple[SelectorType, ...], Mapping[str, str]],
    Mapping[SelectorType | tuple[SelectorType, ...], str | Mapping[str, str]],
    Mapping[tuple[str, ...] | tuple[SelectorType, ...], str],
    Mapping[tuple[str, ...] | tuple[SelectorType, ...], Mapping[str, str]],
    Mapping[tuple[str, ...] | tuple[SelectorType, ...], str | Mapping[str, str]],
    Mapping[str | SelectorType | tuple[str, ...], str],
    Mapping[str | SelectorType | tuple[str, ...], Mapping[str, str]],
    Mapping[str | SelectorType | tuple[str, ...], str | Mapping[str, str]],
    Mapping[str | SelectorType | tuple[SelectorType, ...], str],
    Mapping[str | SelectorType | tuple[SelectorType, ...], Mapping[str, str]],
    Mapping[str | SelectorType | tuple[SelectorType, ...], str | Mapping[str, str]],
    Mapping[str | tuple[str, ...] | tuple[SelectorType, ...], str],
    Mapping[str | tuple[str, ...] | tuple[SelectorType, ...], Mapping[str, str]],
    Mapping[str | tuple[str, ...] | tuple[SelectorType, ...], str | Mapping[str, str]],
    Mapping[SelectorType | tuple[str, ...] | tuple[SelectorType, ...], str],
    Mapping[
        SelectorType | tuple[str, ...] | tuple[SelectorType, ...], Mapping[str, str]
    ],
    Mapping[
        SelectorType | tuple[str, ...] | tuple[SelectorType, ...],
        str | Mapping[str, str],
    ],
    Mapping[str | SelectorType | tuple[str, ...] | tuple[SelectorType, ...], str],
    Mapping[
        str | SelectorType | tuple[str, ...] | tuple[SelectorType, ...],
        Mapping[str, str],
    ],
    Mapping[
        str | SelectorType | tuple[str, ...] | tuple[SelectorType, ...],
        str | Mapping[str, str],
    ],
]

The above is probably going to be too unwieldy to use though.

work around without annotations

One way to prevent the typing warning without annotating the variable is to unpack it as a new dict, ie df.write_excel(..., column_formats={**column_formats})

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.