PEP 726 – Module __setattr__
and __delattr__
- Author:
- Sergey B Kirpichev <skirpichev at gmail.com>
- Sponsor:
- Adam Turner <python at quite.org.uk>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Created:
- 24-Aug-2023
- Python-Version:
- 3.13
- Post-History:
- 06-Apr-2023, 31-Aug-2023
Abstract
This PEP proposes supporting user-defined __setattr__
and __delattr__
methods on modules to extend customization
of module attribute access beyond PEP 562.
Motivation
There are several potential uses of a module __setattr__
:
- To prevent setting an attribute at all (i.e. make it read-only)
- To validate the value to be assigned
- To intercept setting an attribute and update some other state
Proper support for read-only attributes would also require adding the
__delattr__
function to prevent their deletion.
A typical workaround is assigning the __class__
of a module object to a
custom subclass of types.ModuleType
(see [1]).
Unfortunately, this also brings a noticeable speed regression
(~2-3x) for attribute access. It would be convenient to directly
support such customization, by recognizing __setattr__
and __delattr__
methods defined in a module that would act like normal
object.__setattr__()
and object.__delattr__()
methods, except that they will be defined on module instances.
For example
# mplib.py
CONSTANT = 3.14
prec = 53
dps = 15
def dps_to_prec(n):
"""Return the number of bits required to represent n decimals accurately."""
return max(1, int(round((int(n)+1)*3.3219280948873626)))
def prec_to_dps(n):
"""Return the number of accurate decimals that can be represented with n bits."""
return max(1, int(round(int(n)/3.3219280948873626)-1))
def validate(n):
n = int(n)
if n <= 0:
raise ValueError('non-negative integer expected')
return n
def __setattr__(name, value):
if name == 'CONSTANT':
raise AttributeError('Read-only attribute!')
if name == 'dps':
value = validate(value)
globals()['dps'] = value
globals()['prec'] = dps_to_prec(value)
return
if name == 'prec':
value = validate(value)
globals()['prec'] = value
globals()['dps'] = prec_to_dps(value)
return
globals()[name] = value
def __delattr__(name):
if name in ('CONSTANT', 'dps', 'prec'):
raise AttributeError('Read-only attribute!')
del globals()[name]
>>> import mplib
>>> mplib.foo = 'spam'
>>> mplib.CONSTANT = 42
Traceback (most recent call last):
...
AttributeError: Read-only attribute!
>>> del mplib.foo
>>> del mplib.CONSTANT
Traceback (most recent call last):
...
AttributeError: Read-only attribute!
>>> mplib.prec
53
>>> mplib.dps
15
>>> mplib.dps = 5
>>> mplib.prec
20
>>> mplib.dps = 0
Traceback (most recent call last):
...
ValueError: non-negative integer expected
Specification
The __setattr__
function at the module level should accept two
arguments, the name of an attribute and the value to be assigned,
and return None
or raise an AttributeError
.
def __setattr__(name: str, value: typing.Any, /) -> None: ...
The __delattr__
function should accept one argument,
the name of an attribute, and return None
or raise an
AttributeError
:
def __delattr__(name: str, /) -> None: ...
The __setattr__
and __delattr__
functions are looked up in the
module __dict__
. If present, the appropriate function is called to
customize setting the attribute or its deletion, else the normal
mechanism (storing/deleting the value in the module dictionary) will work.
Defining module __setattr__
or __delattr__
only affects lookups made
using the attribute access syntax—directly accessing the module globals
(whether by globals()
within the module, or via a reference to the module’s
globals dictionary) is unaffected. For example:
>>> import mod
>>> mod.__dict__['foo'] = 'spam' # bypasses __setattr__, defined in mod.py
or
# mod.py
def __setattr__(name, value):
...
foo = 'spam' # bypasses __setattr__
globals()['bar'] = 'spam' # here too
def f():
global x
x = 123
f() # and here
To use a module global and trigger __setattr__
(or __delattr__
),
one can access it via sys.modules[__name__]
within the module’s code:
# mod.py
sys.modules[__name__].foo = 'spam' # bypasses __setattr__
def __setattr__(name, value):
...
sys.modules[__name__].bar = 'spam' # triggers __setattr__
How to Teach This
The “Customizing module attribute access” [1] section of the documentation will be expanded to include new functions.
Reference Implementation
The reference implementation for this PEP can be found in CPython PR #108261.
Backwards compatibility
This PEP may break code that uses module level (global) names
__setattr__
and __delattr__
, but the language reference
explicitly reserves all undocumented dunder names, and allows
“breakage without warning” [2].
The performance implications of this PEP are small, since additional dictionary lookup is much cheaper than storing/deleting the value in the dictionary. Also it is hard to imagine a module that expects the user to set (and/or delete) attributes enough times to be a performance concern. On another hand, proposed mechanism allows to override setting/deleting of attributes without affecting speed of attribute access, which is much more likely scenario to get a performance penalty.
Footnotes
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
Source: https://github.com/python/peps/blob/main/peps/pep-0726.rst
Last modified: 2023-09-09 17:39:29 GMT