Mailing List Archive

Let's Fix Class Annotations -- And Maybe Annotations Generally
Howdy howdy.  While working on my PEP I stumbled over a lot of behavior by
annotations that I found inconsistent and inconvenient.  I think there are
several problems here that needs fixing.  This discussion will probably
evolve into a PEP, and I'll be happy to steer that process.  But I'm less
certain about what the right thing to do is.  (Although I do know what I'd
prefer!)  So let's talk about it!

Annotations are represented in Python as a dictionary.  They can be present
on functions, classes, and modules as an attribute called "__annotations__".

We start with: how do you get the annotations from one of these objects?
Surely it's as easy as this line from Lib/inspect.py shows us:

    return func.__annotations__

And yes, that's best practice for getting an annotation from a function
object.
But consider this line from Lib/functools.py:

    ann = getattr(cls, '__annotations__', {})

Huh.  Why doesn't it simply look at cls.__annotations__?  It's because the
language declares that __annotations__ on a class or module is optional.
Since cls.__annotations__ may not be defined, evaluating that might throw an
exception.  Three-argument getattr() is much safer, and I assert it's best
practice for getting the annotations from a module.

But consider this line from Lib/dataclasses.py:

    cls_annotations = cls.__dict__.get('__annotations__', {})

And a very similar line from Lib/typing.py:

    ann = base.__dict__.get('__annotations__', {})

Huh!  Why is this code skipping the attribute entirely, and examining
cls.__dict__?  It's because the getattr() approach has a subtle bug when
dealing with classes.  Consider this example:

    class A:
        ax:int=3
    class B(A):
        pass
    print(getattr(B, '__annotations__', {}))

That's right, B *inherits* A.__annotations__!  So this prints {'ax': int}.

This *can't* the intended behavior of __annotations__ on classes. It's only
supposed to contain annotations for the fields of B itself, not those of one
of its randomly-selected base classes.  But that's how it behaves today--and
people have had to work around this behavior for years.  Examining the class
dict is, sadly, best practice for getting __annotations__ from a class.

So, already: three different objects can have __annotations__, and there are
three different best practices for getting their __annotations__.

Let's zoom out for a moment.  Here's the list of predefined data fields
you can find on classes:

    __annotations__
    __bases__
    __class__
    __dict__
    __doc__
    __module__
    __mro__
    __name__
    __qualname__

All of these describe metadata about the class.  In every case *except one*,
the field is mandatory, which also means it's never inherited. And in every
case *except one*, you cannot delete the field.  (Though you *are* allowed
to overwrite some of them.)

You guessed it: __annotations__ is the exception.  It's optional, and
you're allowed to delete it.  And these exceptions are causing problems.
It seems to me that, if the only way to correctly use a language-defined
attribute of classes is by rooting around in its __dict__, the design is
a misfire.

(Much of the above also applies to modules, too.  The big difference: since
modules lack inheritance, you don't need to look in their __dict__.)

Now consider what happens if my "delayed annotations of annotations using
descriptors" PEP is accepted.  If that happens, pulling __annotations__
out of the class dict won't work if they haven't been generated yet.  So
today's "best practice" becomes tomorrow's "this code doesn't work".
To correctly examine class annotations, code would have to do something
like this, which should work correctly in any Python 3.x version:

    if (getattr(cls, '__co_annotations__', None)
        or ('__annotations__' in cls.__dict__)):
        ann = cls.__annotations__
    else:
        ann = {}

This is getting ridiculous.


Let's move on to a related topic.  For each of the objects that can
have annotations, what happens if o.__annotations__ is set, and you
"del o.__annotations__", then you access o.__annotations__?  It depends on
what the object is, because each of them behaves differently.

You already know what happens with classes: if any of the base classes has
__annotations__ set, you'll get the first one you find in the MRO.  If none
of the bases have __annotations__ set you'll get an AttributeError.

For a module, if you delete it then try to access it, you'll always get
an AttributeError.

For a function, if you delete it then try to get it, the function will
create a new empty dict, store it as its new annotations dict, and return
that.  Why does it do that?  I'm not sure.  The relevent PEP (3107) doesn't
specify this behavior.

So, annotations can be set on three different object types, and each of
those three have a different behavior when you delete the annotations
then try to get them again.



As a final topic: what are the permitted types for __annotations__?
If you say "o.__annotations__ = <x>", what types are and aren't
allowed for <x>?

For functions, __annotations__ may be assigned to either None or
a dict (an object that passes PyDict_Check).  Anything else throws a
TypeError.  For classes and modules, no checking is done whatsoever,
and you can set __annotations__ on those two to any Python object.
While "a foolish consistency is the hobgoblin of little minds",
I don't see the benefit of setting a module's __annotations__ to 2+3j.


I think it's long past time that we cleaned up the behavior of annotations.
They should be simple and consistent across all objects that support them.


At the very least, I think we should make cls.__annotations__ required
rather
than optional, so that it's never inherited.  What should its default value
be?  An empty dict would be more compatible, but None would be cheaper.
Note that creating the empty dict on the fly, the way function objects do,
wouldn't really help--because current best practice means looking in
cls.__dict__.

I also think you shouldn't be able to delete __annotations__ on any of the
three objects (function, class, module).  It should always be set, so that
the best practice for accessing annotations on an object is always
o.__annotations__.


If I could wave my magic wand and do whatever I wanted, I'd change the
semantics for __annotations__ to the following:

* Functions, classes, and modules always have an __annotations__ member set.
* "del o.__annotations__" always throws a TypeError.
* The language will set __annotations__ to a dict if the object has
  annotations, or None if it has no annotations.
* You may set __annotations__, but you can only set it to either None or a
  dict (passes PyDict_Check).
* You may only access __annotations__ as an attribute, and because it's
  always set, best practice is to use "o.__annotations__" (though getattr
  will always work too).
* How __annotations__ is stored is implementation-specific behavior;
  looking in the relevant __dict__ is unsupported.

This would grant sanity and consistency to __annotations__ in a way it's
never so far enjoyed.  The problem is, it's a breaking change. But the
existing semantics are kind of terrible, so at this point my goal is to
break them. I think the best practice needs to stop requiring examining
cls.__dict__; in fact I'd prefer people stop doing it altogether.

If we change the behavior as part of a new release of Python,
code that examines annotations on classes can do a version check:

    if (sys.version_info.major >=3
        and sys.version_info.minor >= 10):

        def get_annotations(o):
            return o.__annotations__ or {}
    else:
        def get_annotations(o):
            # eight or ten lines of complex code goes here
            ...

Or code can just use inspect.get_type_hints(), which is tied to the
Python version
anyway and should always do the right thing.


//arry/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
My code pretty much does what you suggest at the end of your message:

On Mon, 2021-01-11 at 09:22 -0800, Larry Hastings wrote:
> Or code can just use inspect.get_type_hints(), which is tied to the
> Python version
> anyway and should always do the right thing.

So far, this has proven mostly[1] sufficient for my needs in a runtime
type validation and encoding/decoding library.

[1] A pain point for me is the runtime cost of evaluating 3.10 style
type hints, as they're (re-)evaluated for every call to get_type_hints.
I've worked around this for now with my own function affix_type_hints,
which evaluates get_type_hints once and replaces __annotations__ with
the evaluated value. It also addresses a scoping problem where a type
hint may reference a value that's not globally scoped for the object
being annotated; the hint can be evaluated and affixed within that
scope.

Paul
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On Mon, Jan 11, 2021 at 9:23 AM Larry Hastings <larry@hastings.org> wrote:

> [SNIP - background info]
>
>
> If I could wave my magic wand and do whatever I wanted, I'd change the
> semantics for __annotations__ to the following:
>
> * Functions, classes, and modules always have an __annotations__ member
> set.
> * "del o.__annotations__" always throws a TypeError.
> * The language will set __annotations__ to a dict if the object has
> annotations, or None if it has no annotations.
> * You may set __annotations__, but you can only set it to either None or a
> dict (passes PyDict_Check).
> * You may only access __annotations__ as an attribute, and because it's
> always set, best practice is to use "o.__annotations__" (though getattr
> will always work too).
> * How __annotations__ is stored is implementation-specific behavior;
> looking in the relevant __dict__ is unsupported.
>
> This would grant sanity and consistency to __annotations__ in a way it's
> never so far enjoyed. The problem is, it's a breaking change. But the
> existing semantics are kind of terrible, so at this point my goal is to
> break them. I think the best practice needs to stop requiring examining
> cls.__dict__; in fact I'd prefer people stop doing it altogether.
>

So the biggest potential breakages are code that:

1. Directly get the attribute from __dict__
2. The fact that it would no longer be inherited

Am I missing anything else?

For issue #1, it seems that inspect.get_type_hints(), as you point out
below, resolves that. As well, code could be updated appropriately without
much effort to check different places if the attribute was not found in
__dict__.

For issue #2, if the default was `None`, then couldn't that be used as an
implicit feature marker that you can't (incorrectly) rely on inheritance to
percolate up the annotations of the superclass if the subclass happens to
not define any annotations?

This all seems reasonable to me. Since it's a change to the object model it
will probably need a PEP, but I would suspect it would mostly revolve
around guiding people to how to update their code to work across Python
versions.

-Brett

>
> If we change the behavior as part of a new release of Python,
> code that examines annotations on classes can do a version check:
>
> if (sys.version_info.major >=3
> and sys.version_info.minor >= 10):
>
> def get_annotations(o):
> return o.__annotations__ or {}
> else:
> def get_annotations(o):
> # eight or ten lines of complex code goes here
> ...
>
> Or code can just use inspect.get_type_hints(), which is tied to the Python
> version
> anyway and should always do the right thing.
>
>
> */arry*
> _______________________________________________
> Python-Dev mailing list -- python-dev@python.org
> To unsubscribe send an email to python-dev-leave@python.org
> https://mail.python.org/mailman3/lists/python-dev.python.org/
> Message archived at
> https://mail.python.org/archives/list/python-dev@python.org/message/AWKVI3NRCHKPIDPCJYGVLW4HBYTEOQYL/
> Code of Conduct: http://python.org/psf/codeofconduct/
>
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
At last, a nibble on the other fishing line! ;-)


On 1/11/21 1:47 PM, Brett Cannon wrote:
> So the biggest potential breakages are code that:
>
> 1. Directly get the attribute from __dict__
> 2. The fact that it would no longer be inherited
>
> Am I missing anything else?

Those are the big ones, the ones I expect people to actually
experience.  I can name three more breakages, though these get
progressively more obscure:

* Nobody expect o.__annotations__ to ever be None (unless they
assigned None to it themselves).  If the attribute is set they
expect its value to be a dict.
* "del o.__annotations__" currently works on modules and classes if
the attribute is set.  "del fn.__annotations__" always works.
* On modules and classes you can set o.__annotations__ to any Python
value.  (Functions already only permit you to set it to None or a dict.)

I have no idea if anybody is depending on these behaviors.  The lesson
that years of Python core dev has taught me is: if Python exhibits a
behavior, somebody out there depends on it, and you'll break their code
if you change it.  Or, expressed more succinctly, any change is a
breaking change for somebody.  So the question is, is the improvement
this brings worth the breakage it also brings? In this case, I sure hope so!


> For issue #2, if the default was `None`, then couldn't that be used as
> an implicit feature marker that you can't (incorrectly) rely on
> inheritance to percolate up the annotations of the superclass if the
> subclass happens to not define any annotations?

Currently Python never sets o.__annotations__ to None on any object.  So
yes, assuming the user doesn't set it to None themselves, this would be
new behavior.  If I understand your question correctly, yes, users could
write new code that says

