Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clarify the float/int/complex special case #1746

Open
carljm opened this issue May 23, 2024 · 5 comments · May be fixed by #1748
Open

Clarify the float/int/complex special case #1746

carljm opened this issue May 23, 2024 · 5 comments · May be fixed by #1748
Labels
topic: typing spec For improving the typing spec

Comments

@carljm
Copy link
Member

carljm commented May 23, 2024

The typing spec currently says this:

Python’s numeric types complex, float and int are not subtypes of each other, but to support common use cases, the type system contains a straightforward shortcut: when an argument is annotated as having type float, an argument of type int is acceptable; similar, for an argument annotated as having type complex, arguments of type float or int are acceptable.

This is helpful in that it clarifies that there is not a subtype relationship here. It remains unclear (to me, at least) in the following ways:

  1. It implies that this special case applies only to function argument annotations, and not to any other annotation (e.g. an annotation of the type of an attribute of a class). I find this surprising, but perhaps it is intended? I think it is surprising enough that if it's intended, the wording should be even clearer, and explicitly show examples where the special case does not apply outside an argument annotation.
  2. The wording "is acceptable" in the absence of a subtype relationship does not clarify how this special case actually fits into the type system; we can accept an int to a float typed argument, but then how do we type that name within the function body? If what we actually mean here is that float should be interpreted as float | int, then we should say that clearly.

There was some discussion of this on #1663, which is about the different but related question of whether to mention the numeric tower and PEP 3141.

@carljm carljm added the topic: documentation Documentation-related issues and PRs label May 23, 2024
@JelleZijlstra JelleZijlstra added the topic: typing spec For improving the typing spec label May 23, 2024
@carljm carljm removed the topic: documentation Documentation-related issues and PRs label May 23, 2024
@JelleZijlstra
Copy link
Member

We definitely need to change something here. The current wording implying that the special case only applies to parameter types does not describe the behavior of any current type checker, and I don't think it's a desirable behavior.

We already have a test case for this section, specialtypes_promotions.py:

v1: float = 1
v2: complex = 1.2
v2 = 1


def func1(f: float):
    f.numerator  # E

    if not isinstance(f, float):
        f.numerator  # OK

All type checkers tested pass this test, except pyre which fails to flag the f.numerator line.

Note the v1: float = 1 line, which at least implies that the int/float special case also applies to direct assignment.

However, different type checkers actually interpret this case differently. Pyre and mypy treat the body of the if not isinstance check as unreachable and therefore show no errors; pyright instead narrows the type of f to int within this check. Mypy/pyre's approach implies that int is a real subtype of float (which of course it isn't at runtime), though in some contexts mypy does know that it isn't (example: https://mypy-play.net/?mypy=latest&python=3.12&gist=2a0a984149707b4a770276dca5dcea5e).

I like the interpretation where the name float in an annotation is interpreted instead as the union float | int. This makes the special case into a purely syntactic rule applied when parsing type annotations, instead of a more fundamental special case in the type system. I believe pyright basically follows this interpretation, though it may not be quite this simple since its error messages show that it treats float and float | int as distinct types.

@erictraut
Copy link
Collaborator

erictraut commented May 23, 2024

I agree that this section should be clarified.

These are sometimes referred to as "promotion types".

Historically, bytes was also treated as a promotion type with implied subtypes of bytearray and memoryview, although this was never documented in any PEP or spec. Both mypy and pyright implement this behavior for bytes, although PEP 688 has eliminated its need. Pyright offers a way to disable the bytes promotion type using the "disableBytesTypePromotions" configuration option. I plan to disable this feature by default at some point in the future.

About a year ago, I changed pyright's handling of promotion types. I now treat float (when it appears in a type expression) as if it were float | int. Likewise, complex is treated as complex | float | int. I got the idea originally from this mypy issue, although mypy hasn't (yet) implemented it.

When compared to the more simplistic approach of always treating int as a subtype of float, this alternative approach more closely matches the runtime behavior when dealing with isinstance checks

def func1(f: float):
    if isinstance(f, float):
        reveal_type(f)  # pyright and mypy reveal "float"
    else:
        reveal_type(f)  # pyright reveals "int", mypy considers this unreachable

The downside to this approach is that pyright needs to internally distinguish between two variants of float and complex. One variant is the promotion type, the other is the "pure" type. There's unfortunately no way to spell the "pure" type, nor is there a way to tell the two apart in error messages.

@carljm
Copy link
Member Author

carljm commented May 23, 2024

I agree with all the above. Assuming other typing council members feel similarly, I think that clarifies enough to allow a pull request to the spec.

Side note: if we had intersection and negation type expressions, one could spell the "pure" float type as float & ~int. I wouldn't claim that's a nice way to spell it in an error message, though.

@hauntsaninja
Copy link
Collaborator

Yeah, I think KotlinIsland's union of ducklings approach better matches runtime behaviour than what mypy currently does

@JelleZijlstra
Copy link
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: typing spec For improving the typing spec
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants