Skip to content

Commit 43a28ef

Browse files
PEP 726: Module __setattr__ and __delattr__ (#3301)
Co-authored-by: Adam Turner <[email protected]>
1 parent f70b3ba commit 43a28ef

File tree

2 files changed

+189
-0
lines changed

2 files changed

+189
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,7 @@ pep-0721.rst @encukou
602602
pep-0722.rst @pfmoore
603603
pep-0723.rst @AA-Turner
604604
pep-0725.rst @pradyunsg
605+
pep-0726.rst @AA-Turner
605606
pep-0727.rst @JelleZijlstra
606607
# ...
607608
# pep-0754.txt

pep-0726.rst

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
PEP: 726
2+
Title: Module ``__setattr__`` and ``__delattr__``
3+
Author: Sergey B Kirpichev <[email protected]>
4+
Sponsor: Adam Turner <[email protected]>
5+
Discussions-To: https://discuss.python.org/t/32640/
6+
Status: Draft
7+
Type: Standards Track
8+
Content-Type: text/x-rst
9+
Created: 24-Aug-2023
10+
Python-Version: 3.13
11+
Post-History: `06-Apr-2023 <https://discuss.python.org/t/25506/>`__,
12+
`31-Aug-2023 <https://discuss.python.org/t/32640/>`__,
13+
14+
15+
Abstract
16+
========
17+
18+
This PEP proposes supporting user-defined ``__setattr__``
19+
and ``__delattr__`` methods on modules to extend customization
20+
of module attribute access beyond :pep:`562`.
21+
22+
Motivation
23+
==========
24+
25+
There are several potential uses of a module ``__setattr__``:
26+
27+
1. To prevent setting an attribute at all (i.e. make it read-only)
28+
2. To validate the value to be assigned
29+
3. To intercept setting an attribute and update some other state
30+
31+
Proper support for read-only attributes would also require adding the
32+
``__delattr__`` function to prevent their deletion.
33+
34+
A typical workaround is assigning the ``__class__`` of a module object to a
35+
custom subclass of :py:class:`python:types.ModuleType` (see [1]_).
36+
Unfortunately, this also brings a noticeable speed regression
37+
(~2-3x) for attribute *access*. It would be convenient to directly
38+
support such customization, by recognizing ``__setattr__`` and ``__delattr__``
39+
methods defined in a module that would act like normal
40+
:py:meth:`python:object.__setattr__` and :py:meth:`python:object.__delattr__`
41+
methods, except that they will be defined on module *instances*.
42+
43+
For example
44+
45+
.. code:: python
46+
47+
# mplib.py
48+
49+
CONSTANT = 3.14
50+
prec = 53
51+
dps = 15
52+
53+
def dps_to_prec(n):
54+
"""Return the number of bits required to represent n decimals accurately."""
55+
return max(1, int(round((int(n)+1)*3.3219280948873626)))
56+
57+
def prec_to_dps(n):
58+
"""Return the number of accurate decimals that can be represented with n bits."""
59+
return max(1, int(round(int(n)/3.3219280948873626)-1))
60+
61+
def validate(n):
62+
n = int(n)
63+
if n <= 0:
64+
raise ValueError('non-negative integer expected')
65+
return n
66+
67+
def __setattr__(name, value):
68+
if name == 'CONSTANT':
69+
raise AttributeError('Read-only attribute!')
70+
if name == 'dps':
71+
value = validate(value)
72+
globals()['dps'] = value
73+
globals()['prec'] = dps_to_prec(value)
74+
return
75+
if name == 'prec':
76+
value = validate(value)
77+
globals()['prec'] = value
78+
globals()['dps'] = prec_to_dps(value)
79+
return
80+
globals()[name] = value
81+
82+
def __delattr__(name):
83+
if name in ('CONSTANT', 'dps', 'prec'):
84+
raise AttributeError('Read-only attribute!')
85+
del globals()[name]
86+
87+
.. code:: pycon
88+
89+
>>> import mplib
90+
>>> mplib.foo = 'spam'
91+
>>> mplib.CONSTANT = 42
92+
Traceback (most recent call last):
93+
...
94+
AttributeError: Read-only attribute!
95+
>>> del mplib.foo
96+
>>> del mplib.CONSTANT
97+
Traceback (most recent call last):
98+
...
99+
AttributeError: Read-only attribute!
100+
>>> mplib.prec
101+
53
102+
>>> mplib.dps
103+
15
104+
>>> mplib.dps = 5
105+
>>> mplib.prec
106+
20
107+
>>> mplib.dps = 0
108+
Traceback (most recent call last):
109+
...
110+
ValueError: non-negative integer expected
111+
112+
113+
Specification
114+
=============
115+
116+
The ``__setattr__`` function at the module level should accept two
117+
arguments, the name of an attribute and the value to be assigned,
118+
and return :py:obj:`None` or raise an :exc:`AttributeError`.
119+
120+
.. code:: python
121+
122+
def __setattr__(name: str, value: typing.Any, /) -> None: ...
123+
124+
The ``__delattr__`` function should accept one argument,
125+
the name of an attribute, and return :py:obj:`None` or raise an
126+
:py:exc:`AttributeError`:
127+
128+
.. code:: python
129+
130+
def __delattr__(name: str, /) -> None: ...
131+
132+
The ``__setattr__`` and ``__delattr__`` functions are looked up in the
133+
module ``__dict__``. If present, the appropriate function is called to
134+
customize setting the attribute or its deletion, else the normal
135+
mechanism (storing/deleting the value in the module dictionary) will work.
136+
137+
Defining ``__setattr__`` or ``__delattr__`` only affect lookups made
138+
using the attribute access syntax---directly accessing the module
139+
globals is unaffected, e.g. ``sys.modules[__name__].some_global = 'spam'``.
140+
141+
142+
How to Teach This
143+
=================
144+
145+
The "Customizing module attribute access" [1]_ section of the documentation
146+
will be expanded to include new functions.
147+
148+
149+
Reference Implementation
150+
========================
151+
152+
The reference implementation for this PEP can be found in `CPython PR #108261
153+
<https://github.com/python/cpython/pull/108261>`__.
154+
155+
156+
Backwards compatibility
157+
=======================
158+
159+
This PEP may break code that uses module level (global) names
160+
``__setattr__`` and ``__delattr__``, but the language reference
161+
explicitly reserves *all* undocumented dunder names, and allows
162+
"breakage without warning" [2]_.
163+
164+
The performance implications of this PEP are small, since additional
165+
dictionary lookup is much cheaper than storing/deleting the value in
166+
the dictionary. Also it is hard to imagine a module that expects the
167+
user to set (and/or delete) attributes enough times to be a
168+
performance concern. On another hand, proposed mechanism allows to
169+
override setting/deleting of attributes without affecting speed of
170+
attribute access, which is much more likely scenario to get a
171+
performance penalty.
172+
173+
174+
Footnotes
175+
=========
176+
177+
.. [1] Customizing module attribute access
178+
(https://docs.python.org/3.11/reference/datamodel.html#customizing-module-attribute-access)
179+
180+
.. [2] Reserved classes of identifiers
181+
(https://docs.python.org/3.11/reference/lexical_analysis.html#reserved-classes-of-identifiers)
182+
183+
184+
Copyright
185+
=========
186+
187+
This document is placed in the public domain or under the
188+
CC0-1.0-Universal license, whichever is more permissive.

0 commit comments

Comments
 (0)