if o.__annotations__ is None:
    # great, we're in Python 3.10+ and no annotation was set on o!
    ....
else:
    ....

Or they could just look at sys.version_info ;-)


Thanks for your feedback,


//arry/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
I think your analysis of the problems is great. I don't worry about people
deleting `__attributes__` (and those that do can switch to calling its
.clear() method instead) but I worry about people not expecting to get
None. So if you can avoid that in your solution that would be great. The
easiest thing would be just to create an empty `__annotations__` for
classes that have no annotated variables, and to hell with the cost. People
who delete it are already living dangerously.

(I noticed that `__slots__` is missing from your list. Maybe because it
follows yet another pattern?)

On Mon, Jan 11, 2021 at 4:07 PM Larry Hastings <larry@hastings.org> wrote:

>
> At last, a nibble on the other fishing line! ;-)
>
>
> On 1/11/21 1:47 PM, Brett Cannon wrote:
>
> So the biggest potential breakages are code that:
>
> 1. Directly get the attribute from __dict__
> 2. The fact that it would no longer be inherited
>
> Am I missing anything else?
>
> Those are the big ones, the ones I expect people to actually experience.
> I can name three more breakages, though these get progressively more
> obscure:
>
> - Nobody expect o.__annotations__ to ever be None (unless they
> assigned None to it themselves). If the attribute is set they expect its
> value to be a dict.
> - "del o.__annotations__" currently works on modules and classes if
> the attribute is set. "del fn.__annotations__" always works.
> - On modules and classes you can set o.__annotations__ to any Python
> value. (Functions already only permit you to set it to None or a dict.)
>
> I have no idea if anybody is depending on these behaviors. The lesson
> that years of Python core dev has taught me is: if Python exhibits a
> behavior, somebody out there depends on it, and you'll break their code if
> you change it. Or, expressed more succinctly, any change is a breaking
> change for somebody. So the question is, is the improvement this brings
> worth the breakage it also brings? In this case, I sure hope so!
>
>
> For issue #2, if the default was `None`, then couldn't that be used as an
> implicit feature marker that you can't (incorrectly) rely on inheritance to
> percolate up the annotations of the superclass if the subclass happens to
> not define any annotations?
>
> Currently Python never sets o.__annotations__ to None on any object. So
> yes, assuming the user doesn't set it to None themselves, this would be new
> behavior. If I understand your question correctly, yes, users could write
> new code that says
>
> if o.__annotations__ is None:
> # great, we're in Python 3.10+ and no annotation was set on o!
> ....
> else:
> ....
>
> Or they could just look at sys.version_info ;-)
>
>
> Thanks for your feedback,
>
>
> */arry*
> _______________________________________________
> Python-Dev mailing list -- python-dev@python.org
> To unsubscribe send an email to python-dev-leave@python.org
> https://mail.python.org/mailman3/lists/python-dev.python.org/
> Message archived at
> https://mail.python.org/archives/list/python-dev@python.org/message/WAWHRS6RYMCOVQEFMLLRXIYLCHF4FHUJ/
> Code of Conduct: http://python.org/psf/codeofconduct/
>


--
--Guido van Rossum (python.org/~guido)
*Pronouns: he/him **(why is my pronoun here?)*
<http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/>
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On 12/01/21 6:22 am, Larry Hastings wrote:

* The language will set __annotations__ to a dict if the object has
>   annotations, or None if it has no annotations.

That sounds inconvenient -- it means that any code referencing
__annotations__ has to guard against the possibility of it being
None.

If we're changing things, I'm wondering if the best thing would be
to introduce an annotations() function as the new best practice for
getting an object's annotations. It would know how to handle all
the type-specific pecularities, and could take care of things such
as manufacturing an empty dict if the object doesn't have any
annotations.

--
Greg
_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-leave@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/DXQJ2V32YIV5KZQNFSPFVGKKVHIZDFPD/
Code of Conduct: http://python.org/psf/codeofconduct/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
Isn't that just typing.get_type_hints()?

On Mon, Jan 11, 2021 at 5:11 PM Greg Ewing <greg.ewing@canterbury.ac.nz>
wrote:

> On 12/01/21 6:22 am, Larry Hastings wrote:
>
> * The language will set __annotations__ to a dict if the object has
> > annotations, or None if it has no annotations.
>
> That sounds inconvenient -- it means that any code referencing
> __annotations__ has to guard against the possibility of it being
> None.
>
> If we're changing things, I'm wondering if the best thing would be
> to introduce an annotations() function as the new best practice for
> getting an object's annotations. It would know how to handle all
> the type-specific pecularities, and could take care of things such
> as manufacturing an empty dict if the object doesn't have any
> annotations.
>
> --
> Greg
> _______________________________________________
> Python-Dev mailing list -- python-dev@python.org
> To unsubscribe send an email to python-dev-leave@python.org
> https://mail.python.org/mailman3/lists/python-dev.python.org/
> Message archived at
> https://mail.python.org/archives/list/python-dev@python.org/message/DXQJ2V32YIV5KZQNFSPFVGKKVHIZDFPD/
> Code of Conduct: http://python.org/psf/codeofconduct/
>


--
--Guido van Rossum (python.org/~guido)
*Pronouns: he/him **(why is my pronoun here?)*
<http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/>
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On 1/11/21 4:39 PM, Guido van Rossum wrote:
> The easiest thing would be just to create an empty `__annotations__`
> for classes that have no annotated variables, and to hell with the cost.

I assume you'd keep the existing behavior where functions lazy-create an
empty dict if they have no annotations too?

That all would work fine and be consistent, but you'd probably have to
set the empty __annotations__ dict on modules too.  I've noticed that
code that examines annotations tends to handle two classes of objects:
"functions" and "not-functions".  Modules also store their
__annotations__ in their __dict__, so the same code path works fine for
examining the annotations of both classes and modules.


> (I noticed that `__slots__` is missing from your list. Maybe because
> it follows yet another pattern?)

I forgot about __slots__!  Yup, it's optional, and you can even delete
it, though after the class is defined I'm not sure how much difference
that makes.

Slots intelligently support inheritance, too.  I always kind of wondered
why annotations didn't support inheritance--if D is a subclass of C, why
doesn't D.__annotations__ contain all C's annotations too?  But we're
way past reconsidering that behavior now.


Cheers,


//arry/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On Mon, Jan 11, 2021 at 5:21 PM Larry Hastings <larry@hastings.org> wrote:

>
> On 1/11/21 4:39 PM, Guido van Rossum wrote:
>
> The easiest thing would be just to create an empty `__annotations__` for
> classes that have no annotated variables, and to hell with the cost.
>
> I assume you'd keep the existing behavior where functions lazy-create an
> empty dict if they have no annotations too?
>
Indeed -- that's trying to provide a uniform interface.

> That all would work fine and be consistent, but you'd probably have to set
> the empty __annotations__ dict on modules too. I've noticed that code that
> examines annotations tends to handle two classes of objects: "functions"
> and "not-functions". Modules also store their __annotations__ in their
> __dict__, so the same code path works fine for examining the annotations of
> both classes and modules.
>

I'm not against giving all modules an empty `__annotations__` by default.

>
> (I noticed that `__slots__` is missing from your list. Maybe because it
> follows yet another pattern?)
>
> I forgot about __slots__! Yup, it's optional, and you can even delete it,
> though after the class is defined I'm not sure how much difference that
> makes.
>
> Slots intelligently support inheritance, too. I always kind of wondered
> why annotations didn't support inheritance--if D is a subclass of C, why
> doesn't D.__annotations__ contain all C's annotations too? But we're way
> past reconsidering that behavior now.
>

Anyway, `__slots__` doesn't behave that way -- seems it behaves similar to
`__annotations__`.
```
Python 3.9.1 (tags/v3.9.1:1e5d33e, Dec 7 2020, 17:08:21) [MSC v.1927 64
bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> class A: __slots__ = ['a']
...
>>> class B(A): __slots__ = ['b']
...
>>> B.__slots__
['b']
>>> class X(A): pass
...
>>> X.__slots__
['a']
>>> A().__dict__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute '__dict__'
>>> X().__dict__
{}
>>>
```

--
--Guido van Rossum (python.org/~guido)
*Pronouns: he/him **(why is my pronoun here?)*
<http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/>
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On 1/11/21 5:28 PM, Guido van Rossum wrote:
> On Mon, Jan 11, 2021 at 5:21 PM Larry Hastings <larry@hastings.org
> <mailto:larry@hastings.org>> wrote:
>
> Slots intelligently support inheritance, too.  I always kind of
> wondered why annotations didn't support inheritance--if D is a
> subclass of C, why doesn't D.__annotations__ contain all C's
> annotations too?  But we're way past reconsidering that behavior now.
>
>
> Anyway, `__slots__` doesn't behave that way -- seems it behaves
> similar to `__annotations__`.

__slots__ itself doesn't behave that way, but subclasses do inherit the
slots defined on their parent:

class C:
    __slots__ = ['a']

class D(C):
    __slots__ = ['b']

d = D()
d.a = 5
d.b = "foo"
print(f"{d.a=} {d.b=}")

prints

d.a=5 d.b='foo'

That's the inheritance behavior I was referring to.


Cheers,


//arry/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On 1/11/21 5:05 PM, Greg Ewing wrote:
> On 12/01/21 6:22 am, Larry Hastings wrote:
>
>  * The language will set __annotations__ to a dict if the object has
>>    annotations, or None if it has no annotations.
>
> That sounds inconvenient -- it means that any code referencing
> __annotations__ has to guard against the possibility of it being
> None.

It was a balancing act.  Using an 64-byte empty dict per object with no
defined annotations seems so wasteful.  And anything short of an empty
dict, you'd have to guard against.  Current code already has to guard
against "__annotations__ aren't set" anyway, so I figured the cost of
migrating to checking a different condition would be small.  And None is
so cheap, and the guard is so easy:

if o.__annotations__:


> If we're changing things, I'm wondering if the best thing would be
> to introduce an annotations() function as the new best practice for
> getting an object's annotations. It would know how to handle all
> the type-specific pecularities, and could take care of things such
> as manufacturing an empty dict if the object doesn't have any
> annotations.

I guess I'm marginally against this, just because it seems like a
needless change.  We don't need the flexibility of a function with
optional parameters and such, and with a data descriptor we can already
put code behind __annotations__ (as I have already done). Plus, the
function should probably cache its result--you wouldn't want code that
called it ten times to generate ten fresh dicts, would you?--and already
we're most of the way to what I proposed in PEP 649.


Cheers,


//arry/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On Tue, Jan 12, 2021 at 12:56 PM Larry Hastings <larry@hastings.org> wrote:
>
> It was a balancing act. Using an 64-byte empty dict per object with no defined annotations seems so wasteful. And anything short of an empty dict, you'd have to guard against. Current code already has to guard against "__annotations__ aren't set" anyway, so I figured the cost of migrating to checking a different condition would be small. And None is so cheap, and the guard is so easy:
>
> if o.__annotations__:
>

Does it have to be mutable? If not, maybe there could be a singleton
"immutable empty dict-like object", in the same way that an empty
tuple can be put anywhere that expects a sequence. That'd be as cheap
as None (modulo a once-per-interpreter cost for the additional static
object).

