9

I currently use this strategy when I cannot assign default arguments in a function's signature and/or None already has meaning.

from typing import Optional

DEFAULT = object()


# `None` already has meaning.
def spam(ham: Optional[list[str]] = DEFAULT):
    if ham is DEFAULT:
        ham = ['prosciutto', 'jamon']
    if ham is None:
        print('Eggs?')
    else:
        print(str(len(ham)) + ' ham(s).')

Error:

Failed (exit code: 1) (2607 ms)

main.py:7: error: Incompatible default for argument "ham" (default has type "object", argument has type "Optional[List[str]]")
Found 1 error in 1 file (checked 1 source file)
  • How do I type-hint ham without getting errors in mypy? or
  • What strategy should I use instead of DEFAULT = object()?
Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
  • 1
    Something I've been doing lately is: `DEFAULT = (DefaultType := types.new_class("DefaultType"))()` and then type hint using `Union[List[str], DefaultType]`. – Rick Sep 19 '21 at 01:21
  • 5
    This is an active topic in Python language development, with [PEP 661](https://www.python.org/dev/peps/pep-0661/) proposing a new sentinel creator to be added to the standard library. That PEP hasn't been accepted yet, so it may or may not become a thing, but you could probably borrow one of the proposed implementations for your own use in the mean time. I was particularly keen on one not-very-serious proposal to [make sentinels types that are their own metatype (a mind-bending idea that greatly simplifies type annotations)](https://discuss.python.org/t/pep-661-sentinel-values/9126/2). – Blckknght Sep 19 '21 at 01:24
  • I appreciate both of your comments. While I am still reviewing them, I posit that they directly address the question. I take some solace in knowing this topic isn't settled. – zatg98n4qwsb8heo Sep 19 '21 at 01:45

3 Answers3

13

As I commented, this is an active area of development in Python. PEP 661 proposes to add a sentinel function that creates a sentinel object, but until that PEP is approved, you're on your own.

You can take inspiration from some of the proposed (or rejected) options in the PEP however. One very simple approach that plays reasonably well with type hinting is to make your sentinel value a class:

class DEFAULT: pass

Now you can type-hint your function as taking a union of types that includes type[DEFAULT]:

def spam(ham: list[str]|None|type[DEFAULT] = DEFAULT):
Blckknght
  • 100,903
  • 11
  • 120
  • 169
6

Something I like to do — which is only a slight variation on @Blckknght's answer — is to use a metaclass to give my sentinel class a nicer repr and make it always-falsey.


sentinel.py

from typing import Literal 

class SentinelMeta(type):
    def __repr__(cls) -> str:
        return f'<{cls.__name__}>'

    def __bool__(cls) -> Literal[False]:
        return False


class Sentinel(metaclass=SentinelMeta): pass

main.py

from sentinel import Sentinel

class DEFAULT(Sentinel): pass

You use it in type hints exactly in the same way @Blckknght suggests:

def spam(ham: list[str]|None|type[DEFAULT] = DEFAULT): ...

But you have the added advantages that your sentinel value is always falsey and has a nicer repr:

>>> DEFAULT
<DEFAULT>
>>> bool(DEFAULT)
False
Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
  • What is the impetus for making the sentinel always falsey? (And thanks.) – zatg98n4qwsb8heo Sep 19 '21 at 16:24
  • @zatg98n4qwsb8heo essentially the same reasoning that Or Shahaf gives here — a sentinel generally represents an "empty" value, so being always falsey just feels like it makes sense imo. https://discuss.python.org/t/pep-661-sentinel-values/9126/43?u=alexwaygood – Alex Waygood Sep 19 '21 at 17:04
  • I saw that bit earlier, and it's plausible this is an improvement, but I'm not one to opine on it. – zatg98n4qwsb8heo Sep 19 '21 at 17:22
  • 1
    To anyone whom may only read this answer: consider reviewing this [helpful comment](https://stackoverflow.com/questions/69239403/type-hinting-default-parameters#comment122378679_69239403). – zatg98n4qwsb8heo Sep 19 '21 at 17:24
  • 1
    @zatg98n4qwsb8heo Opinions can differ! I think there's good reasons to prefer Blckknght's solution (keep it simple, stupid!). If there were an obvious solution to the problem, then there wouldn't be discussions regarding a possible new module in the standard library. Just thought I'd throw it out my solution to the problem, in case it was helpful :) – Alex Waygood Sep 19 '21 at 17:34
1

Nothing wrong with existing answers but I wanted to say another valid option is using Enum. In fact it's the recommended approach in PEP484 - Support for singleton types in unions:

To allow precise typing in such situations, the user should use the Union type in conjunction with the enum.Enum class provided by the standard library, so that type errors can be caught statically.

You code should then look like this:

from enum import Enum

class Default(Enum):
    token = 0

DEFAULT = Default.token

def spam(ham: list[str] | None | Default = DEFAULT):
    if ham is DEFAULT:
        ham = ["prosciutto", "jamon"]
    if ham is None:
        print("Eggs?")
    else:
        print(str(len(ham)) + " ham(s).")
S.B
  • 13,077
  • 10
  • 22
  • 49