Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

PEP 647 – User-Defined Type Guards

Author:
Eric Traut <erictr at microsoft.com>
Sponsor:
Guido van Rossum <guido at python.org>
Discussions-To:
Typing-SIG list
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
07-Oct-2020
Python-Version:
3.10
Post-History:
28-Dec-2020, 09-Apr-2021
Resolution:
Python-Dev thread

Table of Contents

Important

This PEP is a historical document: seeTypeGuard andtyping.TypeGuard for up-to-date specs and documentation. Canonical typing specs are maintained at thetyping specs site; runtime typing behaviour is described in the CPython documentation.

×

See thetyping specification update process for how to propose changes to the typing spec.

Abstract

This PEP specifies a way for programs to influence conditional type narrowingemployed by a type checker based on runtime checks.

Motivation

Static type checkers commonly employ a technique called “type narrowing” todetermine a more precise type of an expression within a program’s code flow.When type narrowing is applied within a block of code based on a conditionalcode flow statement (such asif andwhile statements), the conditionalexpression is sometimes referred to as a “type guard”. Python type checkerstypically support various forms of type guards expressions.

deffunc(val:Optional[str]):# "is None" type guardifvalisnotNone:# Type of val is narrowed to str...else:# Type of val is narrowed to None...deffunc(val:Optional[str]):# Truthy type guardifval:# Type of val is narrowed to str...else:# Type of val remains Optional[str]...deffunc(val:Union[str,float]):# "isinstance" type guardifisinstance(val,str):# Type of val is narrowed to str...else:# Type of val is narrowed to float...deffunc(val:Literal[1,2]):# Comparison type guardifval==1:# Type of val is narrowed to Literal[1]...else:# Type of val is narrowed to Literal[2]...

There are cases where type narrowing cannot be applied based on staticinformation only. Consider the following example:

defis_str_list(val:List[object])->bool:"""Determines whether all objects in the list are strings"""returnall(isinstance(x,str)forxinval)deffunc1(val:List[object]):ifis_str_list(val):print(" ".join(val))# Error: invalid type

This code is correct, but a type checker will report a type error becausethe valueval passed to thejoin method is understood to be of typeList[object]. The type checker does not have enough information tostatically verify that the type ofval isList[str] at this point.

This PEP introduces a way for a function likeis_str_list to be defined asa “user-defined type guard”. This allows code to extend the type guards thatare supported by type checkers.

Using this new mechanism, theis_str_list function in the above examplewould be modified slightly. Its return type would be changed frombooltoTypeGuard[List[str]]. This promises not merely that the return valueis boolean, but that a true indicates the input to the function was of thespecified type.

fromtypingimportTypeGuarddefis_str_list(val:List[object])->TypeGuard[List[str]]:"""Determines whether all objects in the list are strings"""returnall(isinstance(x,str)forxinval)

User-defined type guards can also be used to determine whether a dictionaryconforms to the type requirements of a TypedDict.

classPerson(TypedDict):name:strage:intdefis_person(val:dict)->"TypeGuard[Person]":try:returnisinstance(val["name"],str)andisinstance(val["age"],int)exceptKeyError:returnFalsedefprint_age(val:dict):ifis_person(val):print(f"Age:{val['age']}")else:print("Not a person!")

Specification

TypeGuard Type

This PEP introduces the symbolTypeGuard exported from thetypingmodule.TypeGuard is a special form that accepts a single type argument.It is used to annotate the return type of a user-defined type guard function.Return statements within a type guard function should return bool values,and type checkers should verify that all return paths return a bool.

In all other respects, TypeGuard is a distinct type from bool. It is not asubtype of bool. Therefore,Callable[...,TypeGuard[int]] is not assignabletoCallable[...,bool].

WhenTypeGuard is used to annotate the return type of a function ormethod that accepts at least one parameter, that function or method istreated by type checkers as a user-defined type guard. The type argumentprovided forTypeGuard indicates the type that has been validated bythe function.

User-defined type guards can be generic functions, as shown in this example:

_T=TypeVar("_T")defis_two_element_tuple(val:Tuple[_T,...])->TypeGuard[Tuple[_T,_T]]:returnlen(val)==2deffunc(names:Tuple[str,...]):ifis_two_element_tuple(names):reveal_type(names)# Tuple[str, str]else:reveal_type(names)# Tuple[str, ...]

Type checkers should assume that type narrowing should be applied to theexpression that is passed as the first positional argument to a user-definedtype guard. If the type guard function accepts more than one argument, notype narrowing is applied to those additional argument expressions.

If a type guard function is implemented as an instance method or class method,the first positional argument maps to the second parameter (after “self” or“cls”).

Here are some examples of user-defined type guard functions that accept morethan one argument:

defis_str_list(val:List[object],allow_empty:bool)->TypeGuard[List[str]]:iflen(val)==0:returnallow_emptyreturnall(isinstance(x,str)forxinval)_T=TypeVar("_T")defis_set_of(val:Set[Any],type:Type[_T])->TypeGuard[Set[_T]]:returnall(isinstance(x,type)forxinval)

The return type of a user-defined type guard function will normally refer toa type that is strictly “narrower” than the type of the first argument (thatis, it’s a more specific type that can be assigned to the more general type).However, it is not required that the return type be strictly narrower. Thisallows for cases like the example above whereList[str] is not assignabletoList[object].

