Skip to content
Permalink
main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time

PEP: 562 Title: Module __getattr__ and __dir__ Author: Ivan Levkivskyi <levkivskyi@gmail.com> Status: Final Type: Standards Track Content-Type: text/x-rst Created: 09-Sep-2017 Python-Version: 3.7 Post-History: 09-Sep-2017 Resolution: https://mail.python.org/pipermail/python-dev/2017-December/151033.html

Abstract

It is proposed to support __getattr__ and __dir__ function defined on modules to provide basic customization of module attribute access.

Rationale

It is sometimes convenient to customize or otherwise have control over access to module attributes. A typical example is managing deprecation warnings. Typical workarounds are assigning __class__ of a module object to a custom subclass of types.ModuleType or replacing the sys.modules item with a custom wrapper instance. It would be convenient to simplify this procedure by recognizing __getattr__ defined directly in a module that would act like a normal __getattr__ method, except that it will be defined on module instances. For example:

# lib.py

from warnings import warn

deprecated_names = ["old_function", ...]

def _deprecated_old_function(arg, other):
    ...

def __getattr__(name):
    if name in deprecated_names:
        warn(f"{name} is deprecated", DeprecationWarning)
        return globals()[f"_deprecated_{name}"]
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

# main.py

from lib import old_function  # Works, but emits the warning

Another widespread use case for __getattr__ would be lazy submodule imports. Consider a simple example:

# lib/__init__.py

import importlib

__all__ = ['submod', ...]

def __getattr__(name):
    if name in __all__:
        return importlib.import_module("." + name, __name__)
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

# lib/submod.py

print("Submodule loaded")
class HeavyClass:
    ...

# main.py

import lib
lib.submod.HeavyClass  # prints "Submodule loaded"

There is a related proposal PEP 549 that proposes to support instance properties for a similar functionality. The difference is this PEP proposes a faster and simpler mechanism, but provides more basic customization. An additional motivation for this proposal is that PEP 484 already defines the use of module __getattr__ for this purpose in Python stub files, see :pep:`484#stub-files`.

In addition, to allow modifying result of a dir() call on a module to show deprecated and other dynamically generated attributes, it is proposed to support module level __dir__ function. For example:

# lib.py

deprecated_names = ["old_function", ...]
__all__ = ["new_function_one", "new_function_two", ...]

def new_function_one(arg, other):
   ...
def new_function_two(arg, other):
    ...

def __dir__():
    return sorted(__all__ + deprecated_names)

# main.py

import lib

dir(lib)  # prints ["new_function_one", "new_function_two", "old_function", ...]

Specification

The __getattr__ function at the module level should accept one argument which is the name of an attribute and return the computed value or raise an AttributeError:

def __getattr__(name: str) -> Any: ...

If an attribute is not found on a module object through the normal lookup (i.e. object.__getattribute__), then __getattr__ is searched in the module __dict__ before raising an AttributeError. If found, it is called with the attribute name and the result is returned. Looking up a name as a module global will bypass module __getattr__. This is intentional, otherwise calling __getattr__ for builtins will significantly harm performance.

The __dir__ function should accept no arguments, and return a list of strings that represents the names accessible on module:

def __dir__() -> List[str]: ...

If present, this function overrides the standard dir() search on a module.

The reference implementation for this PEP can be found in [2].

Backwards compatibility and impact on performance

This PEP may break code that uses module level (global) names __getattr__ and __dir__. (But the language reference explicitly reserves all undocumented dunder names, and allows "breakage without warning"; see [3].) The performance implications of this PEP are minimal, since __getattr__ is called only for missing attributes.

Some tools that perform module attributes discovery might not expect __getattr__. This problem is not new however, since it is already possible to replace a module with a module subclass with overridden __getattr__ and __dir__, but with this PEP such problems can occur more often.

Discussion

Note that the use of module __getattr__ requires care to keep the referred objects pickleable. For example, the __name__ attribute of a function should correspond to the name with which it is accessible via __getattr__:

def keep_pickleable(func):
    func.__name__ = func.__name__.replace('_deprecated_', '')
    func.__qualname__ = func.__qualname__.replace('_deprecated_', '')
    return func

@keep_pickleable
def _deprecated_old_function(arg, other):
    ...

One should be also careful to avoid recursion as one would do with a class level __getattr__.

To use a module global with triggering __getattr__ (for example if one wants to use a lazy loaded submodule) one can access it as:

sys.modules[__name__].some_global

or as:

from . import some_global

Note that the latter sets the module attribute, thus __getattr__ will be called only once.

References

[2]The reference implementation (https://github.com/ilevkivskyi/cpython/pull/3/files)
[3]Reserved classes of identifiers (https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers)

Copyright

This document has been placed in the public domain.