namedtuple Comes in Handy
I've been writing a lot of Python code recently. Oftentimes I struggle with what a method should return when I have to relay more than one value back to the caller. For example:
def PaymentGateway: def do_transaction(self, target, amount, bill_code, **kwargs): """ Perform some transaction against the API. :return: whether the transaction was successful or not :rtype: bool """ # stuff happens here try: result = self.amount_transaction(tx_details) logger.info("Success: CODE=%s Details=%s" % (result.code, result.detail)) return True except GatewayException as ex: logger.error("Transaction failed: ERROR=%s reason=%s" % (ex.err_code, ex.message)) return False
The code that calls do_transaction
might look like this:
if payment_gw.do_transaction(subid, amount, bill_code, service_id, ref_code) is True: # Hooray! Succe$$! report_success("Transaction for %s was successful. Check logs for status code." % subid) else: # Boo report_failure("Transaction failed. I don't know why...")
Many times this is fine, but what if the caller needs the details from the amount_transaction
result or the GatewayException
? A quick solution is to return a dict
:
def PaymentGateway: def do_transaction(self, target, amount, bill_code, **kwargs): """ Perform some transaction against the API. :return: a dict that contains keys 'success', 'code', and 'detail' :rtype: dict """ # stuff happens here try: result = self.amount_transaction(tx_details) logger.info("Success: CODE=%s Details=%s" % (result.code, result.detail)) success_dict = { 'success': True, 'code': result.code, 'detail': result.detail, } return success_dict except GatewayException as ex: logger.error("Transaction failed: ERROR=%s reason=%s" % (ex.err_code, ex.message)) error_dict = { 'success': False, 'code': ex.err_code, 'detail': ex.message, } return error_dict
It works but it's pretty ad-hoc. The structure of whatever do_transaction
returns won't be obvious unless you dig into the code. The caller will end up like:
payment_status = payment_gw.do_transaction(subid, amount, bill_code, service_id, ref_code) if payment_status['success'] is True: # Hooray! Succe$$! report_success("Transaction for %s was successful, status code %s" % (subid, payment_status['code'])) else: # Boo report_failure("Transaction failed, because: %s" % payment_status['detail'])
Now the caller is poluted with literal strings like 'success'
, 'code'
and 'status'
. These can be hell to debug, specially if you happen to misspell one of them in your code. Even if you're using an awesome IDE like PyCharm.
An altenative to defining these ad-hoc dict structures is to use namedtuple from the collections package.
from collections import namedtuple PaymentStatus = namedtuple('PaymentStatus', ['success', 'code', 'detail']) def PaymentGateway: def do_transaction(self, target, amount, bill_code, **kwargs): """ Perform some transaction against the API. :return: whether the transaction was successful or not :rtype: PaymentStatus """ # stuff happens here try: result = self.amount_transaction(tx_details) logger.info("Success: CODE=%s Details=%s" % (result.code, result.detail)) return PaymentStatus(True, result.code, result.detail) except GatewayException as ex: logger.error("Transaction failed: ERROR=%s reason=%s" % (ex.err_code, ex.message)) return PaymentStatus(False, ex.err_code, ex.message)
namedtuple
forces us to be explicit about what do_transaction
returns. And explicit is better than implicit. For the caller, this looks like:
payment_status = payment_gw.do_transaction(subid, amount, bill_code, service_id, ref_code) if payment_status.success is True: # Hooray! Succe$$! report_success("Transaction for %s was successful, status code %s" % (subid, payment_status.code)) else: # Boo report_failure("Transaction failed, because: %s" % payment_status.detail)
This is almost as simple as our first example, and is free of string literals. And if you're using PyCharm, you can take advantage of the code completion which will know about the attributes of your new namedtuple
class:
So if your code is littered with string literals as keys for return values from methods that return dict
, consider having them return a namedtuple
instead.
Comments
Comments powered by Disqus