ChrisA
_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-leave@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/TWBC545KC7CK3DS5JZL4WBJM4BXZZBTI/
Code of Conduct: http://python.org/psf/codeofconduct/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On 1/11/21 6:09 PM, Chris Angelico wrote:
> On Tue, Jan 12, 2021 at 12:56 PM Larry Hastings <larry@hastings.org> wrote:
>> It was a balancing act. Using an 64-byte empty dict per object with no defined annotations seems so wasteful. And anything short of an empty dict, you'd have to guard against. Current code already has to guard against "__annotations__ aren't set" anyway, so I figured the cost of migrating to checking a different condition would be small. And None is so cheap, and the guard is so easy:
>>
>> if o.__annotations__:
>>
> Does it have to be mutable? If not, maybe there could be a singleton
> "immutable empty dict-like object", in the same way that an empty
> tuple can be put anywhere that expects a sequence. That'd be as cheap
> as None (modulo a once-per-interpreter cost for the additional static
> object).

Historically, annotations dicts are mutable.  I don't know how often
people mutate them, but I would assume it's uncommon.  So technically
this would be a breaking change.  But it does seem low-risk.


Cheers,


//arry/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On 12/01/21 2:21 pm, Larry Hastings wrote:
> Slots intelligently support inheritance, too.

Are you sure about that? My experiments suggest that it has
the same problem as __annotations__:

Python 3.8.2 (default, Mar 23 2020, 11:36:18)
[Clang 8.1.0 (clang-802.0.42)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> class C:
... __slots__ = ['a', 'b']
...
>>> class D(C):
... __slots__ = ['c', 'd']
...
>>> class E(D):
... pass
...
>>> C.__slots__
['a', 'b']
>>> D.__slots__
['c', 'd']
>>> E.__slots__
['c', 'd']
>>>

--
Greg
_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-leave@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/L3OFSRVV6DGB4LXO6GY3WW7PBC2VY7AD/
Code of Conduct: http://python.org/psf/codeofconduct/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On 12/01/21 2:46 pm, Larry Hastings wrote:
>
> Using an 64-byte empty dict per object with no
> defined annotations seems so wasteful.  And anything short of an empty
> dict, you'd have to guard against.

If __annotations__ were to return a read-only mapping object
instead of a dict, the same empty object could be used for all
annotationless objects.

--
Greg
_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-leave@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/FEFCXJY52F67R6XEHS4NA3HL6MB4X3I2/
Code of Conduct: http://python.org/psf/codeofconduct/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On 1/11/21 6:31 PM, Greg Ewing wrote:
> On 12/01/21 2:21 pm, Larry Hastings wrote:
>> Slots intelligently support inheritance, too.
>
> Are you sure about that? My experiments suggest that it has
> the same problem as __annotations__:
>
> Python 3.8.2 (default, Mar 23 2020, 11:36:18)
> [Clang 8.1.0 (clang-802.0.42)] on darwin
> Type "help", "copyright", "credits" or "license" for more information.
> >>> class C:
> ...  __slots__ = ['a', 'b']
> ...
> >>> class D(C):
> ...  __slots__ = ['c', 'd']
> ...
> >>> class E(D):
> ...  pass
> ...
> >>> C.__slots__
> ['a', 'b']
> >>> D.__slots__
> ['c', 'd']
> >>> E.__slots__
> ['c', 'd']
> >>>


Guido said the same thing.  I did say "Slots", not "__slots__", though. 
You'll find that your class D supports attributes "a", "b", "c", and
"d", and that's the inheritance I was referring to.


Cheers,


//arry/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
Oh, but the behavior of annotations in e.g. mypy is the same. They are
cumulative.

On Mon, Jan 11, 2021 at 17:42 Larry Hastings <larry@hastings.org> wrote:

>
> On 1/11/21 5:28 PM, Guido van Rossum wrote:
>
> On Mon, Jan 11, 2021 at 5:21 PM Larry Hastings <larry@hastings.org> wrote:
>
>> Slots intelligently support inheritance, too. I always kind of wondered
>> why annotations didn't support inheritance--if D is a subclass of C, why
>> doesn't D.__annotations__ contain all C's annotations too? But we're way
>> past reconsidering that behavior now.
>>
>
> Anyway, `__slots__` doesn't behave that way -- seems it behaves similar to
> `__annotations__`.
>
> __slots__ itself doesn't behave that way, but subclasses do inherit the
> slots defined on their parent:
>
> class C:
> __slots__ = ['a']
>
> class D(C):
> __slots__ = ['b']
>
> d = D()
> d.a = 5
> d.b = "foo"
> print(f"{d.a=} {d.b=}")
>
> prints
>
> d.a=5 d.b='foo'
>
> That's the inheritance behavior I was referring to.
>
>
> Cheers,
>
>
> */arry*
>
--
--Guido (mobile)
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
12.01.21 03:21, Larry Hastings ????:
> I forgot about __slots__!  Yup, it's optional, and you can even delete
> it, though after the class is defined I'm not sure how much difference
> that makes.

It affects pickling if __slotnames__ is not set yet. The latter is set
when you pickle or copy an instance the first time.
_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-leave@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/YR3U2DT4ZDXOO5MXQFPVV3A53B4TQPHW/
Code of Conduct: http://python.org/psf/codeofconduct/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On Mon, Jan 11, 2021 at 5:57 PM Larry Hastings <larry@hastings.org> wrote:

>
> On 1/11/21 5:05 PM, Greg Ewing wrote:
>
> On 12/01/21 6:22 am, Larry Hastings wrote:
>
> * The language will set __annotations__ to a dict if the object has
>
> annotations, or None if it has no annotations.
>
>
> That sounds inconvenient -- it means that any code referencing
> __annotations__ has to guard against the possibility of it being
> None.
>
> It was a balancing act. Using an 64-byte empty dict per object with no
> defined annotations seems so wasteful. And anything short of an empty
> dict, you'd have to guard against. Current code already has to guard
> against "__annotations__ aren't set" anyway, so I figured the cost of
> migrating to checking a different condition would be small. And None is so
> cheap, and the guard is so easy:
>
> if o.__annotations__:
>
>
But if things could fall through in the default case such that use in a
`for` loop, it is nice as Guido pointed out.

The other thing to keep in mind is we are talking about every module,
class, and function getting 64 bytes ... which I bet isn't that much. I bet
you save more memory running with -OO than what this will cost users in
memory.

And I know you were somewhat joking when you mentioned using
sys.version_info, but since this would be behind a __future__ import it
means the version check just means you then need to *potentially* worry
about the semantic shift (until the change becomes permanent). It seems the
changes are all still easy enough to have fallthrough and semantic checks
that it won't be much of a problem. I think it really means people need to
rely on typing.get_type_hints() more than they may be doing right now.


>
> If we're changing things, I'm wondering if the best thing would be
> to introduce an annotations() function as the new best practice for
> getting an object's annotations. It would know how to handle all
> the type-specific pecularities, and could take care of things such
> as manufacturing an empty dict if the object doesn't have any
> annotations.
>
> I guess I'm marginally against this, just because it seems like a needless
> change. We don't need the flexibility of a function with optional
> parameters and such, and with a data descriptor we can already put code
> behind __annotations__ (as I have already done). Plus, the function should
> probably cache its result--you wouldn't want code that called it ten times
> to generate ten fresh dicts, would you?--and already we're most of the way
> to what I proposed in PEP 649.
>

I also don't think introspection on annotations is common enough to warrant
a built-in function; this stuff is meant for tools, not for the average
developer to be dynamically playing with. As Guido pointed out,
typing.get_type_hints() already covers this.
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On 1/12/21 5:28 PM, Brett Cannon wrote:
> The other thing to keep in mind is we are talking about every module,
> class, and function getting 64 bytes ... which I bet isn't that much.

Actually it's only every module and class.  Functions don't have this
problem because they've always stored __annotations__
internally--meaning, peeking in their __dict__ doesn't work, and they
don't support inheritance anyway.  So the number is even smaller than that.

If we can just make __annotations__ default to an empty dict on classes
and modules, and not worry about the memory consumption, that goes a
long way to cleaning up the semantics.


> And I know you were somewhat joking when you mentioned using
> sys.version_info, but since this would be behind a __future__ import

Would it?

My original proposal would make breaking changes to how you examine
__annotations__.  Let's say we put those behind a from __future__
import.  Now we're gonna write library code that examines annotations. 
A user passes in a class and asks us to examine its annotations.  The
old semantics might be active on it, or the new ones.  How do we know
which set of semantics we need to use?

It occurs to me that you could take kls.__module__, pull out the module
from sys.modules, then look inside to see if it contains the correct
"future" object imported from the __future__ module. Is that an approach
we would suggest to our users?

Also, very little code ever examines annotations; most code with
annotations merely defines them.  So I suspect most annotations users
wouldn't care either way--which also means a "from __future__ import"
that changes the semantics of examining or modifying annotations isn't
going to see a lot of uptake, because it doesn't really affect them. 
The change in semantics only affects people whose code examines
annotations, which I suspect is very few.

So I wasn't really joking when I proposed making these changes without a
from __future__ import, and suggested users use a version check.  The
library code would know based on the Python version number which
semantics were active, no peeking in modules to find future object. 
They could literally write what I suggested:

if you know you're running python 3.10 or higher:
    examine using the new semantics
else:
    examine using the old semantics

I realize that's a pretty aggressive approach, which is why I prefaced
it with "if I could wave my magic wand".  But if we're going to make
breaking changes, then whatever we do, it's going to break some people's
code until it gets updated to cope with the new semantics.  In that
light this approach seemed reasonable.

But really this is why I started this thread in the first place. My idea
of what's reasonable is probably all out of whack.  So I wanted to start
the conversation, to get feedback on how much breakage is allowable and
how best to mitigate it.  If it wasn't a controversial change, then we
wouldn't need to talk about it!


And finally: if we really do set a default of an empty dict on classes
and modules, then my other in-theory breaking changes:

* you can't delete __annotations__
* you can only set __annotations__ to a dict or None (this is already
true of functions, but not of classes or modules)

will, I expect, in practice breaking exactly zero code.  Who deletes
__annotations__?  Who ever sets __annotations__ to something besides a
dict?  So if the practical breakage is zero, why bother gating it with
"from __future__ import" at all?


> I think it really means people need to rely on typing.get_type_hints()
> more than they may be doing right now.

What I find frustrating about that answer--and part of what motivated me
to work on this in the first place--is that typing.get_type_hints()
requires your annotations to be type hints.  All type hints are
annotations, but not all annotations are type hints, and it's entirely
plausible for users to have reasonable uses for non-type-hint
annotations that typing.get_type_hints() wouldn't like.

The two things typing.get_type_hints() does, that I know of, that can
impede such non-type-hint annotations are:

* It turns a None annotation into type(None).  Which means now you
can't tell the difference between "None" and "type(None)".
* It regards all string annotations as "forward references", which
means they get eval()'d and the result returned as the annotation. 
typing.get_type_hints() doesn't catch any exceptions here, so if the
eval fails, typing.get_type_hints() fails and you can't use it to
examine your annotations.

PEP 484 "explicitly does NOT prevent other uses of annotations". But if
you force everyone to use typing.get_type_hints() to examine their
annotations, then you have de facto prevented any use of annotations
that isn't compatible with type hints.


Cheers,


//arry/
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On Tue, Jan 12, 2021 at 6:35 PM Larry Hastings <larry@hastings.org> wrote:

>
> On 1/12/21 5:28 PM, Brett Cannon wrote:
>
> The other thing to keep in mind is we are talking about every module,
> class, and function getting 64 bytes ... which I bet isn't that much.
>
> Actually it's only every module and class. Functions don't have this
> problem because they've always stored __annotations__ internally--meaning,
> peeking in their __dict__ doesn't work, and they don't support inheritance
> anyway. So the number is even smaller than that.
>
> If we can just make __annotations__ default to an empty dict on classes
> and modules, and not worry about the memory consumption, that goes a long
> way to cleaning up the semantics.
>

I would like that very much. And the exception for functions is especially
helpful.


> And I know you were somewhat joking when you mentioned using
> sys.version_info, but since this would be behind a __future__ import
>
> Would it?
>
> My original proposal would make breaking changes to how you examine
> __annotations__. Let's say we put those behind a from __future__ import.
> Now we're gonna write library code that examines annotations. A user
> passes in a class and asks us to examine its annotations. The old
> semantics might be active on it, or the new ones. How do we know which set
> of semantics we need to use?
>
> It occurs to me that you could take kls.__module__, pull out the module
> from sys.modules, then look inside to see if it contains the correct
> "future" object imported from the __future__ module. Is that an approach
> we would suggest to our users?
>

You're kidding, right?

Also, very little code ever examines annotations; most code with
> annotations merely defines them. So I suspect most annotations users
> wouldn't care either way--which also means a "from __future__ import" that
> changes the semantics of examining or modifying annotations isn't going to
> see a lot of uptake, because it doesn't really affect them. The change in
> semantics only affects people whose code examines annotations, which I
> suspect is very few.
>

I agree, but they're pretty vocal -- the breakage in get_type_hints() due
to the scope issue in 3.10 (which isn't even in beta) has drawn plenty of
complaints.

Also, dataclasses (which I have to assume is fairly popular :-) introspects
`__annotations__`, and even mutates and sets it.

So I wasn't really joking when I proposed making these changes without a
> from __future__ import, and suggested users use a version check. The
> library code would know based on the Python version number which semantics
> were active, no peeking in modules to find future object. They could
> literally write what I suggested:
>
> if you know you're running python 3.10 or higher:
> examine using the new semantics
> else:
> examine using the old semantics
>
> I realize that's a pretty aggressive approach, which is why I prefaced it
> with "if I could wave my magic wand". But if we're going to make breaking
> changes, then whatever we do, it's going to break some people's code until
> it gets updated to cope with the new semantics. In that light this
> approach seemed reasonable.
>

Is there a way that such code could be written without a version check?
E.g. for modules we could recommend `getattr(m, "__attributes__", None) or
{}`, and that would work in earlier versions too.

I'm not sure what would work for classes, since most code will want to
combine the annotations for all classes in the MRO, and the way to do that
would change -- before 3.10, you *must* use
`cls.__dict__.get("__attributes__")` whereas for 3.10+ you *must* use
`cls.__attributes__`.

Note, for a moment I thought that for modules we don't need to evaluate
annotations lazily (I know that's your other PEP/thread, but still, it
seems related). But we do, because there's an idiom where people write
```
from __future__ import annotations
import typing
if typing.TYPE_CHECKING:
from somewhere import Class
a: Class
```
Here introspecting the annotations would fail, but clearly the intention
was to use them purely for static type checking, so the user presumably
doesn't care.

(But does that mean that if a single annotation cannot be evaluated, the
entire annotations dict becomes inaccessible? That's a general weakness of
the PEP 649 scheme, right?)

But really this is why I started this thread in the first place. My idea
> of what's reasonable is probably all out of whack. So I wanted to start
> the conversation, to get feedback on how much breakage is allowable and how
> best to mitigate it. If it wasn't a controversial change, then we wouldn't
> need to talk about it!
>
>
> And finally: if we really do set a default of an empty dict on classes and
> modules, then my other in-theory breaking changes:
>
> - you can't delete __annotations__
> - you can only set __annotations__ to a dict or None (this is already
> true of functions, but not of classes or modules)
>
> will, I expect, in practice breaking exactly zero code. Who deletes
> __annotations__? Who ever sets __annotations__ to something besides a
> dict? So if the practical breakage is zero, why bother gating it with
> "from __future__ import" at all?
>

Maybe for the benefit of users who rely on some specific library that gets
the annotations out of a class dict. The library could document "don't use
that future annotations because then your annotations won't work" which
would give that library a few releases time to come up with an alternative
strategy.

>
> I think it really means people need to rely on typing.get_type_hints()
> more than they may be doing right now.
>
> What I find frustrating about that answer--and part of what motivated me
> to work on this in the first place--is that typing.get_type_hints()
> requires your annotations to be type hints. All type hints are
> annotations, but not all annotations are type hints, and it's entirely
> plausible for users to have reasonable uses for non-type-hint annotations
> that typing.get_type_hints() wouldn't like.
>
> The two things typing.get_type_hints() does, that I know of, that can
> impede such non-type-hint annotations are:
>
> - It turns a None annotation into type(None). Which means now you
> can't tell the difference between "None" and "type(None)".
> - It regards all string annotations as "forward references", which
> means they get eval()'d and the result returned as the annotation.
> typing.get_type_hints() doesn't catch any exceptions here, so if the eval
> fails, typing.get_type_hints() fails and you can't use it to examine your
> annotations.
>
> PEP 484 "explicitly does NOT prevent other uses of annotations". But if
> you force everyone to use typing.get_type_hints() to examine their
> annotations, then you have de facto prevented any use of annotations that
> isn't compatible with type hints.
>

I suspect that the most common use of annotation introspection is to
implement some kind of runtime type checking scheme (there are many of
those, I think even JSON schema verifiers based on typing.TypedDict) and
those users would presumably be fine with get_type_hints().

Note that PEP 593 introduces a way to attach arbitrary extra data to an
annotation, e.g.
```
UnsignedShort = Annotated[int, struct2.ctype('H')]
name: Annotated[str, struct2.ctype("<10s")]
```


--
--Guido van Rossum (python.org/~guido)
*Pronouns: he/him **(why is my pronoun here?)*
<http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/>
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On Tue, Jan 12, 2021 at 6:31 PM Larry Hastings <larry@hastings.org> wrote:

>
> On 1/12/21 5:28 PM, Brett Cannon wrote:
>
> The other thing to keep in mind is we are talking about every module,
> class, and function getting 64 bytes ... which I bet isn't that much.
>
> Actually it's only every module and class. Functions don't have this
> problem because they've always stored __annotations__ internally--meaning,
> peeking in their __dict__ doesn't work, and they don't support inheritance
> anyway. So the number is even smaller than that.
>
> If we can just make __annotations__ default to an empty dict on classes
> and modules, and not worry about the memory consumption, that goes a long
> way to cleaning up the semantics.
>

Great!


>
> And I know you were somewhat joking when you mentioned using
> sys.version_info, but since this would be behind a __future__ import
>
> Would it?
>

I thought you had proposed that initially, but it appears I mixed this with
your PEP email. ???? Sorry about that!


> My original proposal would make breaking changes to how you examine
> __annotations__. Let's say we put those behind a from __future__ import.
> Now we're gonna write library code that examines annotations. A user
> passes in a class and asks us to examine its annotations. The old
> semantics might be active on it, or the new ones. How do we know which set
> of semantics we need to use?
>
> It occurs to me that you could take kls.__module__, pull out the module
> from sys.modules, then look inside to see if it contains the correct
> "future" object imported from the __future__ module. Is that an approach
> we would suggest to our users?
>
> Also, very little code ever examines annotations; most code with
> annotations merely defines them. So I suspect most annotations users
> wouldn't care either way--which also means a "from __future__ import" that
> changes the semantics of examining or modifying annotations isn't going to
> see a lot of uptake, because it doesn't really affect them. The change in
> semantics only affects people whose code examines annotations, which I
> suspect is very few.
>
> So I wasn't really joking when I proposed making these changes without a
> from __future__ import, and suggested users use a version check. The
> library code would know based on the Python version number which semantics
> were active, no peeking in modules to find future object. They could
> literally write what I suggested:
>
> if you know you're running python 3.10 or higher:
> examine using the new semantics
> else:
> examine using the old semantics
>
> I realize that's a pretty aggressive approach, which is why I prefaced it
> with "if I could wave my magic wand". But if we're going to make breaking
> changes, then whatever we do, it's going to break some people's code until
> it gets updated to cope with the new semantics. In that light this
> approach seemed reasonable.
>
> But really this is why I started this thread in the first place. My idea
> of what's reasonable is probably all out of whack. So I wanted to start
> the conversation, to get feedback on how much breakage is allowable and how
> best to mitigate it. If it wasn't a controversial change, then we wouldn't
> need to talk about it!
>
>
> And finally: if we really do set a default of an empty dict on classes and
> modules, then my other in-theory breaking changes:
>
> - you can't delete __annotations__
> - you can only set __annotations__ to a dict or None (this is already
> true of functions, but not of classes or modules)
>
> will, I expect, in practice breaking exactly zero code. Who deletes
> __annotations__? Who ever sets __annotations__ to something besides a
> dict? So if the practical breakage is zero, why bother gating it with
> "from __future__ import" at all?
>
>
> I think it really means people need to rely on typing.get_type_hints()
> more than they may be doing right now.
>
> What I find frustrating about that answer--and part of what motivated me
> to work on this in the first place--is that typing.get_type_hints()
> requires your annotations to be type hints. All type hints are
> annotations, but not all annotations are type hints, and it's entirely
> plausible for users to have reasonable uses for non-type-hint annotations
> that typing.get_type_hints() wouldn't like.
>

You and I have talked about this extensively, so I'm aware. ????


> The two things typing.get_type_hints() does, that I know of, that can
> impede such non-type-hint annotations are:
>
> - It turns a None annotation into type(None). Which means now you
> can't tell the difference between "None" and "type(None)".
>
> Huh, I wasn't aware of that.

-Brett


>
> -
> - It regards all string annotations as "forward references", which
> means they get eval()'d and the result returned as the annotation.
> typing.get_type_hints() doesn't catch any exceptions here, so if the eval
> fails, typing.get_type_hints() fails and you can't use it to examine your
> annotations.
>
> PEP 484 "explicitly does NOT prevent other uses of annotations". But if
> you force everyone to use typing.get_type_hints() to examine their
> annotations, then you have de facto prevented any use of annotations that
> isn't compatible with type hints.
>
>
> Cheers,
>
>
> */arry*
>
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On Tue, Jan 12, 2021 at 8:00 PM Brett Cannon <brett@python.org> wrote:

>
>
>> - It turns a None annotation into type(None). Which means now you
>> can't tell the difference between "None" and "type(None)".
>>
>> Huh, I wasn't aware of that.
>

This has tripped up many people. Maybe we should just bite the bullet and
change this?

--
--Guido van Rossum (python.org/~guido)
*Pronouns: he/him **(why is my pronoun here?)*
<http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/>
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On Tue, 2021-01-12 at 20:09 -0800, Guido van Rossum wrote:
> On Tue, Jan 12, 2021 at 8:00 PM Brett Cannon <brett@python.org>
> wrote:
> >
> > > * It turns a None annotation into type(None).  Which means now
> > > you
> > > can't tell the difference between "None" and "type(None)".
> > >
> > Huh, I wasn't aware of that.
> >
>
> This has tripped up many people. Maybe we should just bite the bullet
> and change this?

+1, FWIW.
Re: Let's Fix Class Annotations -- And Maybe Annotations Generally [ In reply to ]
On 13/01/21 3:31 pm, Larry Hastings wrote:
>
> Let's say we put those behind a from __future__
> import.  Now we're gonna write library code that examines annotations.
> A user passes in a class and asks us to examine its annotations.  The
> old semantics might be active on it, or the new ones.  How do we know
> which set of semantics we need to use?

This implies that __future__ is the wrong mechanism to use.
It's only appropriate when the changes it triggers are confined
to the module that uses it, which is not the case here.

--
Greg
_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-leave@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/3R3U2DSRVJRPFCXFCGMZG7KRK53THSGZ/
Code of Conduct: http://python.org/psf/codeofconduct/

1 2  View All