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

new "type system concepts" section #1743

Merged
merged 48 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b323afb
initial draft of new concepts section
carljm May 22, 2024
bd4416e
review feedback
carljm May 22, 2024
3c332bf
prefer 'assignable to' over 'consistent subtype of'
carljm May 22, 2024
6603f43
minor tweaks
carljm May 22, 2024
4e2a1ce
Merge branch 'main' into concepts
carljm May 22, 2024
137ab2a
terms have types
carljm May 22, 2024
24e6ee5
review comments
carljm May 22, 2024
9e9ebb4
use asymmetric example of assignable-to
carljm May 22, 2024
1271558
Any is equivalent to Any
carljm May 22, 2024
2ddd4ad
add a short para on the gradual guarantee
carljm May 22, 2024
971e9f2
conciseness tweak
carljm May 22, 2024
10fda4e
use 'assignable to' in Any description
carljm May 22, 2024
f4110bb
incorporate some of Eric Traut's feedback
carljm May 23, 2024
ac6273e
more on union
carljm May 24, 2024
15c0d56
add an example of bounded gradual type
carljm May 24, 2024
efdbc5f
add section on attributes and methods
carljm May 24, 2024
e4537ac
more feedback and tweaks
carljm May 26, 2024
c3bbb52
Merge branch 'main' into concepts
carljm Jun 1, 2024
6bebab4
review comments
carljm Jun 1, 2024
2900cce
a bit more on gradual unions
carljm Jun 1, 2024
182a058
a few more review comments
carljm Jun 1, 2024
351136e
add terms to glossary
carljm Jun 1, 2024
cd03de8
Update glossary.rst
carljm Jun 2, 2024
07941d7
Update glossary.rst
carljm Jun 2, 2024
c55d40a
review comments on glossary
carljm Jun 2, 2024
b1775b1
re-apply review comment
carljm Jun 2, 2024
1a71a72
Apply suggestions from code review
carljm Jun 2, 2024
c18d9e1
audit remainder of type spec for terminology usage
carljm Jun 2, 2024
cca3bcd
review comments
carljm Jun 2, 2024
3d6b406
some review comments
carljm Jun 2, 2024
5d40036
one more review tweak on protocol wording
carljm Jun 2, 2024
1e0d118
Merge branch 'main' into concepts
carljm Jun 12, 2024
cbe6e23
add equivalent, narrow, and wide to glossary
carljm Jun 12, 2024
8954595
add table of type relations
carljm Jun 12, 2024
efaa7ab
explicitly allow inference on missing function annotations
carljm Jun 12, 2024
12ae9eb
some review comments
carljm Jun 12, 2024
ccfef86
Update docs/spec/callables.rst
carljm Jun 12, 2024
e990bda
more review comments
carljm Jun 12, 2024
045d7c2
link 'assignable' to glossary more often in callables doc
carljm Jun 14, 2024
dd6ffd1
add nominal/structural to concepts and glossary
carljm Jun 14, 2024
673a467
don't use 'compatible' in callables doc
carljm Jun 14, 2024
0f5fba4
equivalence of gradual types and union simplification
carljm Jun 14, 2024
a6b3ab0
simplify description of structural subtyping
carljm Jun 15, 2024
e5943a4
define equivalence of gradual types in glossary
carljm Jun 15, 2024
effcdec
more review comments
carljm Jun 17, 2024
9ebaef8
review comments
carljm Jun 19, 2024
6a3b716
Update glossary.rst
carljm Jun 20, 2024
ab7f9ac
review comments
carljm Jun 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions docs/spec/annotations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ hinting is used by filling function annotation slots with classes::
This states that the expected type of the ``name`` argument is
``str``. Analogically, the expected return type is ``str``.

Expressions whose type is a subtype of a specific argument type are
also accepted for that argument.
Expressions whose type is :term:`assignable` to a specific argument type are
also accepted for that argument. Similarly, an expression whose type is
assignable to the annotated return type can be returned from the function.

.. _`missing-annotations`:

Any function without annotations should be treated as having the most
general type possible, or ignored, by any type checker.
Any function without annotations should be treated as having :ref:`Any`
carljm marked this conversation as resolved.
Show resolved Hide resolved
annotations on all arguments and the return type.

Type checkers may choose to entirely ignore (not type check) the bodies of
functions with no annotations, but this behavior is not required.

It is recommended but not required that checked functions have
annotations for all arguments and the return type. For a checked
Expand Down
135 changes: 67 additions & 68 deletions docs/spec/callables.rst
carljm marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ may be given as an ellipsis. For example::
def func(x: AnyStr, y: AnyStr = ...) -> AnyStr: ...

If a non-ellipsis default value is present and its type can be statically
evaluated, a type checker should verify that this type is compatible with the
declared parameter's type::
evaluated, a type checker should verify that this type is :term:`assignable` to
the declared parameter's type::

def func(x: int = 0): ... # OK
def func(x: int | None = None): ... # OK
Expand Down Expand Up @@ -282,8 +282,8 @@ unpacked in the destination callable invocation::
dest = src # WRONG!
dest(**animal) # Fails at runtime.

Similar situation can happen even without inheritance as compatibility
between ``TypedDict``\s is based on structural subtyping.
A similar situation can happen even without inheritance as :term:`assignability
<assignable>` between ``TypedDict``\s is structural.

Source contains untyped ``**kwargs``
""""""""""""""""""""""""""""""""""""
Expand Down Expand Up @@ -423,11 +423,9 @@ the section on `Callback protocols`_.
Meaning of ``...`` in ``Callable``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The ``Callable`` special form supports the use of ``...`` in place of the
list of parameter types. This indicates that the type is consistent with
any input signature. Just as ``Any`` means "any conceivable type that could be
compatible", ``(...)`` means "any conceivable set of parameters that could be
compatible"::
The ``Callable`` special form supports the use of ``...`` in place of the list
of parameter types. This is a :term:`gradual form` indicating that the type is
:term:`consistent` with any input signature::

cb1: Callable[..., str]
cb1 = lambda x: str(x) # OK
Expand Down Expand Up @@ -475,7 +473,7 @@ and are retained as part of the signature::
pass

class B(A):
# This override is OK because it is consistent with the parent's method.
# This override is OK because it is assignable to the parent's method.
def method(self, a: float, /, b: int, *, k: str, m: str) -> None:
pass

Expand All @@ -490,7 +488,7 @@ For example::
f: Callback[...] = cb # OK

If ``...`` is used with signature concatenation, the ``...`` portion continues
to mean "any conceivable set of parameters that could be compatible"::
to be :term:`consistent` with any input parameters::

type CallbackWithInt[**P] = Callable[Concatenate[int, P], str]
type CallbackWithStr[**P] = Callable[Concatenate[str, P], str]
Expand Down Expand Up @@ -522,31 +520,31 @@ and overloads. They can be defined as protocols with a ``__call__`` member::
...

comb: Combiner = good_cb # OK
comb = bad_cb # Error! Argument 2 has incompatible type because of
comb = bad_cb # Error! Argument 2 is not assignable because of
# different parameter name and kind in the callback

Callback protocols and ``Callable[...]`` types can generally be used
interchangeably.


Subtyping rules for callables
-----------------------------
Assignability rules for callables
---------------------------------

A callable type ``A`` is a subtype of callable type ``B`` if the return type
of ``A`` is a subtype of the return type of ``B`` and the input signature
of ``A`` accepts all possible combinations of arguments that the input
signature of ``B`` accepts. All of the specific subtyping rules described below
derive from this general rule.
A callable type ``B`` is :term:`assignable` to callable type ``A`` if the
carljm marked this conversation as resolved.
Show resolved Hide resolved
return type of ``B`` is assignable to the return type of ``A`` and the input
signature of ``B`` accepts all possible combinations of arguments that the
input signature of ``A`` accepts. All of the specific assignability rules
carljm marked this conversation as resolved.
Show resolved Hide resolved
described below derive from this general rule.


Parameter types
^^^^^^^^^^^^^^^

Callable types are covariant with respect to their return types but
contravariant with respect to their parameter types. This means a callable
``A`` is a subtype of callable ``B`` if the types of the parameters of
``B`` are subtypes of the parameters of ``A``. For example,
``(x: float) -> int`` is a subtype of ``(x: int) -> float``::
``B`` is assignable to callable ``A`` if the types of the parameters of
carljm marked this conversation as resolved.
Show resolved Hide resolved
``A`` are assignable to the parameters of ``B``. For example,
``(x: float) -> int`` is assignable to ``(x: int) -> float``::

def func(cb: Callable[[float], int]):
f1: Callable[[int], float] = cb # OK
Expand All @@ -555,11 +553,11 @@ contravariant with respect to their parameter types. This means a callable
Parameter kinds
^^^^^^^^^^^^^^^

Callable ``A`` is a subtype of callable ``B`` if all keyword-only parameters
in ``B`` are present in ``A`` as either keyword-only parameters or standard
(positional or keyword) parameters. For example, ``(a: int) -> None`` is a
subtype of ``(*, a: int) -> None``, but the converse is not true. The order
of keyword-only parameters is ignored for purposes of subtyping::
Callable ``B`` is assignable to callable ``A`` only if all keyword-only
parameters in ``A`` are present in ``B`` as either keyword-only parameters or
standard (positional or keyword) parameters. For example, ``(a: int) -> None``
is assignable to ``(*, a: int) -> None``, but the converse is not true. The
order of keyword-only parameters is ignored for purposes of assignability::

class KwOnly(Protocol):
def __call__(self, *, b: int, a: int) -> None: ...
Expand All @@ -571,10 +569,10 @@ of keyword-only parameters is ignored for purposes of subtyping::
f1: KwOnly = standard # OK
f2: Standard = kw_only # Error

Likewise, callable ``A`` is a subtype of callable ``B`` if all positional-only
parameters in ``B`` are present in ``A`` as either positional-only parameters
or standard (positional or keyword) parameters. The names of positional-only
parameters are ignored for purposes of subtyping::
Likewise, callable ``B`` is assignable to callable ``A`` only if all
positional-only parameters in ``A`` are present in ``B`` as either
positional-only parameters or standard (positional or keyword) parameters. The
carljm marked this conversation as resolved.
Show resolved Hide resolved
names of positional-only parameters are ignored for purposes of assignability::

class PosOnly(Protocol):
def __call__(self, not_a: int, /) -> None: ...
Expand All @@ -590,9 +588,9 @@ parameters are ignored for purposes of subtyping::
``*args`` parameters
^^^^^^^^^^^^^^^^^^^^

If a callable ``B`` has a signature with a ``*args`` parameter, callable ``A``
must also have a ``*args`` parameter to be a subtype of ``B``, and the type of
``B``'s ``*args`` parameter must be a subtype of ``A``'s ``*args`` parameter::
If a callable ``A`` has a signature with a ``*args`` parameter, callable ``B``
must also have a ``*args`` parameter to be assignable to ``A``, and the type of
``A``'s ``*args`` parameter must be assignable to ``B``'s ``*args`` parameter::

class NoArgs(Protocol):
def __call__(self) -> None: ...
Expand All @@ -611,12 +609,12 @@ must also have a ``*args`` parameter to be a subtype of ``B``, and the type of
f4: IntArgs = float_args # OK

f5: FloatArgs = no_args # Error: missing *args parameter
f6: FloatArgs = int_args # Error: float is not subtype of int
f6: FloatArgs = int_args # Error: float is not assignable to int

If a callable ``B`` has a signature with one or more positional-only parameters,
a callable ``A`` is a subtype of ``B`` if ``A`` has an ``*args`` parameter whose
type is a supertype of the types of any otherwise-unmatched positional-only
parameters in ``B``::
If a callable ``A`` has a signature with one or more positional-only
parameters, a callable ``B`` is assignable to ``A`` only if ``B`` has an
``*args`` parameter whose type is assignable from the types of any
carljm marked this conversation as resolved.
Show resolved Hide resolved
otherwise-unmatched positional-only parameters in ``A``::

class PosOnly(Protocol):
def __call__(self, a: int, b: str, /) -> None: ...
Expand All @@ -634,26 +632,26 @@ parameters in ``B``::
def __call__(self, a: int, b: str) -> None: ...

def func(int_args: IntArgs, int_str_args: IntStrArgs, str_args: StrArgs):
f1: PosOnly = int_args # Error: str is not subtype of int
f1: PosOnly = int_args # Error: str is not assignable to int
f2: PosOnly = int_str_args # OK
f3: PosOnly = str_args # OK
f4: IntStrArgs = str_args # Error: int | str is not subtype of str
f5: IntStrArgs = int_args # Error: int | str is not subtype of int
f4: IntStrArgs = str_args # Error: int | str is not assignable to str
f5: IntStrArgs = int_args # Error: int | str is not assignable to int
f6: StrArgs = int_str_args # OK
f7: StrArgs = int_args # Error: str is not subtype of int
f7: StrArgs = int_args # Error: str is not assignable to int
f8: IntArgs = int_str_args # OK
f9: IntArgs = str_args # Error: int is not subtype of str
f9: IntArgs = str_args # Error: int is not assignable to str
f10: Standard = int_str_args # Error: keyword parameters a and b missing
f11: Standard = str_args # Error: keyword parameter b missing


``**kwargs`` parameters
^^^^^^^^^^^^^^^^^^^^^^^

If a callable ``B`` has a signature with a ``**kwargs`` parameter (without
an unpacked ``TypedDict`` type annotation), callable ``A`` must also have a
``**kwargs`` parameter to be a subtype of ``B``, and the type of
``B``'s ``**kwargs`` parameter must be a subtype of ``A``'s ``**kwargs``
If a callable ``A`` has a signature with a ``**kwargs`` parameter (without
an unpacked ``TypedDict`` type annotation), callable ``B`` must also have a
``**kwargs`` parameter to be assignable to ``A``, and the type of
``A``'s ``**kwargs`` parameter must be assignable to ``B``'s ``**kwargs``
parameter::

class NoKwargs(Protocol):
Expand All @@ -673,12 +671,12 @@ parameter::
f4: IntKwargs = float_kwargs # OK

f5: FloatKwargs = no_kwargs # Error: missing **kwargs parameter
f6: FloatKwargs = int_kwargs # Error: float is not subtype of int
f6: FloatKwargs = int_kwargs # Error: float is not assignable to int

If a callable ``B`` has a signature with one or more keyword-only parameters,
a callable ``A`` is a subtype of ``B`` if ``A`` has a ``**kwargs`` parameter
whose type is a supertype of the types of any otherwise-unmatched keyword-only
parameters in ``B``::
If a callable ``A`` has a signature with one or more keyword-only parameters,
a callable ``B`` is assignable to ``A`` if ``B`` has a ``**kwargs`` parameter
whose type is assignable from the types of any otherwise-unmatched keyword-only
parameters in ``A``::

class KwOnly(Protocol):
def __call__(self, *, a: int, b: str) -> None: ...
Expand All @@ -696,19 +694,19 @@ parameters in ``B``::
def __call__(self, a: int, b: str) -> None: ...

