Allow the parameter name _ (underscore) multiple times in a function signature

One thing I’ve often noticed when implementing interfaces from third party libraries in Python is that I can’t explicitly state that I won’t be using several function parameters.

For example, if I make a custom parameter type in the excellent Click library:

class Package(ParamType):
    def convert(
        self,
        value: Any,
        _: Optional[click.Parameter],
        _: Optional[click.Context]
    ) -> str:
        if str(value) in self.packages():
            return str(value)

        # More stuff...

        self.fail(f"Unrecognized package: {value!r}")

This crashes with:

SyntaxError: duplicate argument ‘_’ in function definition

Using an underscore as a parameter name or variable name is a common and accepted way to communicate that a value won’t be used. For example, this is a common usage of destructuring statements:

_, _, three = (1, 2, 3)

That’s why it seems to me both intuitive and useful if Python allowed multiple function parameters with the same name, as long as that name is “_” (underscore).

What do other people think?

Are there any downsides to this, other than potentially being difficult to implement?


I know there are other workarounds for this issue, but they all seem really awkward and verbose, definitely not Pythonic.

Just use *_

This would work for this specific scenario, but it’s highly situational and it’s ugly to boot.

Prefix with underscore

Use variable names like “_param” and “ctx". This is not as clear as naming the parameter "”. It’s also not as well supported by tools, pyright-langserver still complains about unused parameters, unlike with a plain underscore.

Immediately delete the parameters

Like:

def convert(
    self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
) -> str:
    del param  # Not used
    del ctx  # Not used

    # ...

This has the advantage that the value is technically used (since it’s part of a del statement) and tools won’t complain, and it can’t accidentally be used later. However, I still find this verbose and awkward, not Pythonic.

2 Likes

What if the caller of the function provides the values as keyword arguments?

The parameter name is part of the interface!

If the interface expects a parameter context and you name it _ instead, the caller cannot write package.convert(value, context=cxt). Which means you have broken the interface.

More generally, functions and methods shouldn’t take parameters that aren’t used.

The only two exceptions, and even there it is a code smell, are:

  1. tramp data that exists only to be passed on;
  2. if you are implementing an interface, e.g. when overloading a method and need to duplicate their parameters.

But in both cases, you should use the same parameter names as the method you are overloading.

(I suppose a purist may argue that tramp data actually is used, it is merely used deeper in the function’s implementation.)

In your case, if you are implementing an interface, but some of the parameters are not needed, that’s a code smell. That suggests that, perhaps:

  • Your implementation is not really implementing the same interface, just a portion of it;
  • or the interface is badly designed, over-engineered, or both.

But let’s assume that, despite the smell, it actually is necessary for you to have unused parameters. That’s fine. You just state that in your documentation: “The context is not used.” You can’t change the parameter name without breaking the interface.

I suppose that there is an exception in the some interfaces may use positional-only parameters, where the name isn’t part of the interface. So now we have a tiny fraction of a fraction of a fraction:

  • the tiny fraction of code that can justify the presence of an unused parameter;
  • the tiny fraction of those where there are two or more such unused parameters;
  • and the tiny fraction of that where the parameter names are not part of the interface.

For such a tiny niche use-case, it is not worth relaxing the rule against duplicate parameter names. Just name the parameters _1, _2 etc.

1 Like

These (along with all underscore-lead parameter names, eg __, _context) are not considered unused in PyCharm. I’ve only ever used them in test code though.

In my experience, callback functions and similar interfaces rarely specify a parameter name.

This dismisses well-designed interface and frameworks, like WSGI or Ariadne, where you are expected to provide your own implementations as a callback or decorated function. These often provide arguments that you won’t need in all cases. Calling this is a “code smell” is dismissive and arguably wrong.

Right, that’s fair. So I guess for this to work, Python would have to detect that the function was overridden, then actually use the parameter names from the original function rather than underscores.

It’s my impression that implementing interfaces that take far more arguments than is usually needed is very common, but I guess I may have been pretty unfortunate with the environments I’ve worked in… Lots of interfaces ported from Java, by Java developers. I constantly miss the ability to just name my parameters “_” to get my linters and language servers to stop whining about them being unused.

Of course, I didn’t consider that this would break the function being called with keyword arguments. It may still be possible though, Python would just have to copy the parameter definition at that position from the overridden function, which is probably too much complexity added for a feature that would rarely be used.

Alternatively this might be implemented with a new type, something like:

from typing import Unused

def foo(fooz: Unused[str], baz: int):
    ...

This could act precisely like the wrapped type, but signal to linters and development tools that the value should never be used. Type checkers could even raise an error of the value is used.

Does this seem more reasonable? (Of course, I don’t know how to gauge how many people would find this useful)

I think it would be useful to have the Unused annotation. I think it should also be allowed to use Unused alone:

def foo(fooz: Unused, baz: int):
    ...

It should either be like not type-annotated fooz or equivalent of fooz: Unused[Any].

2 Likes

Not sure about that one, as it would allow calling code to pass the wrong type to the function. That’s probably a bad thing in the long run.

It seems to me that if tools like linters complain about unused variables, and don’t give you a way to (selectively) turn that warning off, then that’s an issue with your linter, not with Python.

Allowing _ to be used without warning about an unused variable is nothing more than a very limited form of the linter having a mechanism to selectively turn the “unused variable” warning off. Allowing _1, _2, etc., would be more flexible, but still not as useful as a more general method like

# lint: ignore_unused: context, data
def my_callback(something_important, context, data):
    do_something_with(something_important)
5 Likes

I guess the whole underscoring business is a moot point when it comes to function parameters, since changing the parameter name changes the interface. At least, it doesn’t help when implementing interfaces.

A comment to ignore this specific warning would be good, and it definitely already exists for pylint since you can selectively disable any warnings, but it’s not really a general solution. There are lots of language servers and code analysis tools, it would be nice to have a general way to mark a parameter as not being used. I think the type wrapper may be a good way to achieve that, but I guess I should open a new idea thread for that.

But it does help when you are implementing someone else’s interface whose values you don’t care about (e.g. a context manager where you are not going to introspect on the exception details passed into __exit__).

3 Likes

I agree with those who say this is a linter issue.

A common way to quiet linters is to assert unused identifiers:

def my_callback(something_important, context, data):
    assert context and data
    do_something_with(something_important)