This article will not cover spaghetti code or nasty one liners, but rather complain and discuss commonly found patterns which range from mildly annoying or unaesthetic all the way to outright dangerous. Patterns discussed here are ones I have come across in production code. Note that all production code has been abstracted away and may come from open-source software or industry code bases. Some patterns discussed might mainly occur in ML or scientific code bases, however most will be relevant to all Python programmers.
We do not offer solutions to each antipattern, but do try to discuss why these problems may occur. It's important to recognise that problems deeper in a code base, such as nasty funcs to handle edge cases with a dozen conditionals, are often a symptom of bad upstream design.
Nones & NaNs
From optional args and returns to placeholder variables, None is the bane of clean code.
could this arg be None? maybe...
Here is the worst example I have ever seen of what optional args can do to a code base:
def multiply_potentially_nones(arg1: Optional[float] = None,
arg2: Optional[float] = None) -> float:
if arg1 is not None and arg2 is not None:
return arg1*arg2
if arg1 is None and arg2 is not None:
return arg2
if arg1 is not None and arg2 is None:
return arg1
While this could be simplified to something like:
arg1 = arg1 if arg1 is not None else 1.0
arg2 = arg2 if arg2 is not None else 1.0
return arg1*arg2
which is mildly cleaner, it's still painful to read and quite hacky.
if not None
This brings us to the
x = x if x is not None else replacement_var
or
x = replacement_var if x is None else x
or
We now come to the first dangerous pattern! Using conditional statements to abuse Pythons truethiness/falsiness of various datatypes. In case you aren't aware, here's the behavior that is abused:
print(None or "hi there")
>>> hi there
In production this hides and looks like:
x: dict = x or {}
which is often hidden tucked away in the return statement.
So why is this so bad?
Not only None
is falsy!
print([] or "hi there")
print("" or "hi there")
print({} or "hi there")
print(0 or "hi there")
print(0.0 or "hi there")
print statements are perhaps not the best way to highlight this, you can also do bool([])
.
So doing:
x: dict = x or {}
will return {}
if x is any falsy value, rather than a None. This is extremely dangerous, for example imagine our previous func multiply_potentially_nones
:
def multiply_potentially_nones(arg1: Optional[float] = None,
arg2: Optional[float] = None) -> float:
arg1 = arg1 or 1.0
arg2 = arg2 or 1.0
return arg1*arg2
print(multiply_potentially_nones(5, None))
>>> 5.0 # wooo!
print(multiply_potentially_nones(5, 0.0))
>>> 5.0 # oh dear
Other Common Antipatterns
I'll also include a section detailing common issues
Mutable Default Args
Using mutable types as default args, such as:
def f(arg1: list = []):
...
is either irresponsible (when exploiting the mutable property is not intended), or hacky!
Gotta Catch them All!
Unlike Pokemon, you should not catch all your exceptions.
try:
...
except:
print("Glad we avoided that error!")
Globals
x: int = 0
def f():
global x
x += 1
Obj.__magic__
x: dict = ...
x.__len__()
Python bypasses magic methods for performance
and many more!
- assigned lambdas
- methods that should be funcs or staticmethods
- methods that should be properties
- overwriting builtins
- not leveraging object structures for iteration (eg
.items()
) etc...