Has there been any discussion on deprecating the use of _ in the RHS?

It can, because you are talking about a scenario where a user who is not versed in the rules of the language is confused.

there is no way for it mean 42.

Same issue. You start your argument with a fictitious user who doesn’t know how binding works in a match statement but then the same user is supposed to know that _ cannot be used in the RHS.

If you don’t mind me asking, how much code have you written that uses gettext’s _()? Perhaps your experience in this regards dwarfs mine and give you a different perspective.

I know that I have used it in a few personal projects including one where it appears 845 times (and counting) – likely very little compared with people that use Django or Flask for designing international websites. As far as I know, gettext existing tools (with the exception of some tools written by Barry Warsaw) require the use of _() to identify the strings needing translation, extract them to a .po file so that translations can be performed by humans. Because it is a well-established convention, it is easy to browse projects which support internationalization and immediately identify which strings are translatable and which ones are not.

Unless and until there are well-publicised tools that can support the use of arbitrary function names to identify translations, and that conventions are well established in terms of what the name(s) of such function(s) should be other than _, I think it is premature to discuss deprecating _ in the RHS.

Also, in a standard Python interpreter, _ can be used to recover the last output. With interpreters written in pure Python, this is done internally by using _ in the RHS. Prohibiting this usage would mean that something else would have to replace _ as a convention when using Python’s REPL.

6 Likes

xgettext supports arbitrary strings for the .pot file creation for the singular and plural forms. For a very long time I have been using S_ and P_ for example as keywords.

See xgettext(1) - Linux manual page and the --keyword option

2 Likes

My proposal would be different: match statements and linters should accept __ (double underscore) as a throwaway target.

Going further and making it a true throwaway (so even regular assignments would discard the value instead of binding it) is also more plausible for double underscore than it would be for a single underscore.

4 Likes

Let’s make it a true no-content dunder: ____
:grin:

3 Likes

Nice :slight_smile:

An additional variant did occur to me: it’s currently annoying to both accept-and-ignore keyword arguments in a function (which you may need to do for things like callback interface compatibility, or parent class interface compatibility).

At the moment, dunder-prefixed function args are a bit weird, since they are affected by name mangling inside classes, even though that isn’t a particular useful thing for the interpreter to do:

>>> def f(__dunder_arg=None):
...     return locals()
...
>>> f()
{'__dunder_arg': None}
>>> class C:
...     def m(self, __dunder_arg=None):
...         return locals()
...
>>> C().m()
{'self': <__main__.C object at 0x7fce1ccf7da0>, '_C__dunder_arg': None}

It makes me wonder if we could repurpose __dunder_arg to mean “Accept dunder_arg as an argument, but throw it away instead of binding it as a local variable”.

That said, my current approach to handling the accept-and-ignore case is to put an explicit del ignored_arg in the function body (since that counts as using it, making linters happy), and that’s definitely clearer than repurposing a leading double-underscore would be.

1 Like

That’s definitely a standard approach since Ruff and other linters silence their “unused parameter” warning if you do that.

This is a very cool idea.

You’re right: I don’t have a lot of experience with gettext’s conventional use of _(). I was under the perhaps mistaken impression that this use was totally dwarfed by the discard use of _.

This is really the main motivation for this proposal. Python programs have settled on two major uses of _: as a conventional function for i18n and as a discard variable. I was under the impression that the latter was so much more common that I would like to discourage all other uses of _. Is the gettext use very common? Is it worth protecting it?

How bad would it be to search-and-replace such cases with tr? Is tr("some string") much worse than _("some string").

I mean it’s a a chicken and egg thing: You won’t change the standard unless people are nudged away from current standard (e.g., by linters). And I think the status quo whereby _ has two meanings is inferior to a status quo where you have one convention for discard and a separate one for i18n.

If you look, I’m not suggesting that the REPL change at all.

Here’s another idea: in optimized scopes (i.e. functions, not module-level code or class bodies), if the compiler detects that _ is never read, writes to it would discard the value instead.
This means _ would not exist in locals(), and it would not appear in debuggers.
Type checkers should treat it as Any in this case.

As an optimization that only affects introspection tools (and type checkers), it might get accepted. But, I doubt it would be worth the effort – it only allows the value to be deleted sooner.

4 Likes

I never noticed that before, and it actually seems like a bug! Why are method arguments getting name mangled? I’m trying to reason about the private name mangling rules to figure out if it’s intentional or not. I agree it isn’t particularly useful!

It still “works” in that you can reference the __dunder_arg local inside m() so it probably gets mangled through the whole function.

1 Like

It’s kind of besides the point. The gettext use case predates the discard convention by many years, decades if you count the GNU gettext precedent.

You could get a sense of the magnitude of the effort by grepping the GNU Mailman source[1], which is the progenitor of flufl.i18n, string.Template, and other Python i18n support. It’s simply infeasible to change perfectly working code, even if automated, then test it, and ensure that all related tooling everywhere in the ecosystem adjusts correctly to the change.


  1. I’m not going to do it ↩︎

2 Likes

I have great deal of respect for your viewpoint, but I find it really hard to see why precedence is the deciding factor whereas prevalence is “besides the point”.

It seems like you believe, based on your experience, that the discard use is more prevalent in the entire corpus of Python code than the i18n use, whereas the i18n use predates the discard use? Does your experience agree with mine here?

I recognize that these two usages have been coexisting and could continue to coexist. However, in an ideal world, each usage would have its own separate idiom because that would be easier for code readers.

If we were to push one usage to “move over”, is it offensive to suggest that it should be the less prevalent one? Surely that disrupts less code, which makes it a more practical choice. I think there seems to be some attachment to protecting a (respectfully) antiquated usage that has come to be overshadowed.

I agree. I’m not proposing immediate deprecation. I’m proposing:

This could mitigate new code from using _ in the i18n sense, and thereby may make deprecation a decade from now easier. This is about working in small incremental steps towards a more perfect future for Python.

In existing code, you can simply disable linter rules that you don’t like.

Maybe my use of “besides the point” was inelegant. I personally don’t think this is a problem in need of a fix. I’ll just wait for the PEP before chiming in any further. :wink:

I think you’re either overstating the problems caused by the current situation, or pursuing this idea out of some desire for an “ideal world”, as you put it.

In reality, though, I don’t think this is a big enough problem to be worth changing anything. I’ve spent more of my time reading this discussion than I have dealing with issues related to this in the whole of my programming career…

9 Likes

There’s just no way we’re going to break existing code, antiquated or not, no matter what the perceived value of doing so may be.

5 Likes

I think there’s one problem worth solving: being able to genuinely discard items from tuple unpacking.

Assigning to __, and then deleting it afterwards works, but it upsets typecheckers if you do it multiple times in the same function. Ignoring multiple function args has even more problems.

I also still regularly get Stack Overflow rep from my answer about single underscores at What is the purpose of the single underscore "_" variable in Python? - Stack Overflow, which suggests the triple purpose of the single underscore does confuse people.

1 Like

I’m really curious how often this is actually important to do, versus something that people think they should do. If you’re repeatedly assigning stuff to _, the old objects will be cleaned up fairly quickly by garbage collection. Unless the final assignment to _ is a really huge object, I doubt it makes a huge difference?

For me, it’s mostly a matter of getting linters and typecheckers to stop complaining about it.

If they were all consistent in treating __ as a throwaway variable of type Any that should never appear on the RHS of any operation other than del, I’d find the status quo much less irritating.

Actually achieving that level of consistency would likely require a PEP though, and if we’re going to go that far, we may as well go all the way and propose making __ a hard keyword for a universally non-binding assignment target (without the restriction to match statements that _ suffers from in that role).

5 Likes

IIUC this is related to underscores on the LHS and not RHS right?

If attempting to assign to __ has no effect, then attempting to read from __ is guaranteed to fail, so what __ means as an assignment target ends up affecting what’s valid on the RHS as well.

Continuing to allowing del __ would just be a backwards compatibility aid, so unaffected code like the following didn’t stop working:

first, *__, last = iterable
del __

A true throwaway variable could also allow for some interesting additional features, like specifying that iteration will stop without exhausting the iterator if *__ is the last item in a tuple unpacking operation. Given that feature, it becomes much easier to incrementally consume stateful iterators:

itr = iter(iterable)
command_name, *__= itr
command = named_commands[command]
command(*itr)

You can even safely consume infinite iterators that way. (Before anyone suggests using next(), that throws StopIteration if the iterable is finished, whereas tuple unpacking throws a far more informative ValueError that specifies not only how many values were expected, but also how many were actually available before StopIteration was thrown)

You could also force evaluation of an entire iterable without keep more than a single value in memory:

*__, last = iterable

The backwards compatibility arguments for not doing this for _ are valid, but those arguments don’t apply to __, as the only conventional use of that is as a throwaway variable for people that want to avoid even potential conflicts with the other two uses of _.

1 Like

Do you ever find yourself cheating it by explicitly declaring _ as Any?

_: Any
x, _ = alpha()
_, _, _, y = beta()

I’ve run into similar situations, but I’ve always solved them by adding an _-prefixed name, as in…

x, _x_index = alpha()
_, _, _, y = beta()

I ask partly out of pure curiosity, but also because _: Any is the closest thing you can do today, and it works just fine.


Regarding __, I have seen code in the wild which uses it for signatures which take any kwargs but don’t use them. Stuff like

def make_constant_converter(value: Any) -> MyConverterType[Any]:
    def _converter(*_, **__):
        return value
    return _converter  # type: ignore

where MyConverterType is some protocol, and we’re conforming to it but we don’t actually care about it for implementation.

I’m not sure if it has any bearing or causes issues, but I’m just noting that I’ve seen real use of __ as an identifier.


My overall take on this thread is that “practicality beats purity”.[1]


  1. I don’t like quoting the Zen because it’s so often misapplied or treated as gospel, but this one does seem appropriate. ↩︎