Skip to content

PEP 726: Module __setattr__ and __delattr__ #3301

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

Merged
merged 11 commits into from
Aug 31, 2023
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ pep-0721.rst @encukou
pep-0722.rst @pfmoore
pep-0723.rst @AA-Turner
pep-0725.rst @pradyunsg
pep-0726.rst @AA-Turner
pep-0727.rst @JelleZijlstra
# ...
# pep-0754.txt
Expand Down
188 changes: 188 additions & 0 deletions pep-0726.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
PEP: 726
Title: Module ``__setattr__`` and ``__delattr__``
Author: Sergey B Kirpichev <[email protected]>
Sponsor: Adam Turner <[email protected]>
Discussions-To: https://discuss.python.org/t/32640/
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Created: 24-Aug-2023
Python-Version: 3.13
Post-History: `06-Apr-2023 <https://discuss.python.org/t/25506/>`__,
`31-Aug-2023 <https://discuss.python.org/t/32640/>`__,


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__``:

1. To prevent setting an attribute at all (i.e. make it read-only)
2. To validate the value to be assigned
3. 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 :py:class:`python: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
:py:meth:`python:object.__setattr__` and :py:meth:`python:object.__delattr__`
methods, except that they will be defined on module *instances*.

For example

.. code:: python

# 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]

.. code:: pycon

>>> 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 :py:obj:`None` or raise an :exc:`AttributeError`.

.. code:: python

def __setattr__(name: str, value: typing.Any, /) -> None: ...

The ``__delattr__`` function should accept one argument,
the name of an attribute, and return :py:obj:`None` or raise an
:py:exc:`AttributeError`:

.. code:: python

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 ``__setattr__`` or ``__delattr__`` only affect lookups made
using the attribute access syntax---directly accessing the module
globals is unaffected, e.g. ``sys.modules[__name__].some_global = 'spam'``.


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
<https://github.com/python/cpython/pull/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
=========

.. [1] Customizing module attribute access
(https://docs.python.org/3.11/reference/datamodel.html#customizing-module-attribute-access)

.. [2] Reserved classes of identifiers
(https://docs.python.org/3.11/reference/lexical_analysis.html#reserved-classes-of-identifiers)


Copyright
=========

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