Skip to content

Types for singleton objects #236

Closed
Closed
@JukkaL

Description

@JukkaL

I repeatedly encounter code that uses a singleton instance that is only used to mark some special condition. Here is an example:

_empty = object()

def f(x=_empty):
    if x is _empty:
        # default argument value
        return 0
    elif x is None:
        # argument was provided and it's None
        return 1
    else:
        # non-None argument provided
       return x * 2

Currently PEP 484 doesn't provide primitives for type checking such code precisely. Workarounds such as casts or declaring the type of the special object as Any are possible. We can't use the type object in a union type, since that would cover all possible values, as object is the common base class of everything.

We could refactor the code to use a separate class for the special object and use isinstance to check for the special value, but this may require changing the code in non-trivial ways for larger examples and carries some runtime overhead as isinstance checks are slower than is operations. Example:

from typing import Union

class Empty: pass

_empty = Empty()

def f(x: Union[int, None, Empty] = _empty) -> int:
    # use isinstance so that type checkers can recognize that we are decomposing a union
    if isinstance(x, Empty):
        # default argument value
        return 0
    elif x is None:
        # argument was provided and it's None
        return 1
    else:
        # non-None argument provided
       return x * 2

A potentially better way is to support user-defined types that only include a single instance. This can be seen as a generalization of the None type. Here is a potential way to use this to type check the example:

from typing import SingletonType, Union

_empty = object()

Empty = SingletonType('Empty', _empty)

def f(x: Union[int, None, Empty] = _empty) -> int:
    if x is _empty:
        # default argument value
        return 0
    elif x is None:
        # argument was provided and it's None
        return 1
    else:
        # non-None argument provided
       return x * 2

The final example is better than second example in a few ways:

  • It's closer to the original code.
  • It's as fast as the original code (modulo one-time overhead due to type annotations).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions