Allow None in list initializer to return an empty list: list(None) -> []

To avoid boiler plate code:

def prepend_42(elements: Iterable[int] | None = None) -> list[int]:
    if elements is None:
        return [42]
    return [42] + list(elements)

would be equivalent to

def prepend_42(elements: Iterable[int] | None = None) -> list[int]:
    return [42] + list(elements)

# Wanted behavior
# prepend_42() -> [42]
# prepend_42([100]) -> [42, 100]

What’s wrong with this? The signature is clearer and it already works:

def prepend_42(elements: Iterable[int] = ()) -> list[int]:
    return [42] + list(elements)
9 Likes

@Nineteendo Or return [42, *elements].

6 Likes

I was already wondering if I should mention that. :slight_smile:

I don’t know why there’s a trend of using None for the default, instead of the actual value. Unless you want to allow to monkey patch sys.stdout for instance.

To me using None was natural because the natural pattern would be

def prepend_42(elements: list[int] = []):
   ...

and then you discover there’s problems with using [] as a default argument value. The stupid boiler plate solution that always works is

def prepend_42(elements: list[int] | None = None):
   if elements is None:
      elements = []
   ...

I’m now trying to transition to the use of () and frozendict() etcetera instead of using None, but the pattern of using None was easier to discover.

For example, this guide recommends the None paradigm to replaced mutable defaults: Default arguments in Python - GeeksforGeeks
Most help I found on the internet does.

1 Like

I think you best bet to make it more convenient is: PEP 505 – None-aware operators | peps.python.org and other considerations along these lines

2 Likes

How would you create a list consisting of a single None if this is added?

Don’t you mean frozenset()? Pep 416 was rejected.

It might be a matter of performance though, as an identity check is faster than a conversion to list. But in case you already support an iterable, using a tuple as default is a no-brainer. Maybe this is something linters can help with?

A list expects an iterable, not multiple arguments.

1 Like

frozendict is imported from the fozendict library frozendict · PyPI
Yes I would use frozenset too.


@dg-pb
I think getting rid of Nones would be a better improvement rather than making it easier to work with.

A lot could be improved via education.

def f(x: list|tuple = ()):
  ...

is already a better pattern than

def (f:x list|None = None):
  ...

in a myriad of ways. More readable, less boilerplate. Forces you to use good habits like not mutating function arguments.
None-aware operators sound like they would increase bad programming habits and increase silent bugs.

All that said, the pattern def f(x: list|tuple = ()) is relatively hard to discover in my experience, and not very common in code I’ve seen.

The python ecosystem would probably be significantly improved if def f(x:list ?= []) was available as an alternative, because it would reduce instances of def (f:x list|None = None), which would also reduce the need for None-aware operators.

1 Like

Probably because if you are only using tuple to allow for an immutable default, then you are almost certainly only using the argument as an Iterable or a Sequence, and don’t need to specify list or tuple as the argument type.

def f(x: Iterable = ()):
    ...

def g(x: Sequence = ()):
    ...

I don’t know why there’s a trend of using None for the default

It’s needed, when an effective mutable default is required. People are probably getting carried away with that pattern.

A disadvantage of Sequence or Iterable compared to list | tuple is that it accepts str when passing a str is often a bug.

accidentally typing g("abc") when g(["abc"]) is correct

That problem will be caught by the type checker if you use list | tuple

An advantage of those is that it accepts str as an iterable of characters, allowing you to use g("abc") when g(["a", "b", "c"]) would be correct.

So we could compare which disadvantage is more significant:
a bug vs an inconvenience that isn’t a bug

And we could compare which case is more common:
using single character strings vs multiple character strings

It’s only a bug if you expected "abc" to be allowed and treated differently from ["a", "b", "c"] in the first place. which is not the case if you initially were happy with list as a type hint.

I’m -1. IMHO passing None to a constructor 99% of the time is an error.

This looks wrong, but I’m not sure I’m understanding what you’re saying. I’m not sure who your usages of “you” refer to, taking into account that the person designing the API, and the person implementing the function, and the person calling the function can all be different people. Who is “you”?

I often use more_itertools.always_iterable to deal with these differences.

That is the correct way if you want elements to be Optional.

If you want to omit elements when calling the function, use the example provided by @Nineteendo:

The built-in list() only allows omitting the argument. Its argument is not Optional.

I agree with you that not using None is an improvement if you have a better option.

However, list | tuple with a default value isn’t an improvement? First, mutable defaults will make linters rightly complain even if you never seem to mutate the argument (it can be hard to verify that; what if you store it as a member variable?)

Second, tighter type annotations are usually better than loose ones, so either pick list or tuple, or if you truly support both because your function canonicalizes it, then choose Sequence.

Right, I think that’s for good reasons, but I guess we disagree

I guess you mean deferred evaluation? If so, then yes, this is probably the biggest motivator for it.