Using @public and @private

This library provies two very simple decorators that document the publicness of the names in your module. They keep your module’s __all__ in sync so you don’t have to.

Background

__all__ is great. It has both functional and documentation purposes.

The functional purpose is that it directly controls which module names are imported by the from <module> import * statement. In the absence of an __all__, when this statement is executed, every name in <module> that does not start with an underscore will be imported. This often leads to importing too many names into the module. That’s a good enough reason not to use from <module> import * with modules that don’t have an __all__.

In the presence of an __all__, only the names specified in this list are imported by the from <module> import * statement. This in essence gives the <module> author a way to explicitly state which names are for public consumption.

And that’s the second purpose of __all__; it serves as module documentation, explicitly naming the public objects it wants to export. You can print a module’s __all__ and get an explicit declaration of its public API.

The problem

__all__ has two problems.

First, it separates the declaration of a name’s public export semantics from the implementation of that name. Usually the __all__ is put at the top of the module, although this isn’t required, and in some cases it’s actively prohibited. So when you’re looking at the definition of a function or class in a module, you have to search for the __all__ definition to know whether the function or class is intended for public consumption.

This leads to the second problem, which is that it’s too easy for the __all__ to get out of sync with the module’s contents. Often a function or class is renamed, removed, or added without the __all__ being updated. Then it’s difficult to know what the module author’s intent was, and it can lead to an exception when a string appearing in __all__ doesn’t match an existing name in the module. Some tools like Sphinx will complain when names appear in __all__ don’t appear in the module. All of this points to the root problem; it should be easy to keep __all__ in sync!

The solution

This package provides a way to declare a name’s publicness right at the point of its declaration, and to infer the name to export from that definition. In this way, a module’s author never explicitly sets the __all__ so there’s no way for it to get out of sync.

This package, and Python issue 26632, propose just such a solution, in the form of a public builtin that can be used as either a decorator, or a callable.

>>> from public import public

You’ll usually use this as a decorator, for example:

>>> @public
... def foo():
...    pass

or:

>>> @public
... class Bar:
...     pass

The __all__ after both of those code snippets has both names in it:

>>> print(__all__)
['foo', 'Bar']

Note that you do not need to initialize __all__ in the module, since public will do it for you. Of course, if your module already has an __all__, it will add any new names to the existing list.

The requirements to use the @public decorator are simple: the decorated thing must have a __name__ attribute. Since you’ll overwhelmingly use it to decorate functions and classes, this will always be the case. If the object has a __module__ attribute, that string is used to look up the module object in sys.modules, otherwise the module is extracted from the globals where the decorator is called.

There’s one other common use case that isn’t covered by the @public decorator. Sometimes you want to declare simple constants or instances as publicly available. You can’t use the @public decorator for two reasons: constants don’t have a __name__ and Python’s syntax doesn’t allow you to decorate such constructs.

To solve this use case, public is also a callable function accepting keyword arguments. An example makes this obvious:

>>> public(SEVEN=7)
>>> public(a_bar=Bar())

The module’s __all__ now contains both of the keys:

>>> print(__all__)
['foo', 'Bar', 'SEVEN', 'a_bar']

and as should be obvious, the module contains name bindings for these constants:

>>> print(SEVEN)
7
>>> print(a_bar)
<....Bar object at ...>

Multiple keyword arguments are allowed:

>>> public(ONE=1, TWO=2)
>>> print(__all__)
['foo', 'Bar', 'SEVEN', 'a_bar', 'ONE', 'TWO']

>>> print(ONE)
1
>>> print(TWO)
2

@private

You might also want to be explicit about your private, i.e. non-public names. This library also provides an @private decorator for this purpose. While it mostly serves for documentation purposes, this decorator also ensures that the decorated object’s name does not appear in the __all__:

>>> from public import private

>>> @private
... def foo():
...    pass

>>> print(__all__)
['Bar', 'SEVEN', 'a_bar', 'ONE', 'TWO']

You can see here that foo has been removed from the __all__. It’s okay if the name doesn’t appear in __all__ at all:

>>> @private
... class Baz:
...     pass

>>> print(__all__)
['Bar', 'SEVEN', 'a_bar', 'ONE', 'TWO']

In this case, Baz never appears in __all__. Like with @public, the @private decorator will add any missing __all__, but if it exists in the module, it must be a list. There is no functional API for @private.

Making @public and @private built-ins

It can get rather tedious if you have to add the above import in every module where you want to use it. What if you could put public into Python’s builtins? Then it would be available in all your code for free:

>>> from public import install
>>> install()

and now you can just use @public and @private without having to import anything in your other modules.

Caveats

There are some important usage restrictions you should be aware of:

  • Only use @public and @private on top-level object. Specifically, don’t try to use either decorator on a class method name. While the declaration won’t fail, you will get an exception when you attempt to from <module> import * because the name pulled from __all__ won’t be in the module’s globals.

  • If you explicitly set __all__ in your module, be sure to set it to a list. Some style guides require __all__ to be a tuple, but since that’s immutable, as soon as @public tries to append to it, you will get an exception. Best practice is to not set __all__ explicitly; let @public and @private do it!

  • If you still want __all__ to be immutable, put the following at the bottom of your module:

    __all__ = tuple(__all__)
    

Alternatives

This isn’t a unique approach to @public. Other implementations do exist. There are some subtle differences between this package and those others. This package:

  • uses keyword arguments to map names which don’t have an __name__ attribute;

  • can be used to bind names and values into a module’s globals;

  • can optionally put public in builtins.