I recently had a discussion about the difference of Python’s Show
When do I use assert?You use # We have widget like this: # # Step size: ( ) auto (•) manual: [___300] seconds # Bad: if self.button_auto.isChecked(): return None else: # The user provided a manual step size: return self.step_size_input.value() # Good: if self.button_auto.isChecked(): return None else: assert self.button_manual.isChecked() return self.step_size_input.value()0 should always be a number between 0 and 10”). Assertions can help others (including future-you) to understand your code and they should make your program crash if the assertion becomes false (because of a programming error). Here are some examples for when # Bad: def func(a, b): x = [a, b] ... # Some operations on "x" # x should still contain 2 elements: return *x # Good: def func(a, b): x = [a, b] ... # Some operations on "x" assert len(x) == 2, f'len(x) == {len(x)}' return *x # We have widget like this: # # Step size: ( ) auto (•) manual: [___300] seconds # Bad: if self.button_auto.isChecked(): return None else: # The user provided a manual step size: return self.step_size_input.value() # Good: if self.button_auto.isChecked(): return None else: assert self.button_manual.isChecked() return self.step_size_input.value() # Bad: def fill_list(self): # self.dest should be empty, but self.source not: while self.source_not_empty(): self.dest.append(self.take_from_source()) # Good: def fill_list(self): assert len(self.dest) == 0 assert len(self.source) > 0 while self.source_not_empty(): self.dest.append(self.take_from_source()) assert len(self.dest) > 0 Validation of input parametersSometimes, # We have widget like this: # # Step size: ( ) auto (•) manual: [___300] seconds # Bad: if self.button_auto.isChecked(): return None else: # The user provided a manual step size: return self.step_size_input.value() # Good: if self.button_auto.isChecked(): return None else: assert self.button_manual.isChecked() return self.step_size_input.value()4. In this case, you would suddenly loose all input validation. # We have widget like this: # # Step size: ( ) auto (•) manual: [___300] seconds # Bad: if self.button_auto.isChecked(): return None else: # The user provided a manual step size: return self.step_size_input.value() # Good: if self.button_auto.isChecked(): return None else: assert self.button_manual.isChecked() return self.step_size_input.value()5 also carry less semantics than “normal” exceptions – like # We have widget like this: # # Step size: ( ) auto (•) manual: [___300] seconds # Bad: if self.button_auto.isChecked(): return None else: # The user provided a manual step size: return self.step_size_input.value() # Good: if self.button_auto.isChecked(): return None else: assert self.button_manual.isChecked() return self.step_size_input.value()6 when the user passed a string and not a number, or # We have widget like this: # # Step size: ( ) auto (•) manual: [___300] seconds # Bad: if self.button_auto.isChecked(): return None else: # The user provided a manual step size: return self.step_size_input.value() # Good: if self.button_auto.isChecked(): return None else: assert self.button_manual.isChecked() return self.step_size_input.value()7 when the user passed a negative number when only positive numbers are allowed. # Bad: def sum(a, b): assert isinstance(a, int), 'a must be an int' assert a >= 0, 'a must be >= 0' ... # Good: def sum(a, b): if not isinstance(a, int): raise TypeError(f'a must be an int but is a {type(a)}') if a < 0: raise ValueError(f'a must be >= 0 but is {a}') ... There are, however, exceptions possible for this rule. Imagine the following snippet where we want to add/delete a user from a group on an LDAP server: def add_to_group(conn, user, group): _modify_group(conn, user, group, ldap3.MODIFY_ADD) def delete_from_group(conn, user, group) _modify_group(conn, user, group, ldap3.MODIFY_DELETE) def _modify_group(conn, user, group, op): assert op in {ldap3.MODIFY_ADD, ldap3.MODIFY_DELETE} conn.modify(group.dn, {'uniqueMember': [(op, [user.username])]}) In this case, it’s appropriate to use # We have widget like this: # # Step size: ( ) auto (•) manual: [___300] seconds # Bad: if self.button_auto.isChecked(): return None else: # The user provided a manual step size: return self.step_size_input.value() # Good: if self.button_auto.isChecked(): return None else: assert self.button_manual.isChecked() return self.step_size_input.value()9 should never be called by our users but only by # Bad: def fill_list(self): # self.dest should be empty, but self.source not: while self.source_not_empty(): self.dest.append(self.take_from_source()) # Good: def fill_list(self): assert len(self.dest) == 0 assert len(self.source) > 0 while self.source_not_empty(): self.dest.append(self.take_from_source()) assert len(self.dest) > 00 and # Bad: def fill_list(self): # self.dest should be empty, but self.source not: while self.source_not_empty(): self.dest.append(self.take_from_source()) # Good: def fill_list(self): assert len(self.dest) == 0 assert len(self.source) > 0 while self.source_not_empty(): self.dest.append(self.take_from_source()) assert len(self.dest) > 01. What kind of exceptions should I raise?Within you own code, the built-in exception types – especially # We have widget like this: # # Step size: ( ) auto (•) manual: [___300] seconds # Bad: if self.button_auto.isChecked(): return None else: # The user provided a manual step size: return self.step_size_input.value() # Good: if self.button_auto.isChecked(): return None else: assert self.button_manual.isChecked() return self.step_size_input.value()7, # Bad: def fill_list(self): # self.dest should be empty, but self.source not: while self.source_not_empty(): self.dest.append(self.take_from_source()) # Good: def fill_list(self): assert len(self.dest) == 0 assert len(self.source) > 0 while self.source_not_empty(): self.dest.append(self.take_from_source()) assert len(self.dest) > 03, # We have widget like this: # # Step size: ( ) auto (•) manual: [___300] seconds # Bad: if self.button_auto.isChecked(): return None else: # The user provided a manual step size: return self.step_size_input.value() # Good: if self.button_auto.isChecked(): return None else: assert self.button_manual.isChecked() return self.step_size_input.value()6 and # Bad: def fill_list(self): # self.dest should be empty, but self.source not: while self.source_not_empty(): self.dest.append(self.take_from_source()) # Good: def fill_list(self): assert len(self.dest) == 0 assert len(self.source) > 0 while self.source_not_empty(): self.dest.append(self.take_from_source()) assert len(self.dest) > 05 – are usually sufficient. Libraries often define their own exception hierarchy with their own base class. For example, Requests has # Bad: def fill_list(self): # self.dest should be empty, but self.source not: while self.source_not_empty(): self.dest.append(self.take_from_source()) # Good: def fill_list(self): assert len(self.dest) == 0 assert len(self.source) > 0 while self.source_not_empty(): self.dest.append(self.take_from_source()) assert len(self.dest) > 06 from which all other exception types inherit. This makes it a lot easier for users to catch your library’s exceptions. If you raise all kinds of exceptions (and then you don’t document that, too), your users will end up using a broad/bare excepts which is bad. # Bad: def fill_list(self): # self.dest should be empty, but self.source not: while self.source_not_empty(): self.dest.append(self.take_from_source()) # Good: def fill_list(self): assert len(self.dest) == 0 assert len(self.source) > 0 while self.source_not_empty(): self.dest.append(self.take_from_source()) assert len(self.dest) > 07 is bad because it results in all kinds of nasty side effects. For example, it would also catch (and possibly ignore) # Bad: def fill_list(self): # self.dest should be empty, but self.source not: while self.source_not_empty(): self.dest.append(self.take_from_source()) # Good: def fill_list(self): assert len(self.dest) == 0 assert len(self.source) > 0 while self.source_not_empty(): self.dest.append(self.take_from_source()) assert len(self.dest) > 08 ( # Bad: def fill_list(self): # self.dest should be empty, but self.source not: while self.source_not_empty(): self.dest.append(self.take_from_source()) # Good: def fill_list(self): assert len(self.dest) == 0 assert len(self.source) > 0 while self.source_not_empty(): self.dest.append(self.take_from_source()) assert len(self.dest) > 09) or # Bad: def sum(a, b): assert isinstance(a, int), 'a must be an int' assert a >= 0, 'a must be >= 0' ... # Good: def sum(a, b): if not isinstance(a, int): raise TypeError(f'a must be an int but is a {type(a)}') if a < 0: raise ValueError(f'a must be >= 0 but is {a}') ...0. And that’s why you should also never raise # Bad: def sum(a, b): assert isinstance(a, int), 'a must be an int' assert a >= 0, 'a must be >= 0' ... # Good: def sum(a, b): if not isinstance(a, int): raise TypeError(f'a must be an int but is a {type(a)}') if a < 0: raise ValueError(f'a must be >= 0 but is {a}') ...1. TLDR
|