2

Suppose you have a function Car.find_owner_from_plate_number(plate_number) that will raise an Exception if plate is unknown and return an Owner object if plate number exists.
Now, you do not need Owner information in your script, just to know if plate number exists (ie no exception raised)

owner = Car.find_owner_from_plate_number('ABC123')
_ = Car.find_owner_from_plate_number('ABC123')
Car.fund_owner_from_plate_number('ABC123')

With first, IDE will complain that owner is not used afterwards
Second is ok since _ is a global variable, but will assign memory in line with Owner's size
Third should also do the job, cherry on the cake without consuming memory if I'm correct.

What's the best way / more pythonic between 2nd and 3rd way? I ask because I often see 2nd way but I would be tempted to say 3rd is best.

comte
  • 3,092
  • 5
  • 25
  • 41
  • 4
    absolutely the third way. If you don't need to assign the returned value to something, then don't. Other languages allow this as well, and the third way is preferred in them too. – Green Cloak Guy Oct 25 '21 at 15:02
  • 2
    I have *never* seen the second way used, unless you count unpacking (where some parts of the return get names and the unused parts get assigned to `_`). – Samwise Oct 25 '21 at 15:07
  • 1
    better to have the function return a boolean imo. raising an exception is an expensive operation, especially if the `except` block happens somewhat often. – rv.kvetch Oct 25 '21 at 15:09
  • @GreenCloakGuy you gave me the answer, thx, could you please copy/paste it as an answer so I can accept it as prefered answer. – comte Oct 25 '21 at 15:09
  • 2
    @rv.kvetch no because in such case you loose the flexiblity to have the Owner object when needed. – comte Oct 25 '21 at 15:09
  • I would say that the design is not optimal. Your method has a return value **and** a side effect. You seem to only care about the side effect. Maybe you should separate the operations. – Wombatz Oct 25 '21 at 15:10
  • 2
    @rv.ketch Raising an exception isn't as expensive in Python as it is in other languages. In fact it's often the preferred way to propagate errors. – Matthias Oct 25 '21 at 15:21

2 Answers2

1

As a rule, if you don't care about the returned value of a function, then don't assign it to anything, and it will vanish once you're done with it. This applies not only in python, but also in Java and other garbage-collected languages. In C/C++ you need to be more careful with memory management to make sure you're not leaving any memory leaks, but they do allow this behavior, and if you didn't explicitly use malloc() it's usually fine.

As for whether it's good design for find_owner_from_plate_number() to be built this way: it's okay but not great. The optimal design (more idiomatic in Java, in my experience, less so in python since exceptions are more integrated into control flow in general) would probably be

  • if plate is present, return an Owner
  • if plate is not present, return None (or the language's equivalent null value, if not writing python)

and save exception-throwing for actual error behavior that isn't expected to happen during normal use (invalid license plate format, could not connect to database, etc.). In general, try to avoid using thrown exceptions for control flow, if you can avoid it.

That said, I've seen both patterns in use at various points, and using exception behavior in this way isn't unheard of.

Green Cloak Guy
  • 23,793
  • 4
  • 33
  • 53
  • 1
    "but also probably preferred in python" Not at all. Throwing an exception on lookup failure is so common, Python has *three* builtin exceptions just to signal this – the generic ``LookupError``, the ``IndexError`` used by sequences and the ``KeyError`` used by mappings. – MisterMiyagi Oct 25 '21 at 15:19
  • My understanding in Python is that Exception can be part of flow control unlike Java for instance, since they are far cheaper. But seems it's a bigger debate than my question ! (https://softwareengineering.stackexchange.com/questions/351110/are-exceptions-for-flow-control-best-practice-in-python) – comte Oct 25 '21 at 15:22
1

TLDR: If you do not need the return value, do not assign it at all. If you need some parts of the return value, assign the rest to _.


Python explicitly has an "expression statement" that solely exists to evaluate an expression and ignore its return value. Since this is the simplest way to ignore return values, it should be the preferred approach whenever applicable.

Car.find_owner('ABC123')

When the return value should be ignored only partially, an "assignment statement" is required to assign the parts of interest. In this case, the name _ is idiomatic to mean "unused" for the parts that must be assigned to something but are not of interest; it is often used as *_¹ to ignore arbitrary many items.

# we only want the owner and ignore the model
owner, _ = Car.find_owner_and_model('ABC123')
# ignore everything but the first item
owner, *_ = Car.find_owner_model_make_and_other_stuff('ABC123')

Note that _ is still a fully functional variable which keeps its assigned object alive until the end of the scope or deletion. This is not usually an issue when used in short-lived functions, but may require explicit cleanup when used for large objects at global scope.


¹ The * is commonly known as the "star" or "splat" operator. It denotes iterable packing/unpacking.

MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119