8

I'm using argspec in a function that takes another function or method as the argument, and returns a tuple like this:

(("arg1", obj1), ("arg2", obj2), ...)

This means that the first argument to the passed function is arg1 and it has a default value of obj1, and so on.

Here's the rub: if it has no default value, I need a placeholder value to signify this. I can't use None, because then I can't distinguish between no default value and default value is None. Same for False, 0, -1, etc. I could make it a tuple with a single element, but then the code for checking it would be ugly, and I can't easily turn it into a dict. So I thought I'd create a None-like object that isn't None, and this is what I've come up with:

class MetaNoDefault(type):
    def __repr__(cls):
        return cls.__name__
    __str__ = __repr__

class NoDefault(object):
    __metaclass__ = MetaNoDefault

Now ("arg1", NoDefault) indicates arg1 has no default value, and I can do things like if obj1 is NoDefault: etc. The metaclass makes it print as just NoDefault instead of <class '__main__.NoDefault'>.

Is there any reason not to do it like this? Is there a better solution?

Lauritz V. Thaulow
  • 49,139
  • 12
  • 73
  • 92
  • Is there any reason why you can not have tuple of just one element for no defaults? Something like: `(("arg1",), ("arg2", obj2), ...)` where "arg1" has no default. You can then do if `len(the_tuple) == 1` etc. – Fenikso May 31 '11 at 10:05
  • 2
    @FMc: In order to avoid mistakes coming from the accidental modification of the default arguments, it is better to never use mutable defaults. –  May 31 '11 at 10:07
  • @Fenikso I've covered this possibility in my question. In short, I find it inelegant, even though it _is_ intuitive. – Lauritz V. Thaulow May 31 '11 at 10:21

4 Answers4

5

I had a similar situation some time ago. Here's what I came up with.

# Works like None, but is also a no-op callable and empty iterable.
class NULLType(type):
    def __new__(cls, name, bases, dct):
        return type.__new__(cls, name, bases, dct)
    def __init__(cls, name, bases, dct):
        super(NULLType, cls).__init__(name, bases, dct)
    def __str__(self):
        return ""
    def __repr__(self):
        return "NULL"
    def __nonzero__(self):
        return False
    def __len__(self):
        return 0
    def __call__(self, *args, **kwargs):
        return None
    def __contains__(self, item):
        return False
    def __iter__(self):
        return self
    def next(*args):
        raise StopIteration
NULL = NULLType("NULL", (type,), {})

It can also act as a null callable and sequence.

Keith
  • 42,110
  • 11
  • 57
  • 76
4

There isn't any reason to not use such sentinel objects for such purposes. As an alternative to a class object, you could also create a singleton instance of a dynamically created type:

NoDefault = type('NoDefault', (object,), {
    '__str__': lambda s: 'NoDefault', '__repr__': lambda s: 'NoDefault'})()
4

My favourite sentinel is Ellipsis, and if I may quote myself:

it's there, it's an object, it's a singleton, and its name means "lack of", and it's not the overused None (which could be put in a queue as part of normal data flow). YMMV.

Community
  • 1
  • 1
tzot
  • 92,761
  • 29
  • 141
  • 204
  • 1
    However, it's disappearing in Python 3. :-o – Keith May 31 '11 at 11:36
  • 4
    @Keith: citation, please. AFAIK in Python 3 you can even say `a = ...` (i.e. the `...` syntax can be used outside of slices), so I have the impression that it's not disappearing. – tzot May 31 '11 at 12:26
  • It seems I could quote myself from [even earlier](http://mail.python.org/pipermail/python-list/2004-August/273139.html) :) – tzot May 31 '11 at 14:30
  • 1
    @Keith: `Ellipsis` is still present in Python 3. Just read the documentation for proof. –  May 31 '11 at 14:39
  • I could have swore I read that somewhere... but I guess not. I should stop writing here in the middle of the night when I'm tired. – Keith May 31 '11 at 16:32
  • This is what I ended up using, along with a comment explaining its purpose. – Lauritz V. Thaulow Jun 01 '11 at 15:08
  • `Ellipsis` is however not great when you're using full type checking. 1. MyPy doesn't exclude Ellipsis from a type when it's been excluded with an if, as it does for None, or Enums per [PEP484](https://github.com/python/typing/pull/240). 2. `builtins.Ellipsis` is not valid as a type, so you can't really use it to type arguments. – Navid Khan Sep 30 '22 at 19:51
  • @NavidKhan type checking can be an issue, yes, since type checkers tend to accept the instance`None` in place of its type `NoneType`; one can do something along the lines: `EllipsisType = type(Ellipsis)` and use that in type declarations. – tzot Oct 01 '22 at 21:59
0
class Unset:
  pass

def my_method(optional_argument: str | None | Unset = Unset()):
  if not isinstance(optional_argument, Unset):
    ...
  ...

An implementation like this has the following benefits:

  1. It allows you to use None as a valid value for optional_argument.
  2. It allows for full type checking, as opposed to something like Ellipsis (...) that would throw builtins.Ellipsis is not valid as a type. (None is allowed to mean NoneType when used in type annotations)
  3. MyPy excludes Unset from a type when it's been excluded with an if, as it does for None, or Enums per PEP484.
Navid Khan
  • 979
  • 11
  • 24
  • The type of Ellipsis is `type(Ellipsis)` just like the type of None is `type(None)`. `None` is not a type but it gets preferential treatment in type checkers (ie when used in type declarations it is interpreted as `NoneType`). – tzot Oct 01 '22 at 22:02
  • Well, Ellipsis does not get preferential treatment, which makes it a bad replacement for None when using type checking. – Navid Khan Oct 03 '22 at 07:21
  • Then you should accordingly amend the bullet 2 because as it is now you're saying the obvious (Ellipsis is not a type) while not mentioning the unobvious (None is allowed to mean NoneType when used in type annotations) and thus make your answer more helpful. – tzot Oct 04 '22 at 14:41