When a conditional statement includes a call to a user-defined type guardfunction, and that function returns true, the expression passed as the firstpositional argument to the type guard function should be assumed by a statictype checker to take on the type specified in the TypeGuard return type,unless and until it is further narrowed within the conditional code block.

Some built-in type guards provide narrowing for both positive and negativetests (in both theif andelse clauses). For example, consider thetype guard for an expression of the formxisNone. Ifx has a type thatis a union of None and some other type, it will be narrowed toNone in thepositive case and the other type in the negative case. User-defined typeguards apply narrowing only in the positive case (theif clause). The typeis not narrowed in the negative case.

OneOrTwoStrs=Union[Tuple[str],Tuple[str,str]]deffunc(val:OneOrTwoStrs):ifis_two_element_tuple(val):reveal_type(val)# Tuple[str, str]...else:reveal_type(val)# OneOrTwoStrs...ifnotis_two_element_tuple(val):reveal_type(val)# OneOrTwoStrs...else:reveal_type(val)# Tuple[str, str]...

Backwards Compatibility

Existing code that does not use this new functionality will be unaffected.

Notably, code which uses annotations in a manner incompatible with thestdlib typing library should simply not import TypeGuard.

Reference Implementation

The Pyright type checker supports the behavior described in this PEP.

Rejected Ideas

Decorator Syntax

The use of a decorator was considered for defining type guards.

@type_guard(List[str])defis_str_list(val:List[object])->bool:...

The decorator approach is inferior because it requires runtime evaluation ofthe type, precluding forward references. The proposed approach was also deemedto be easier to understand and simpler to implement.

Enforcing Strict Narrowing

Strict type narrowing enforcement (requiring that the type specifiedin the TypeGuard type argument is a narrower form of the type specifiedfor the first parameter) was considered, but this eliminates valuableuse cases for this functionality. For instance, theis_str_list exampleabove would be considered invalid becauseList[str] is not a subtype ofList[object] because of invariance rules.

One variation that was considered was to require a strict narrowing requirementby default but allow the type guard function to specify some flag toindicate that it is not following this requirement. This was rejected becauseit was deemed cumbersome and unnecessary.

Another consideration was to define some less-strict check that ensures thatthere is some overlap between the value type and the narrowed type specifiedin the TypeGuard. The problem with this proposal is that the rules for typecompatibility are already very complex when considering unions, protocols,type variables, generics, etc. Defining a variant of these rules that relaxessome of these constraints just for the purpose of this feature would requirethat we articulate all of the subtle ways in which the rules differ and underwhat specific circumstances the constrains are relaxed. For this reason,it was decided to omit all checks.

It was noted that without enforcing strict narrowing, it would be possible tobreak type safety. A poorly-written type guard function could produce unsafe oreven nonsensical results. For example:

deff(value:int)->TypeGuard[str]:returnTrue

However, there are many ways a determined or uninformed developer can subverttype safety – most commonly by usingcast orAny. If a Pythondeveloper takes the time to learn about and implement user-definedtype guards within their code, it is safe to assume that they are interestedin type safety and will not write their type guard functions in a way that willundermine type safety or produce nonsensical results.

Conditionally Applying TypeGuard Type

It was suggested that the expression passed as the first argument to a typeguard function should retain its existing type if the type of the expression wasa proper subtype of the type specified in the TypeGuard return type.For example, if the type guard function isdeff(value:object)->TypeGuard[float] and the expression passed to this function is of typeint, it would retain theint type rather than take on thefloat type indicated by the TypeGuard return type. This proposal wasrejected because it added complexity, inconsistency, and opened up additionalquestions about the proper behavior if the type of the expression was ofcomposite types like unions or type variables with multiple constraints. It wasdecided that the added complexity and inconsistency was not justified giventhat it would provide little or no added value.

Narrowing of Arbitrary Parameters

TypeScript’s formulation of user-defined type guards allows for any inputparameter to be used as the value tested for narrowing. The TypeScript languageauthors could not recall any real-world examples in TypeScript where theparameter being tested was not the first parameter. For this reason, it wasdecided unnecessary to burden the Python implementation of user-defined typeguards with additional complexity to support a contrived use case. If suchuse cases are identified in the future, there are ways the TypeGuard mechanismcould be extended. This could involve the use of keyword indexing, as proposedinPEP 637.

Narrowing of Implicit “self” and “cls” Parameters

The proposal states that the first positional argument is assumed to be thevalue that is tested for narrowing. If the type guard function is implementedas an instance or class method, an implicitself orcls argument willalso be passed to the function. A concern was raised that there may becases where it is desired to apply the narrowing logic onself andcls.This is an unusual use case, and accommodating it would significantlycomplicate the implementation of user-defined type guards. It was thereforedecided that no special provision would be made for it. If narrowingofself orcls is required, the value can be passed as an explicitargument to a type guard function.

Copyright

This document is placed in the public domain or under theCC0-1.0-Universal license, whichever is more permissive.


Source:https://github.com/python/peps/blob/main/peps/pep-0647.rst

Last modified:2024-06-11 22:12:09 GMT


[8]ページ先頭

©2009-2025 Movatter.jp