def func(int_kwargs: IntKwargs, int_str_kwargs: IntStrKwargs, str_kwargs: StrKwargs):
f1: KwOnly = int_kwargs # Error: str is not subtype of int
f1: KwOnly = int_kwargs # Error: str is not assignable to int
f2: KwOnly = int_str_kwargs # OK
f3: KwOnly = str_kwargs # OK
f4: IntStrKwargs = str_kwargs # Error: int | str is not subtype of str
f5: IntStrKwargs = int_kwargs # Error: int | str is not subtype of int
f4: IntStrKwargs = str_kwargs # Error: int | str is not assignable to str
f5: IntStrKwargs = int_kwargs # Error: int | str is not assignable to int
f6: StrKwargs = int_str_kwargs # OK
f7: StrKwargs = int_kwargs # Error: str is not subtype of int
f7: StrKwargs = int_kwargs # Error: str is not assignable to int
f8: IntKwargs = int_str_kwargs # OK
f9: IntKwargs = str_kwargs # Error: int is not subtype of str
f9: IntKwargs = str_kwargs # Error: int is not assignable to str
f10: Standard = int_str_kwargs # Error: Does not accept positional arguments
f11: Standard = str_kwargs # Error: Does not accept positional arguments

Subtyping relationships for callable signatures that contain a ``**kwargs``
Assignability relationships for callable signatures that contain a ``**kwargs``
carljm marked this conversation as resolved.
Show resolved Hide resolved
with an unpacked ``TypedDict`` are described in the section :ref:`above <unpack-kwargs>`.


Expand All @@ -732,10 +730,10 @@ to a ``Callable`` parameterized by ``P``::
Default argument values
^^^^^^^^^^^^^^^^^^^^^^^

If a callable ``A`` has a parameter ``x`` with a default argument value and
``B`` is the same as ``A`` except that ``x`` has no default argument, then
``A`` is a subtype of ``B``. ``A`` is also a subtype of ``C``
if ``C`` is the same as ``A`` with parameter ``x`` removed::
If a callable ``C`` has a parameter ``x`` with a default argument value and
``A`` is the same as ``C`` except that ``x`` has no default argument, then
``C`` is assignable to ``A``. ``C`` is also assignable to ``A``
if ``A`` is the same as ``C`` with parameter ``x`` removed::

class DefaultArg(Protocol):
def __call__(self, x: int = 0) -> None: ...
Expand All @@ -754,9 +752,9 @@ if ``C`` is the same as ``A`` with parameter ``x`` removed::
Overloads
^^^^^^^^^

If a callable ``A`` is overloaded with two or more signatures, it is a subtype
of callable ``B`` if *at least one* of the overloaded signatures in ``A`` is
a subtype of ``B``::
If a callable ``B`` is overloaded with two or more signatures, it is assignable
to callable ``A`` if *at least one* of the overloaded signatures in ``B`` is
assignable to ``A``::

class Overloaded(Protocol):
@overload
Expand All @@ -778,8 +776,9 @@ a subtype of ``B``::
f2: StrArg = overloaded # OK
f3: FloatArg = overloaded # Error

If a callable ``B`` is overloaded with two or more signatures, callable ``A``
is a subtype of ``B`` if ``A`` is a subtype of *all* of the signatures in ``B``::
If a callable ``A`` is overloaded with two or more signatures, callable ``B``
is assignable to ``A`` if ``B`` is assignable to *all* of the signatures in
``A``::

class Overloaded(Protocol):
@overload
Expand Down
7 changes: 4 additions & 3 deletions docs/spec/class-compat.rst
carljm marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.. _`class-compat`:

Class type compatibility
Class type assignability
========================

.. _`classvar`:
Expand Down Expand Up @@ -97,8 +97,9 @@ annotated in ``__init__`` or other methods, rather than in the class::
(Originally specified by :pep:`698`.)

When type checkers encounter a method decorated with ``@typing.override`` they
should treat it as a type error unless that method is overriding a compatible
method or attribute in some ancestor class.
should treat it as a type error unless that method is overriding a method or
attribute in some ancestor class, and the type of the overriding method is
assignable to the type of the overridden method.
carljm marked this conversation as resolved.
Show resolved Hide resolved


.. code-block:: python
Expand Down
Loading