Skip to content

Specify behavior for Literal? types. #19625

Description

@randolf-scholz

Mypy's Literal? types are briefly documented here, however, their behavior is not completely specified.

Intuitively, a value like Literal["x"]? means that, depending on context, the value could be Literal["x"] or a plain str. This makes Literal["x"]? effectively a gradual type. I noticed some bugs with how mypy treats these types, most notably #19560. To holistically address such issues, I propose the following concrete specification:

0. prelim

Within this post, I interpret

  • / mypy.subtypes.is_subtype as checking "assignable" / "consistent subtyping"
  • <: / mypy.subtypes.is_proper_subtype as checking "(proper) subtyping"

If this is incorrect, please let me know.

1. AnyOf specification

We define:

  1. x <: AnyOf[y₁, ..., yₙ] if and only if x <: Union[y₁, ..., yₙ]
  2. x ≲ AnyOf[y₁, ..., yₙ] if and only if x ≲ yₖ for some k.
  3. AnyOf[y₁, ...., yₙ] <: x if and only if Union[y₁, ..., yₙ] <: x
  4. AnyOf[y₁, ...., yₙ] ≲ x if and only if yₖ ≲ x for all k.

2. Definition of Literal[T]?

Literal[T]? denotes a gradual type equivalent to one of

  1. AnyOf[T, str] if T is a literal string
  2. AnyOf[T, int] if T is a literal integer
  3. AnyOf[T, bool] if T is a literal boolean
  4. AnyOf[T, NoneType] if T is a lite
  5. AnyOf[T, EnumType] if T is a literal enum

the first member is called its value, the latter its fallback.

It satisfies the following rules:

  1. Literal[T]? is assignable to both its fallback and value.
  2. Both its fallback and value are assignable to Literal[T]?.
  3. Literal[T]? is a (proper) subtype of its fallback.
  4. Its value is a proper subtype of Literal[T]?.
  5. Literal[T]? is not a (proper) subtype of its value.
  6. Its fallback is not a (proper) subtype of Literal[T]?.

