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
PEP 646: Decide on substitution behavior #91162
Comments
We've had some disagreement about the behavior of TypeVarTuple substitution related to PEP-646, and the discussion has now spilled around multiple PRs. I'd like to use this issue to come to an agreement so we don't have to chase through so many different places. Links:
I'd like to ask that until we come to an agreement we hold off on making any more changes, so we don't have to go back and forth and we ensure that the eventual solution covers all edge cases. The disagreement is about what to do with TypeVarTuple substitution: the behavior when a generic type is subscripted, like There are two possible extreme approaches:
|
Thanks for starting this, Jelle - I was a bit unsure about how to proceed here. Given that #31800 is already merged, I'd also propose something halfway between the two extremes: return a sensible substitution when the logic to compute that isn't too onerous, and a new GenericAlias object when it is. The upsides are that we'd probably be able to return reasonable substitutions for the vast majority of cases, and that we wouldn't have to remove what's already been merged. The downsides would be lack of consistency, and the potential for changing rules about what does and doesn't return a full substitution as time goes on and new features are added. |
(Having said that, to be clear: my preferred solution currently would still be the solution where we just return a new GenericAlias for anything involving a TypeVarTuple. The crux is what Serhiy is happy with.) |
Thanks Matthew! Merged PRs can still be reverted, and we have some time before the feature freeze. I'd like to hear what Guido and Ken think too. If we go with the GenericAlias substitution, we need to make sure that such aliases still work as base class. That would need some C work to make types.GenericAlias.__mro_entries__ recurse if the alias's origin is itself a GenericAlias. There's a few other subtleties to think about; I can work on that but don't have a ton of time today. |
I am for consistent behavior. If return GenericAlias(GenericAlias(tuple, Unpack[Ts]), (int, str)) for tuple[*Ts][int, str], we should also return GenericAlias(GenericAlias(list, T), int) for list[T][int], etc. And it will cause multiple problems:
It may be that will need to use it as a fallback for cases like tuple[T, *Ts][*Ts2] (currently it is error). But I am not sure that such cases should be supported. |
I think I'm with Serhiy, I don't understand the hesitance to transform tuple[*Ts][int, str] into tuple[int, str]. What would be an example of a substitution that's too complex to do? |
It's simple if you only look at simple examples. Here are some examples current main (with Serhiy's patch for the Python version of typing) gets wrong: >>> from typing import *
>>> Ts = TypeVarTuple("Ts")
>>> T1 = TypeVar("T1")
>>> T2 = TypeVar("T2")
>>> Tuple[T1, Unpack[Ts], T2][int, Unpack[tuple[int]]] # expect error
typing.Tuple[int, *tuple[int]]
>>> Tuple[T1, Unpack[Ts], str, T2][int, Unpack[Ts]] # expect error (T2 missing)
typing.Tuple[int, str, *Ts] # it put *Ts in the wrong place
>>> Tuple[T1, Unpack[Ts], str, T2][int, Unpack[Ts], Unpack[Ts]] # expect error (*Ts can't substitute T2)
typing.Tuple[int, *Ts, str, *Ts]
>>> class G(Generic[T1, Unpack[Ts], T2]): pass
...
>>> G[int] # expect error
__main__.G[int] We can probably fix that, but I'm not looking forward to implementing the fixed logic in both Python and C. Also, I'm worried that it won't work with future extensions to the type system (e.g., the rumored Map operator) that may go into 3.12 or later versions. |
The first case will be practically fixed by GH 32030 after chenging the grammar to allow unpacking in index tuple: A[*B]. Two other cases will be fixed by GH 32031. It does not require any C code. In the last case no error is raised because some error checks are skipped if any of Generic arguments is a TypeVarTuple. We just need to add such checks. This is Python-only code too. Note that the alternative proposition is even more lenient to errors. |
[Guido]
We also need to remember the dreaded arbitrary-length tuple. For example, I think it should be the case that: T = TypeVar('T')
Ts = TypeVarTuple('Ts')
class C(Generic[*Ts]): pass
Alias = C[T, *Ts]
Alias2 = Alias[*tuple[int, ...]]
# Alias2 should be C[int, *tuple[int, ...]] Ok, this is a bit of a silly example, but if we're committing to evaluating substitutions correctly, we should probably make even this kind of example behave correctly so that users who accidentally do something silly can debug what's gone wrong. [Serhiy]
Definitely true.
Huh, I didn't know about this one. Fair enough, this is totally a downside.
This could admittedly be thorny. We'd have to think it through carefully. Admittedly also a downside.
Oh, also interesting - I didn't know about this one either. Could you give an example?
We actually deliberately chose not to unpack concrete tuple types - see the description of #30398, under the heading 'Starred tuple types'. (If you see another way around it, though, let me know.)
I'm also not sure about this one; disallowing unpacked TypeVarTuples in argument lists to generic aliases completely (if I've understood right?) seems like too restrictive a solution. I can imagine there might be completely legitimate cases where the ability to do this would be important. For example: DType = TypeVar('DType')
Shape = TypeVarTuple('Shape')
class Tensor(Generic[DType, *Shape]): ...
Uint8Tensor = Tensor[uint8, *Shape]
Unit8BatchTensor = Uint8Tensor[Batch, *Shape]
True, but at least it's predictably lenient to errors - I think the repr makes it very clear that "Woah, you're doing something advanced here. You're on your own!" I think it better fits the principle of least astonishment to have something that consistently lets through all errors of a certain class than something that sometimes catches errors and sometimes doesn't. |
P.s. To be clear, (I think?) these are all substitutions that are computable. We *could* implement the logic to make all these evaluate correctly if we wanted to. It's just a matter of how much complexity we want to allow in typing.py (or in the runtime in general, if we say farmed some of this logic out to a separate module). |
I'd like to look at this as a case of simplifying something to its simplest canonical form, but no simpler. This is what the existing fixed-typevar expansion does: e.g. tuple[str, T, T][int] becomes tuple[str, int, int]. I propose that we try to agree on a set of rules for what can be simplified further and what cannot, when we have B = C[...]; A = B[...], (IOW A = C[...][...]), for various shapes of the subscripts to C and B. Note that what's relevant for the second subscript is C[...].__parameters__, so I'll call that "left" below.
TBH case 5 is the most complex and I may have overlooked something. I'm more sure of cases 1-4. |
tuple[int, ...] includes also an empty tuple, and in this case there is no value for T.
If __origin__, __parameters__, __args__ are a mess, it will definitely break a code which use them.
You assumed that *tuple[str, bool] in def foo(*args: *tuple[str, bool]) should give foo.__annotations__['args'] = tuple[str, bool], but it should rather give (str, bool). No confusion with tuple[str, bool]. And one of PEP-646 options is to implement star-syntax only in subscription, not in var-parameter type annotations.
No, it will only be disallowed in substitution of a VarType. Tuple[T][*Ts] -- error. Tuple[*Ts][*Ts2] -- ok. I propose to implement simple and strict rules, and later add support of new cases where it makes sense. |
I do not understand this. Do you forbid simplifying of tuple[*Ts, float][str, *tuple[int, ...]] to tuple[str, *tuple[int, ...], float]? I think that the rule should be that *tuple[X, ...] cannot split between different variables. Or that it cannot substitute a TypeVar. A more strong variant of rule 4.
I think that it will better to flag it as an error now. Later, after all code be merged and all edge cases be handled we can return here and reconsider this. There are workarounds for this.
These tricks are common in functional programming. The rest of the rules match my implementations more or less. |
Apologies for the slow reply - coming back to this now that the docs and pickling issues are mostly sorted. [Serhiy]
This was my initial intuition too, but Pradeep pointed out to me in #31021 (comment) that for tuple[int, ...], Python has chosen the opposite mindset: instead of assuming the worst-case scenario, we assume the best-case scenario. Thus, the following type-checks correctly with mypy (https://mypy-play.net/?mypy=latest&python=3.10&gist=b9ca66fb7d172f939951a741388836a6): def return_first(tup: tuple[int, ...]) -> int:
return tup[0]
tup: tuple[()] = ()
return_first(tup)
Fair point, we could technically distinguish between tuple[str, bool] and (str, bool). But if I was a naive user and I saw Also though, there's a second reason mentioned in #30398 why
As in, we would allow
Ah, gotcha. My mistake. [Guido] I ran out of time this evening :) Will reply properly soon. |
[Guido]
Alright, let me think this through with some examples to get my head round it. It would prohibit the following difficult case: class C(Generic[*Ts]): ...
Alias = C[T, *Ts]
Alias[*tuple[int, ...]] # Does not simplify; stays C[T, *Ts][*tuple[int, ...]] That seems pretty reasonable. It would also prohibit these other relatively simple cases, but I guess that's fine: Alias = C[*Ts]
Alias[*tuple[int, ...]] # Does not simplify; stays C[*Ts][*tuple[int, ...]]
Alias = C[T, *tuple[int, ...]]
Alias[str] # Does not simplify; stays C[T, *tuple[int, ...]][str]
Is this to say that we effectively prohibit binding *tuple[...] to anything? If we can simplify without binding *tuple[...] to anything, then we do simplify, but otherwise, we don't simplify? So under this rule, the following WOULD work? Alias = C[T, *tuple[int, ...]]
Alias[str] # Simplifies to C[str, *tuple[int, ...]], because we didn't have to bind *tuple[int, ...] to do it
Alright, so this is business as usual.
So then: class C(Generic[*Ts]): ...
Alias = C[T, *Ts]
Alias[()] # Raises error
Alias[int] # Simplifies to C[int, *Ts]
Alias[int, str] # Simplifies to C[int, str]
Alias[int, str, bool] # Simplifies to C[int, str, bool] Yup, seems straightforward.
Ok, so this is about the following situations: class C(Generic[*Ts]): ...
Alias = C[T1, T2]
Alias[*Ts] # Does not simplify; stays C[T1, T2][*Ts] Yikes - in fact, this is actually super hairy; I hadn't thought about this edge case at all in the PEP. Agreed that it seems reasonable not to simplify here.
Was that a typo? Surely tuple[int, int][*Ts] isn't valid - since tuple[int, int] doesn't have any free parameters?
Ok, this also makes sense. --- Still, though, doesn't the point that Serhiy brought up about __origin__, __parameters__ and __args__ still apply? In cases where we *don't* simplify, there'd still be the issue of what we'd set these things to be. This evening I'll also revisit the PRs adding tests for substitution to try and make them a comprehensive reference as to what's currently possible. |
Ok, https://github.com/python/cpython/pull/32341/files is a reference of how the current implementation behaves. Fwiw, it *is* mostly correct - with a few minor tweaks it might be alright for at least the 3.11 release. In particular, instead of dealing with the thorny issue of what to do about splitting unpacked arbitrary-length tuples over multiple type variables - e.g. C[T, *Ts][*tuple[int, ...]] - instead either deciding to try and evaluate it properly and living with the complexity, or leaving it unsimplified and living with the __args__, __parameters__ and __origin__ problem - for now, we could just raise an exception for any substitutions which involve an unpacked arbitrary-length tuple, since I'd guess it's going to be an extremely rare use-case. |
We need to move on this, because the outcome of this discussion is a release blocker for 3.11b1 -- the next release! |
Copying in correspondence by email while issues were being migrated:
|
I feel I need to add this same remark here: @mrahtz @JelleZijlstra @serhiy-storchaka Is it okay if I unsubscribe from these conversations and let you all come up with a compromise? I feel that while we ought to have a policy formulated and mostly implemented by beta 1 (May 6), tweaks of both the policy and the implementation during the beta period until RC 1 (Aug/Sept?) should be allowable. |
Ok, thinking about things more in #32341, I would propose the following spec:
|
[Pradeep] Sorry for the slow reply too. Busy week :(
I've just realised - is this actually what we said in the PEP? https://peps.python.org/pep-0646/#behaviour-when-type-parameters-are-not-specified says: (emphasis mine)
The section on aliases, https://peps.python.org/pep-0646/#aliases, says similarly:
That is, only the Admittedly we maybe could have been clearer on this in the PEP. I don't think we ever say explicitly what type is assigned to the other type parameters in a case like this. But I think it's natural to assume that DType = TypeVar('DType')
Shape = TypeVarTuple('Shape')
class Array(Generic[DType, *Shape]): ...
MultiDeviceArray = Array[DType, *Shape] I think that Is this analysis correct? |
Looks like the only interpretation that makes sense. If you write |
[Matthew]
Yes, that is indeed how Pyre handles aliases with missing type arguments. T is replaced with [Matthew]
That's not true. We explicitly mention the behavior for
In other words, the splitting behavior of unbounded tuples over a mix of Lmk if that makes sense. I'm happy to hop on a VC call this week, if this is something you'd like to discuss further. Might be faster than waiting for each other's replies :) Road aheadWe're back to the original question of what to do about
I favor option (1). Curious to hear your thoughts, since it's not clear which way you are leaning. FootnoteIf the question is why would anyone specify
|
[Pradeep]
For this kind of thing, I think text works better for me - it's helpful to have time to think. Thanks for offering, though :)
Isn't
Didn't we write that assuming that
Ah, sorry if I wasn't explicit enough on this earlier - yeah, for this case, I'm also on board with deferring its implementation at runtime until someone finds a use-case for it - both because it could take a while to implement and test, and because I think Jelle has a remit to try and keep complexity out of I definitely agree it's not worth banning in the PEP. |
I hesitate to add anything, but here's something nevertheless. I feel that there's something missing from our notation, and that is how to indicate that the number of dimensions is def f(a: int, *b: int) -> int: ...
t: tuple[int, ...] = ()
f(*t) # OK in mypy, fails at runtime even though We should probably just accept this. Certainly it seems PEP 646 accepted this for some cases already (the quote by Pradeep), so it seems fair to accept it in the case of a type alias as well. OT: While reading these deliberations, am beginning to find the notation |
[Guido]
Assuming that: DType = TypeVar('DType')
Shape = TypeVarTuple('Shape')
class ArrayShapeOnly(Generic[*Shape]): ...
class ArrayDTypeAndShape(Generic[DType, *Shape]]): ... Doesn't Or is your point that there should be some nicer syntax for specifying this? In practice, I'd expect most library authors to cater for this with an alias along the lines of: ArrayWithArbitraryNumDimensions = ArrayDTypeAndShape[DType, *tuple[Any, ...]] So maybe special syntax wouldn't be necessary?
def f(a: int, *b: int) -> int: ...
t: tuple[int, ...] = ()
f(*t) # OK in mypy, fails at runtime even though f(*()) is rejected. To check that I understand:
I might just be tired, but could you expand on the connection between this case and the "arbitrary number of dimensions" case?
I hear you that it's a bit of a tedious syntax. And yes, you're right - the reason I'm hesitant to use But I don't think we should discuss it yet, because it's a decision that'll impact a lot of future users of array typing who don't exist yet and who therefore can't weigh in on the discussion. Ideally we'd talk about it once, say, NumPy has adopted PEP 646, and people have been using it for NumPy annotations for a good 6 months or so. |
Fine by me!
The idea I was getting across was that even if But if users tried to define a generic alias That is why I was arguing against forbidding this. Since we agree on not forbidding this in the PEP, no worries.
If we had an alias such as So, yes, it is always feasible to add type arguments separately for each
+1. Hope there's nothing else blocking this thread, then. |
First responding to Pradeep: What does "punting on the implementation" mean in practice? Does it just mean that Next to Matthew, in reverse order: (c) I will happily await discussing the semantics of (b) Yes, you understand my claim about (a) I would be happy with the syntax |
[Guido]
Right now, Will defer to Matthew in case he wants to clarify. |
Leaving it a runtime error does go against the general trend to be more lenient at runtime than in the static checker, though. So if there's another approach that's not too much effort I'd rather not have the runtime error. If we leave it a runtime error in 3.11.0, could we fix it in 3.11.1, or would we have to wait until 3.12? If 3.11.1, that would be acceptable. |
Long thread is long The issue of
|
Agreed. Regarding punting on the implementation, your proposal is to only punt on the case where there is an extra type ( |
Actually, I meant The change would have to be making it I'll put up a PR to clarify the code and wording (assuming it's ok to update the PEP, since this is an oversight in the naming). |
[Guido]
Ok, great :) Lower bounds on the number of type arguments
Ok, so if I understand right, you're saying that, regarding whether we should allow... T = TypeVar('T')
Shape = TypeVarTuple('Shape')
class Array(Generic[*Shape]): ...
def foo(x: Array[int, *tuple[int, ...]): ...
x: Array[*tuple[int, ...]]
foo(x) ...given that we're passing "Zero or more def bar(a: int, *b: int): ...
t: tuple[int, ...]
foo(*t) ...and furthermore noting that, if we do allow this, one downside is that it prevents us from being able to properly enforce a lower bound of the number of type arguments in cases where we don't have an upper bound on the number of type arguments, like in If so - hmm, interesting point. I agree the validity of the I guess what that means for arrays is that we couldn't catch something like this: class Array(Generic[*Shape]): ...
# Argument must have at least one dimension - scalar arrays like Array[()] not allowed!
def only_accepts_arrays(x: Array[Any, *tuple[Any, ...]]): ...
def returns_scalar_or_array() → Array[*tuple[Any, ...]]: ...
x = returns_scalar_or_array()
only_accepts_array(x) # x might be Array[()], so this might not be ok! This is mildly annoying, but seems like an ok sacrifice to make in order to enable gradual typing - allowing What we're punting on
Close. There's a similar case where the unpacked arbitrary-length tuple lines up exactly with a class Array(Generic[*Ts]): pass
Alias = Array[*Ts, T]
Alias[*tuple[int, ...], str] # Evaluates to Array[*tuple[int, ...], str] So I think the actual logic would be something like:
To put it concisely: we're punting on the case where it's a valid substitution, but the unpacked arbitrary-length tuple in the arguments doesn't line up with the |
[Pradeep]
Oh! Sorry, I didn't realise you intended |
Good, we’re in full agreement.
On Sat, May 28, 2022 at 03:57 Matthew Rahtz ***@***.***> wrote:
[Guido]
(c) I will happily await discussing the semantics of A[T, ..., S] until
there's actual experience with PEP 646 (i.e., well after 3.11 is released).
Ok, great :)
Lower bounds on the number of type arguments
(b) Yes, you understand my claim about f(*t) vs. f(*()). The reason I
connect vararg variable substitutions with vararg function calls is that in
both cases we have a single notation that means "arbitrary number" which is
generally understood to mean "zero or more" but which is also acceptable in
situations where the requirement is "N or more, for some N larger than
zero". (We emphasize that an empty tuple is a valid match for tuple[int,
...].)
(a) I would be happy with the syntax A[X, *tuple[X, ...]] to indicate "A
with 1 or more X parameters". However, we accept some other type whose
meaning is "A with 0 or more X parameters" as a valid substitution. So
really it appears that as soon as we have a type whose parameter count has
no upper bound, we appear to drop the lower bound -- we cannot distinguish
between "count >= 0" and "count >= 1". I drew the analogy with function
calls because they have the same issue and we seem to be fine with that.
Ok, so if I understand right, you're saying that, regarding whether we
should allow...
T = TypeVar('T')
Shape = TypeVarTuple('Shape')
class Array(Generic[*Shape]): ...
def foo(x: Array[int, *tuple[int, ...]): ...
x: Array[*tuple[int, ...]]
foo(x)
...given that we're passing "Zero or more int" to something that seems to
require "One or more int", you're arguing that it *should* be allowed, by
analogy with the following, which is fine in mypy...
def bar(a: int, *b: int): ...
t: tuple[int, ...]
foo(*t)
...and furthermore noting that, if we *do* allow this, one downside is
that it prevents us from being able to properly enforce a *lower* bound
of the number of type arguments in cases where we don't have an *upper*
bound on the number of type arguments, like in foo above. Is that right?
If so - hmm, interesting point. I agree the validity of the bar example
suggests we should also be fine with corresponding foo example. But it
hadn't occurred to me that this requires us to give up lower bound
verification.
I guess what that means for arrays is that we couldn't catch something
like this:
class Array(Generic[*Shape]): ...
# Argument must have at least one dimension - scalar arrays like Array[()] not allowed!
def only_accepts_arrays(x: Array[Any, *tuple[Any, ...]]): ...
def returns_scalar_or_array() → Array[*tuple[Any, ...]]: ...
x = returns_scalar_or_array()
only_accepts_array(x) # x might be Array[()], so this might not be ok!
This is mildly annoying, but seems like an ok sacrifice to make in order
to enable gradual typing - allowing Array == Array[*tuple[Any, ...]] to
be a 'universal' array.
What we're punting on
Regarding punting on the implementation, your proposal is to only punt on
the case where there is an extra type (str in your example) in the actual
parameters following *tuple[int, ...], right? I can live with that then,
though if Serhiy manages to fix it I wouldn't stop him.
Close. There's a similar case where the unpacked arbitrary-length tuple
lines up exactly with a TypeVarTuple and all other types line up exactly
with TypeVars, which is easy to evaluate, and which the current runtime
therefore evaluates fine:
class Array(Generic[*Ts]): pass
Alias = Array[*Ts, T]
Alias[*tuple[int, ...], str] # Evaluates to Array[*tuple[int, ...], str]
So I think the actual logic would be something like:
- If the type argument list consists of a single unpacked
arbitrary-length tuple *tuple[int, ...], then we're fine.
- Else if the type argument list "lines up exactly" with the type
arguments - such that the type argument list is the same length as the type
parameter list, and the TypeVarTuple in the type parameter list lines
up with the unpacked arbitrary-length tuple in the type argument list (and
there are no other unpacked arbitrary-length tuples in the type argument
list), then we're fine.
- Else, error.
To put it concisely: we're punting on the case where it's a valid
substitution, but the unpacked arbitrary-length tuple in the arguments
doesn't line up with the TypeVarTuple in the parameters.
—
Reply to this email directly, view it on GitHub
<#91162 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAWCWMRGEZXLUTYMYFELOKLVMH3YVANCNFSM5TEGYM5Q>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
--
--Guido (mobile)
|
…able-size tuple For example: A[T, *Ts][*tuple[int, ...]] -> A[int, *tuple[int, ...]] A[*Ts, T][*tuple[int, ...]] -> A[*tuple[int, ...], int]
I have wrote the Python implementation and now working on the C code. Please look whether the Python implementation works as you expect. For simplicity, it forbids substitution of multiple unpacked var-tuples, e.g. |
The following examples now work:
|
Oh no :( I already did a bunch of work on the Python side of this yesterday in #93318. I'm upset that I wasted a Saturday on this. Please check more carefully next time whether someone else is already working on it before starting. Edit: ah, sorry, I thought you'd merged it already - I didn't realise it was a WIP PR. Still, unfortunate that we ended up duplicating each other's work here :( But yes, the behaviour looks correct. And thanks for implementing the C version. I also looked at this yesterday but got a bit lost, so I appreciate you taking care of it. |
I am sorry. I promised to work on it almost a month ago, but I only had the time and inspiration to do it last weekend. GitHub does not send notifications about the linked PRs by email, so I was unaware of your work. #93330 is now ready for review. I have also another, simpler, version, which moves a lot of the C code to Python, but I need more time to polish it. |
pythonGH-92335) (cherry picked from commit 9d25db9) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
…es (GH-92335) (#92484) * gh-91162: Fix substitution of unpacked tuples in generic aliases (GH-92335) (cherry picked from commit 9d25db9) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> * Regenerate ABI file Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Pablo Galindo <pablogsal@gmail.com>
#93412 is an alternative implementation which does complex things in Python and calls the Python code from C. Seems it can also simplify the code of collections.abc.Callable (because the code is more generic now), but I left a clean up to a separate PR. |
Oh, fair enough. In that case I'll just say: thank you for your continued work on this :) |
…ypeVar and TypeVarTuple parameters (alt) (GH-93412) For example: A[T, *Ts][*tuple[int, ...]] -> A[int, *tuple[int, ...]] A[*Ts, T][*tuple[int, ...]] -> A[*tuple[int, ...], int]
…over TypeVar and TypeVarTuple parameters (alt) (pythonGH-93412) For example: A[T, *Ts][*tuple[int, ...]] -> A[int, *tuple[int, ...]] A[*Ts, T][*tuple[int, ...]] -> A[*tuple[int, ...], int] (cherry picked from commit 3473817) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: