It is always a bad idea to write a method returning a null
value because it requires the client to remember to check for null:
- it is foisting problems upon the caller methods, postponing conditional checks and creating work for later on that one might forget to implement.
- it invites errors; all it takes is one missing
none
checks to have your application spinning out of control. - if you are still tempted to return
none
from a method, consider throwing an exception or special-case object instead.
Note: this works well for methods returning iterable types e.g. list, set, strings… as you can just return an empty list. Returning an empty custom-object such as instantiated class is more hairy. In such edge-case only, you can return null.
from config.constants import REGISTERED_ITEMS
def retrieve_registered_item_information(item):
if item is not None:
item_id = item.get_id()
if item_id is not None:
return REGISTERED_ITEMS.get_item(item_id).get_info()
As a demonstration for our second aforementioned point, did you noticed the fact that there wasn’t a null
check in the last line? What about the item not being retrieved among the REGISTERED_ITEMS but you, still trying to access the get_info()
method of a None
element? You will get an error for sure.
Example
You have the following structured json object you want to extract the id from:
{
"id": "42",
"name": "some_name",
"data": [...]
}
def get_object_id(object: dict) -> str | None:
candidate_id = None
try:
candidate_id = object["id"]
except KeyError as message:
logger.error(f"Error retrieving the object id: {message}")
return candidate_id
The above method is not ideal:
- You have a mixed type between
str
andNone
. You do not want your method to be schizophrenic but rather it to be type-consistent instead. - Some python versions do not accept type annotations with
|
operators. Python 3.9+ solves this problem.
Instead, always favour the following accessor method as a nice remedy:
def get_object_id(object: dict) -> str:
candidate_id = ""
try:
candidate_id = object["id"]
except KeyError as message:
logger.error(f"Error retrieving the object id: {message}")
return candidate_id
There are multiple reasons and benefits for that:
- You remove the returned type ambiguity and the returned type is consistent. Whatever might happens, you always return a string value.
- It removes the type annotation error you might get on the old python versions otherwise.
Caution: Last but not the least, note that python always implicitly returns a None
value; wether you add a return statement or not. The three following code snippets are equivalent:
def foo():
pass
def faa():
return
def fii():
return None
You can try it yourself:
python> result = foo()
python> print(result)
None
The advantage of explicitly using a return
is that it acts as a break
statement:
def fuu():
return 42
a = 5
return a
python> fuu()
42
Notes:
- We have used a
logger
object to handle logs for us. More on pythonlogging
in this article (in progress). - We have prefixed the names of our accessor methods using
get
. More on how to find meaningful names for your variables in this article (in progress).
As a conclusion, you do not want to rock the boat. Be careful when returning a null
value and always favour other options. Your code will be way cleaner and you will minimize the chance of getting an error 🛶