Revive PEP 640 - Unused variable syntax

I was wondering if we could revive PEP 640 (rejected because it received too little support from core devs).

? = ...  # linter: consider removing assignment
x, ?, z = ...
x, *?, z = ...
for ? in range(3): ...  # including comprehension forms
for x, ?, z in matrix: ...  # including comprehension forms
with open(f) as ?: ...  # linter: consider removing alias
with func() as (x, ?, z): ...

Advantages over _:

  • Assignments wouldn’t happen
  • Doesn’t interfere with internationalisation
  • Doesn’t shadow the previous result in REPL

My addition: If we allow to specify a name, it could also be used to maintain LSP, as a leading _ doesn’t work.

class Spam:
    eggs: int = 0

    def set_eggs(self, eggs: int) -> None:
        self.eggs = eggs

class DummySpam1(Spam):
    # Method `set_eggs` could be a function, class method, or static method
    # Unused method argument: `eggs`
    def set_eggs(self, eggs: int) -> None:

class DummySpam2(Spam):
    def set_eggs(?self, ?eggs: int) -> None:

class DummySpam3(Spam):
    def set_eggs(?self, ?eggs: int) -> None:
        # Undefined name `self`, `eggs`
        self.eggs = eggs


>>> spam = DummySpam2()
>>> spam.set_eggs(eggs=1)
>>> spam.eggs

Real world example that would benefit from this. This also allows to document the unused arguments:

?drive, root, tail = posixpath.splitroot(path)

I might be mistaken, but could bar() be implemented faster than foo() as arguments don’t need to be processed?

def foo():

def bar(*?, **?):


What has changed since this was last suggested?

1 Like

The old proposal didn’t allow for named throwaway arguments.

We might not need to reserve a new character, though:

def set_eggs(.self, .eggs: int) -> None: ...

cc @thomas.

Frankly, I don’t see a usecase here or anything having changed since that was rejected. you can already name your throwaway arguments by common convention:

scheme, _netloc, _path, params, query, fragment = urlparse(URL)

in this particular case, it’s a named tuple, you can just do:

parsed = urlparse(URL)

and use parsed.scheme, parsed.params, etc. directly

It’s not possible to optimize beyond this scope in python just by marking it as an unused value. Iteration is stateful and is allowed to have side effects, so the interpreter still needs to retrieve each value when unpacking. Not assigning it to a local value is something that can be explored without new syntax with the work on the optimizing interpreter/JIT.

1 Like

With locals, yes, this can be explored as a pure optimization (anything that’s STORE_FASTed and never subsequently LOAD_FASTed can be optimized out). Doesn’t work for other assignment types though.

TBH I’m not sure how much value this would even have, though.

1 Like

And class methods? OK, it’s niche (feel free to disregard it as a minor issue), but removing the parameters or using a leading _ violates LSP:

class DummySpam1(Spam):
    def set_eggs() -> None:

class DummySpam2(Spam):
    def set_eggs(_self, _eggs: int) -> None:

Therefore, I need a lot of comments to suppress the warning for all my type checkers, because I don’t like to use # type: ignore

Only this was valid in the old proposal:

?, root, tail = posixpath.splitroot(path)

Then type checkers and linters should be smarter. Adding a language feature to help suppress unnecessary linter warnings is not a good motivation. PyCharm for example explicitly doesn’t warn me about the unused parameters in these situations.


Class method overrides are a specific example of a more general case of writing a function to fit a protocol. Another notable example of the same problem is event handler functions; if you want to respond to a button click, you usually have to accept some sort of argument (which gives, for example, information about event timings, which mouse button or pointer was used, etc), even if you don’t need it.

Usually, these sorts of protocols are defined in terms of positional arguments. So renaming it to have a leading underscore would be valid, albeit a little confusing. Personally, though, I would prefer to keep the “correct” parameter name, as otherwise it creates a sharp distinction between the functions that use the argument and the ones that don’t - even if they are otherwise filling the same sort of slot.

The only problem here is the type checker warning. There’s no actual code problem, just a type checker problem. That strongly suggests that the solution is a type checker solution. Is “# type: ignore” a problem? Describe how, not just that you don’t want to use it, and maybe a solution can be found.


Adding new syntax isn’t backportable at all, and to top things off, the 3.13 beta freeze was today, meaning that the earliest this could make it in is 3.14. If this were to become the norm, then the earliest many libraries could safely adopt it would be in October 2029.

Well, that includes the simplest type checkers. I use multiple at the strictest settings to find the most issues.

Additionally, type checkers have a lot of work determining this, and in case of regular functions they simply can’t figure this out.

Thanks for the information. That makes it a bit less niche than I thought.

This new syntax would allow you to keep the correct argument name (keyword addressable), but make it not accesible. See it as a type hint.

Yes that is a problem, it disables all inspections on that function, so I need to do this individually.

We could add typing.Unused (or import it from _typeshed in a TYPE_CHECKING block).
There could still be a shorter syntax for new versions though. And having to wait untill 3.14 applies to all new features that are being discussed right now.

typing.Unused is an interesting idea, but how would that be better than just using _?

Well it’s longer, but the argument can use the intended name, allowing it to follow LSP.

If you’re saying that this would be a typehint to mark that a variable isn’t ever used, then just disable the specific warning whatever linter is giving you for an unused variable. That’s not a type system issue. If instead, you want it to not be allowed to pass that variable, then it’s still an LSP issue even with the same name as it’s indicated as not something which should be used as a safe replacement.

extra syntax or constructs in the standard library should help users. If it only exists to shut up a misbehaving lint, just disable that linter rule instead.

1 Like

I already use that, because the signature is what I intended:

class AnsiContext(TerminalContext):
    """Class for ansi contexts decorators."""
    def _disable(self) -> None:  # noqa: PLR6301
        print(end=end, flush=True)
    def _enable(self) -> None:  # noqa: PLR6301
        print(end=start, flush=True)

Try not to repeat arguments though.

I want to allow to pass it, not being able to use the parameter value could be helpful. Not allowing that, is precisely what violates LSP. I’m not sure what you mean with the rest of this statement.

In the ideal world, I would like to be able to write code without tripping up the type checker.
I only globally disable inspections that are way too strict (like not allowing print()).

How would this help with that? As I said above in the part you decided not to quote, unused variables are not a type system issue. This sounds more like you have a problem with specific tools enforcing something beyond what belongs in the type system and should take it up with that tool or disable that rule. I have several public and private code bases that are type-checked with strict type-checking that have unused variables in some functions due to being used as callbacks. I’ve not run into an issue with a type checker as a result.


It makes intent of the user clear: implementing a protocol and not using the wrong variable or forgetting to use it. So, this is a useful check to have:

def print_point(x, y):  # [unused-argument]
    print(f"Point is located at {x},{x}")

Isn’t that because the callback function is annotated with a callable (so it only uses positional only arguments)?

No, it’s because I don’t rename the parameters when the names are prescribed. I keep it pragmatic and understand when things are being used to conform to an existing API, and as a result, my type checker doesn’t complain about type errors.

If you have a tool causing you issues because it isn’t smart enough to look at how a function is used and see that the parameter needs to exist, that’s not a strong reason to shove more things into the type system or add syntax, that’s a strong reason to take it up with the tool or stop using that tool. Detecting that a parameter needs to exist for compatibility with use is something that the type system already supports.

1 Like

This reads as admonishing the person you are responding to, which I do not think is appropriate. If that was not your intent, perhaps reword to clarify what you meant.

1 Like