Which (I think) makes Literal["x"]? essentially AnyOf[Literal["x"], str] (see python/typing#566)

Rationale:

  • ① and ②: should be obvious.
  • ③ and ④: is because in either case, when Literal["x"]? refers to Literal["x"] or when it refers to str,
    both times Literal["x"] is a proper subtype, and str a proper supertype.
  • ⑤ and ⑥: are because the proper subtyping is not satisfied for both possible choices.

2. Changes to basic Ops

Below is a list of changes that derive from these rules and the assumption that meet and join are symmetric. These were generated using the following test script:

Details
frommypy.meetimportmeet_typesfrommypy.joinimportjoin_typesfrommypy.nodesimportBlock, ClassDef, SymbolTable, TypeInfofrommypy.subtypesimportrestrict_subtype_awayasrestrict_types, is_proper_subtype, is_subtype, is_same_typefrommypy.typesimportInstance, LiteralType, TypeOfAny, AnyType, UnionTypefrommypy.typeopsimportmake_simplified_unionasunionST=SymbolTable()
defmake_typeinfo(name: str, module_name: str="__main__") ->TypeInfo:
class_def=ClassDef(name, Block([])) # Create a dummy ClassDefinfo=TypeInfo(ST, class_def, module_name)
class_def.info=info# circular referencereturninfo# Create demo typesstr_info=make_typeinfo("str", module_name="builtins")
str_info.mro= [str_info] # Simplify MRO for this examplestr_type=Instance(str_info, [], last_known_value=None)
max_litr=LiteralType("x", fallback=str_type)
sum_litr=LiteralType("y", fallback=str_type)
sum_inst=Instance(str_info, [], last_known_value=sum_litr)
max_inst=Instance(str_info, [], last_known_value=max_litr)
any_type=AnyType(TypeOfAny.unannotated)
print(f"\nMEET SCHEMA")
print(f"meet_types({max_litr!s:<16}, {str_type!s:<16}) = {meet_types(max_litr, str_type)}")
print(f"meet_types({str_type!s:<16}, {max_litr!s:<16}) = {meet_types(str_type, max_litr)}")
print(f"meet_types({max_inst!s:<16}, {str_type!s:<16}) = {meet_types(max_inst, str_type)}")
print(f"meet_types({str_type!s:<16}, {max_inst!s:<16}) = {meet_types(str_type, max_inst)}")
print(f"meet_types({max_litr!s:<16}, {max_litr!s:<16}) = {meet_types(max_litr, max_litr)}")
print(f"meet_types({max_litr!s:<16}, {max_inst!s:<16}) = {meet_types(max_litr, max_inst)}")
print(f"meet_types({max_inst!s:<16}, {max_litr!s:<16}) = {meet_types(max_inst, max_litr)}")
print(f"meet_types({max_inst!s:<16}, {max_inst!s:<16}) = {meet_types(max_inst, max_inst)}")
print(f"meet_types({max_litr!s:<16}, {sum_litr!s:<16}) = {meet_types(max_litr, sum_litr)}")
print(f"meet_types({max_litr!s:<16}, {sum_inst!s:<16}) = {meet_types(max_litr, sum_inst)}")
print(f"meet_types({max_inst!s:<16}, {sum_litr!s:<16}) = {meet_types(max_inst, sum_litr)}")
print(f"meet_types({max_inst!s:<16}, {sum_inst!s:<16}) = {meet_types(max_inst, sum_inst)}")
print(f"\nJOIN SCHEMA")
print(f"join_types({max_litr!s:<16}, {str_type!s:<16}) = {join_types(max_litr, str_type)}")
print(f"join_types({str_type!s:<16}, {max_litr!s:<16}) = {join_types(str_type, max_litr)}")
print(f"join_types({max_inst!s:<16}, {str_type!s:<16}) = {join_types(max_inst, str_type)}")
print(f"join_types({str_type!s:<16}, {max_inst!s:<16}) = {join_types(str_type, max_inst)}")
print(f"join_types({max_litr!s:<16}, {max_litr!s:<16}) = {join_types(max_litr, max_litr)}")
print(f"join_types({max_litr!s:<16}, {max_inst!s:<16}) = {join_types(max_litr, max_inst)}")
print(f"join_types({max_inst!s:<16}, {max_litr!s:<16}) = {join_types(max_inst, max_litr)}")
print(f"join_types({max_inst!s:<16}, {max_inst!s:<16}) = {join_types(max_inst, max_inst)}")
print(f"join_types({max_litr!s:<16}, {sum_litr!s:<16}) = {join_types(max_litr, sum_litr)}")
print(f"join_types({max_litr!s:<16}, {sum_inst!s:<16}) = {join_types(max_litr, sum_inst)}")
print(f"join_types({max_inst!s:<16}, {sum_litr!s:<16}) = {join_types(max_inst, sum_litr)}")
print(f"join_types({max_inst!s:<16}, {sum_inst!s:<16}) = {join_types(max_inst, sum_inst)}")
print(f"\nUNION SCHEMA")
print(f"union({max_litr!s:<16}, {str_type!s:<16}) = {union([max_litr, str_type])}")
print(f"union({str_type!s:<16}, {max_litr!s:<16}) = {union([str_type, max_litr])}")
print(f"union({max_inst!s:<16}, {str_type!s:<16}) = {union([max_inst, str_type])}")
print(f"union({str_type!s:<16}, {max_inst!s:<16}) = {union([str_type, max_inst])}")
print(f"union({max_litr!s:<16}, {max_litr!s:<16}) = {union([max_litr, max_litr])}")
print(f"union({max_litr!s:<16}, {max_inst!s:<16}) = {union([max_litr, max_inst])}")
print(f"union({max_inst!s:<16}, {max_litr!s:<16}) = {union([max_inst, max_litr])}")
print(f"union({max_inst!s:<16}, {max_inst!s:<16}) = {union([max_inst, max_inst])}")
print(f"union({max_litr!s:<16}, {sum_litr!s:<16}) = {union([max_litr, sum_litr])}")
print(f"union({max_litr!s:<16}, {sum_inst!s:<16}) = {union([max_litr, sum_inst])}")
print(f"union({max_inst!s:<16}, {sum_litr!s:<16}) = {union([max_inst, sum_litr])}")
print(f"union({max_inst!s:<16}, {sum_inst!s:<16}) = {union([max_inst, sum_inst])}")
print(f"\nRESTRICT SCHEMA")
print(f"restrict_types({max_litr!s:<16}, {str_type!s:<16}) = {restrict_types(max_litr, str_type)}")
print(f"restrict_types({str_type!s:<16}, {max_litr!s:<16}) = {restrict_types(str_type, max_litr)}")
print(f"restrict_types({max_inst!s:<16}, {str_type!s:<16}) = {restrict_types(max_inst, str_type)}")
print(f"restrict_types({str_type!s:<16}, {max_inst!s:<16}) = {restrict_types(str_type, max_inst)}")
print(f"restrict_types({max_litr!s:<16}, {max_litr!s:<16}) = {restrict_types(max_litr, max_litr)}")
print(f"restrict_types({max_litr!s:<16}, {max_inst!s:<16}) = {restrict_types(max_litr, max_inst)}")
print(f"restrict_types({max_inst!s:<16}, {max_litr!s:<16}) = {restrict_types(max_inst, max_litr)}")
print(f"restrict_types({max_inst!s:<16}, {max_inst!s:<16}) = {restrict_types(max_inst, max_inst)}")
print(f"restrict_types({max_litr!s:<16}, {sum_litr!s:<16}) = {restrict_types(max_litr, sum_litr)}")
print(f"restrict_types({max_litr!s:<16}, {sum_inst!s:<16}) = {restrict_types(max_litr, sum_inst)}")
print(f"restrict_types({max_inst!s:<16}, {sum_litr!s:<16}) = {restrict_types(max_inst, sum_litr)}")
print(f"restrict_types({max_inst!s:<16}, {sum_inst!s:<16}) = {restrict_types(max_inst, sum_inst)}")
print(f"\n SUBTYPE SCHEMA")
print(f"is_subtype({max_litr!s:<16}, {str_type!s:<16}) = {is_subtype(max_litr, str_type)}")
print(f"is_subtype({str_type!s:<16}, {max_litr!s:<16}) = {is_subtype(str_type, max_litr)}")
print(f"is_subtype({max_inst!s:<16}, {str_type!s:<16}) = {is_subtype(max_inst, str_type)}")
print(f"is_subtype({str_type!s:<16}, {max_inst!s:<16}) = {is_subtype(str_type, max_inst)}")
print(f"is_subtype({max_litr!s:<16}, {max_litr!s:<16}) = {is_subtype(max_litr, max_litr)}")
print(f"is_subtype({max_litr!s:<16}, {max_inst!s:<16}) = {is_subtype(max_litr, max_inst)}")
print(f"is_subtype({max_inst!s:<16}, {max_litr!s:<16}) = {is_subtype(max_inst, max_litr)}")
print(f"is_subtype({max_inst!s:<16}, {max_inst!s:<16}) = {is_subtype(max_inst, max_inst)}")
print(f"is_subtype({max_litr!s:<16}, {sum_litr!s:<16}) = {is_subtype(max_litr, sum_litr)}")
print(f"is_subtype({max_litr!s:<16}, {sum_inst!s:<16}) = {is_subtype(max_litr, sum_inst)}")
print(f"is_subtype({max_inst!s:<16}, {sum_litr!s:<16}) = {is_subtype(max_inst, sum_litr)}")
print(f"is_subtype({max_inst!s:<16}, {sum_inst!s:<16}) = {is_subtype(max_inst, sum_inst)}")
print(f"\n PROPER SUBTYPE SCHEMA")
print(f"is_proper_subtype({max_litr!s:<16}, {str_type!s:<16}) = {is_proper_subtype(max_litr, str_type)}")
print(f"is_proper_subtype({str_type!s:<16}, {max_litr!s:<16}) = {is_proper_subtype(str_type, max_litr)}")
print(f"is_proper_subtype({max_inst!s:<16}, {str_type!s:<16}) = {is_proper_subtype(max_inst, str_type)}")
print(f"is_proper_subtype({str_type!s:<16}, {max_inst!s:<16}) = {is_proper_subtype(str_type, max_inst)}")
print(f"is_proper_subtype({max_litr!s:<16}, {max_litr!s:<16}) = {is_proper_subtype(max_litr, max_litr)}")
print(f"is_proper_subtype({max_litr!s:<16}, {max_inst!s:<16}) = {is_proper_subtype(max_litr, max_inst)}")
print(f"is_proper_subtype({max_inst!s:<16}, {max_litr!s:<16}) = {is_proper_subtype(max_inst, max_litr)}")
print(f"is_proper_subtype({max_inst!s:<16}, {max_inst!s:<16}) = {is_proper_subtype(max_inst, max_inst)}")
print(f"is_proper_subtype({max_litr!s:<16}, {sum_litr!s:<16}) = {is_proper_subtype(max_litr, sum_litr)}")
print(f"is_proper_subtype({max_litr!s:<16}, {sum_inst!s:<16}) = {is_proper_subtype(max_litr, sum_inst)}")
print(f"is_proper_subtype({max_inst!s:<16}, {sum_litr!s:<16}) = {is_proper_subtype(max_inst, sum_litr)}")
print(f"is_proper_subtype({max_inst!s:<16}, {sum_inst!s:<16}) = {is_proper_subtype(max_inst, sum_inst)}")
print(f"\n SAME TYPE SCHEMA")
print(f"is_same_type({max_litr!s:<16}, {str_type!s:<16}) = {is_same_type(max_litr, str_type)}")
print(f"is_same_type({str_type!s:<16}, {max_litr!s:<16}) = {is_same_type(str_type, max_litr)}")
print(f"is_same_type({max_inst!s:<16}, {str_type!s:<16}) = {is_same_type(max_inst, str_type)}")
print(f"is_same_type({str_type!s:<16}, {max_inst!s:<16}) = {is_same_type(str_type, max_inst)}")
print(f"is_same_type({max_litr!s:<16}, {max_litr!s:<16}) = {is_same_type(max_litr, max_litr)}")
print(f"is_same_type({max_litr!s:<16}, {max_inst!s:<16}) = {is_same_type(max_litr, max_inst)}")
print(f"is_same_type({max_inst!s:<16}, {max_litr!s:<16}) = {is_same_type(max_inst, max_litr)}")
print(f"is_same_type({max_inst!s:<16}, {max_inst!s:<16}) = {is_same_type(max_inst, max_inst)}")
print(f"is_same_type({max_litr!s:<16}, {sum_litr!s:<16}) = {is_same_type(max_litr, sum_litr)}")
print(f"is_same_type({max_litr!s:<16}, {sum_inst!s:<16}) = {is_same_type(max_litr, sum_inst)}")
print(f"is_same_type({max_inst!s:<16}, {sum_litr!s:<16}) = {is_same_type(max_inst, sum_litr)}")
print(f"is_same_type({max_inst!s:<16}, {sum_inst!s:<16}) = {is_same_type(max_inst, sum_inst)}")

The feature results are based on commit 8dbc066 of PR #19605

MEET SCHEMA

leftrightmasterfeaturechanged
"x"str"x""x"
str"x""x""x"
"x"?str"x"?"x"?
str"x"?str"x"?⚠️
"x""x""x""x"
"x""x"?"x""x"
"x"?"x""x"?"x"⚠️
"x"?"x"?"x"?"x"?
"x""y"NeverNever
"x""y"?"x"Never⚠️
"x"?"y""y"Never⚠️
"x"?"y"?"x"?str⚠️

JOIN SCHEMA

leftrightmasterfeaturechanged
"x"strstrstr
str"x"strstr
"x"?strstrstr
str"x"?strstr
"x""x""x""x"
"x""x"?"x""x"?⚠️
"x"?"x""x""x"?⚠️
"x"?"x"?str"x"?⚠️
"x""y"strstr
"x""y"?strstr
"x"?"y"strstr
"x"?"y"?strstr

UNION SCHEMA

leftrightmasterfeaturechanged
"x"strstrstr
str"x"strstr
"x"?strstrstr
str"x"?strstr
"x""x""x""x"
"x""x"?"x""x"?⚠️
"x"?"x""x""x"?⚠️
"x"?"x"?"x"?"x"?
"x""y""x" | "y""x" | "y"
"x""y"?"x" | "y"?"x" | "y"?
"x"?"y""x"? | "y""x"? | "y"
"x"?"y"?"x"? | "y"?"x"? | "y"?

RESTRICT SCHEMA

leftrightmasterfeaturechanged
"x"strNeverNever
str"x"strstr
"x"?strNeverNever
str"x"?NeverNever
"x""x"NeverNever
"x""x"?NeverNever
"x"?"x""x"?Never⚠️
"x"?"x"?NeverNever
"x""y""x""x"
"x""y"?NeverNever
"x"?"y""x"?"x"?
"x"?"y"?NeverNever

SUBTYPE SCHEMA

leftrightmasterfeaturechanged
"x"strTrueTrue
str"x"FalseFalse
"x"?strTrueTrue
str"x"?TrueTrue
"x""x"TrueTrue
"x""x"?TrueTrue
"x"?"x"TrueTrue
"x"?"x"?TrueTrue
"x""y"FalseFalse
"x""y"?TrueTrue
"x"?"y"FalseFalse
"x"?"y"?TrueTrue

PROPER SUBTYPE SCHEMA

leftrightmasterfeaturechanged
"x"strTrueTrue
str"x"FalseFalse
"x"?strTrueTrue
str"x"?TrueFalse⚠️
"x""x"TrueTrue
"x""x"?TrueTrue
"x"?"x"TrueFalse⚠️
"x"?"x"?TrueTrue
"x""y"FalseFalse
"x""y"?TrueFalse⚠️
"x"?"y"FalseFalse
"x"?"y"?TrueFalse⚠️

SAME TYPE SCHEMA

leftrightmasterfeaturechanged
"x"strFalseFalse
str"x"FalseFalse
"x"?strTrueFalse⚠️
str"x"?TrueFalse⚠️
"x""x"TrueTrue
"x""x"?TrueFalse⚠️
"x"?"x"TrueFalse⚠️
"x"?"x"?TrueTrue
"x""y"FalseFalse
"x""y"?FalseFalse
"x"?"y"FalseFalse
"x"?"y"?TrueTrue

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions