PEP: 9999 Title: Module __setattr__ and __delattr__ Author: Sergey B Kirpichev Sponsor: TBD Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 24-Apr-2023 Python-Version: 3.13 Discussions-To: TBD Post-History: `6-Apr-2023 `__ Abstract ======== It is proposed to support ``__setattr__`` and ``__delattr__`` methods defined 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 attribute at all (make one 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 ``__delattr__`` helper function to prevent their deletion as well. Typical workaround is assigning ``__class__`` of a module object to a custom subclass of :py:class:`python:types.ModuleType` (see [1]_). Unfortunately, this also coming with a noticiable speed regression (~2-3x) for attribute *access*. It would be convenient to directly support such customizations, e.g. having a clear way to make module attributes effectively read-only, by recognizing ``__setattr__``/``__delattr__`` methods defined in a module that would act like a normal :py:meth:`python:object.__setattr__`/:py:meth:`python:object.__delattr__` methods, except that they will be defined on module *instances*. For example:: # cat 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] # python -q >>> 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, respectively, the name of an attribute and the value to be assigned, and return ``None`` or raise an ``AttributeError``:: def __setattr__(name: str, value: Any) -> None: ... The ``__delattr__`` function should accept one argument which is the name of an attribute and return ``None`` or raise an ``AttributeError``:: def __delattr__(name: str): -> None: ... The ``__setattr__``/``__delattr__`` functions are searched in the module ``__dict__``. If present, suitable function is called to customize setting of the attribute or it's deletion, else the normal mechanism (storing/deleting the value in the module dictionary) will work. Defining ``__setattr__``/``__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'``. The reference implementation for this PEP can be found in [2]_. 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" [3]_. 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. Copyright ========= This document has been placed in the public domain. References ========== .. [1] Customizing module attribute access (https://docs.python.org/3/reference/datamodel.html#customizing-module-attribute-access) .. [2] The reference implementation (https://github.com/skirpichev/cpython/tree/module-setdelattr) .. [3] Reserved classes of identifiers (https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers)