Sole underscore in python
A sole underscore _
is often used in code snippets to drop one or more
outputs.
proc = subprocess.Popen(...)
_, errs = proc.communicate()
# do smth with errs
Why do we use _
in place of a variable name in this case? Is it a
convention or there is some magic behind? Let’s investigate with dis
.
A simple case first.
def f(some_tuple):
_, b = some_tuple
def g(some_tuple):
a, b = some_tuple
import dis
dis.dis(f)
dis.dis(g)
Output:
2 0 LOAD_GLOBAL 0 (some_tuple)
2 UNPACK_SEQUENCE 2
4 STORE_FAST 0 (_) <<< Assigned here
6 STORE_FAST 1 (b)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
vs
2 0 LOAD_GLOBAL 0 (some_tuple)
2 UNPACK_SEQUENCE 2
4 STORE_FAST 0 (a)
6 STORE_FAST 1 (b)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
Seems like no difference: the bytecode is identical and _
is treated as
a proper variable name. Let’s check if we can access it.
_, a = "hello", "world"
print(_)
Output:
hello
Good news! Python does not care about the underscore itself and does not treat it specially. This means less special cases, less unseen bugs and less things to remember.
Now, let’s check other contexts: simple context manager first.
def f():
with context_manager as _:
pass
dis.dis(f)
Output:
2 0 LOAD_GLOBAL 0 (context_manager)
2 SETUP_WITH 9 (to 22)
4 STORE_FAST 0 (_) <<< `_` is assigned as
any other name
3 6 POP_BLOCK
2 8 LOAD_CONST 0 (None)
10 DUP_TOP
12 DUP_TOP
14 CALL_FUNCTION 3
16 POP_TOP
18 LOAD_CONST 0 (None)
20 RETURN_VALUE
>> 22 WITH_EXCEPT_START
24 POP_JUMP_IF_TRUE 14 (to 28)
26 RERAISE 1
>> 28 POP_TOP
30 POP_TOP
32 POP_TOP
34 POP_EXCEPT
36 POP_TOP
38 LOAD_CONST 0 (None)
40 RETURN_VALUE
Nothing special here: _
is assigned and accessible. Exceptions:
def f():
try:
raising_function()
except Exception as _:
pass
dis.dis(f)
Output:
3 0 SETUP_FINALLY 6 (to 14)
4 2 LOAD_GLOBAL 0 (raising_function)
4 CALL_FUNCTION 0
6 POP_TOP
8 POP_BLOCK
10 LOAD_CONST 0 (None)
12 RETURN_VALUE
5 >> 14 DUP_TOP
16 LOAD_GLOBAL 1 (Exception)
18 JUMP_IF_NOT_EXC_MATCH 25 (to 50)
20 POP_TOP
22 STORE_FAST 0 (_) <<< Assigned as well!
24 POP_TOP
26 SETUP_FINALLY 7 (to 42)
6 28 POP_BLOCK
30 POP_EXCEPT
32 LOAD_CONST 0 (None)
34 STORE_FAST 0 (_) <<< and here
36 DELETE_FAST 0 (_)
38 LOAD_CONST 0 (None)
40 RETURN_VALUE
>> 42 LOAD_CONST 0 (None)
44 STORE_FAST 0 (_) <<< here as well
46 DELETE_FAST 0 (_)
48 RERAISE 1
5 >> 50 RERAISE 0
Loops:
def f():
for _ in iterator:
pass
dis.dis(f)
Output:
3 0 LOAD_GLOBAL 0 (iterator)
2 GET_ITER
>> 4 FOR_ITER 2 (to 10)
6 STORE_FAST 0 (_) <<< Assigned
4 8 JUMP_ABSOLUTE 2 (to 4)
3 >> 10 LOAD_CONST 0 (None)
12 RETURN_VALUE
All these examples seem to consider _
as any other variable name.
But there is one case when _
is treated differently. The match
statement introduced in 3.10
clearly discriminates matching against _
. Demonstrating this is
slightly non-trivial. The following single-case match code is equivalent
to assignment whatever = var
.
def f():
match var:
case whatever:
pass
dis.dis(f)
Output:
3 0 LOAD_GLOBAL 0 (var)
4 2 STORE_FAST 0 (whatever)
5 4 LOAD_CONST 0 (None)
6 RETURN_VALUE
That is: load the value in var
with LOAD_GLOBAL
and store
it into whatever
with STORE_FAST
.
Using _
in place of whatever
introduces a small change in the
bytecode.
def f():
match var:
case _:
pass
dis.dis(f)
Output:
3 0 LOAD_GLOBAL 0 (var)
4 2 POP_TOP
5 4 LOAD_CONST 0 (None)
6 RETURN_VALUE
In place of STORE_FAST
we now see POP_TOP
which simply discards var
.
The name _
is never assigned and does not exist at all. Thus, the code
is equivalent to a single statement var
which checks if var
exists
and ignores otherwise.
Obviously, there is no difference if a throw-away variable name is used
once. But the following code will likely raise a NameError
:
def f(var):
match var:
case _:
pass
print(_)
f('hello world')
NameError: name '_' is not defined
While using whatever
in place of _
is actually a valid logic.
def f(var):
match var:
case whatever:
pass
print(whatever)
f('hello world')
Output:
hello world
Why is it designed like this? I have no idea. But I very well imagine this point discussed during job interviews: python 3.10 actually treats the underscore variable in a special way.